Почему оптимизатор запросов выбирает совершенно разные планы запросов? - PullRequest
0 голосов
/ 24 мая 2018

Давайте представим следующую таблицу в SQL Server 2016

-- generating 1M test table with four attributes
WITH x AS 
(
  SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n)
), t1 AS
(
  SELECT ones.n + 10 * tens.n + 100 * hundreds.n + 1000 * thousands.n + 10000 * tenthousands.n + 100000 * hundredthousands.n as id  
  FROM x ones,     x tens,      x hundreds,       x thousands,       x tenthousands,       x hundredthousands
)
SELECT  id,
        id % 50 predicate_col,
        row_number() over (partition by id % 50 order by id) join_col, 
        LEFT('Value ' + CAST(CHECKSUM(NEWID()) AS VARCHAR) + ' ' + REPLICATE('*', 1000), 1000) as padding
INTO TestTable
FROM t1
GO

-- setting the `id` as a primary key (therefore, creating a clustered index)
ALTER TABLE TestTable ALTER COLUMN id int not null
GO
ALTER TABLE TestTable ADD CONSTRAINT pk_TestTable_id PRIMARY KEY (id)

-- creating a non-clustered index
CREATE NONCLUSTERED INDEX ix_TestTable_predicate_col_join_col
ON TestTable (predicate_col, join_col)
GO

Хорошо, и теперь, когда я запускаю следующие запросы, имеющие немного отличающиеся предикаты (b.predicate_col <= 0 против b.predicate_col =0) У меня совершенно другие планы. </p>

-- Q1
select b.id, b.predicate_col, b.join_col, b.padding
from TestTable b
join TestTable a on b.join_col = a.id
where a.predicate_col = 1 and b.predicate_col <= 0
option (maxdop 1)

-- Q2
select b.id, b.predicate_col, b.join_col, b.padding
from TestTable b
join TestTable a on b.join_col = a.id
where a.predicate_col = 1 and b.predicate_col = 0
option (maxdop 1)

enter image description here

Если я смотрю планы запросов, то ясно, что он решает присоединиться к ключусначала выполняется поиск вместе с некластеризованным поиском индекса, а затем он выполняет окончательное соединение с некластеризованным индексом в случае Q1 (что плохо).Гораздо лучшее решение в случае Q2: он сначала присоединяется к некластеризованным индексам, а затем выполняет окончательный поиск ключей.

Вопрос в том, почему это так, и я могу как-то его улучшить?

В моем интуитивном понимании гистограмм должно быть легко оценить правильный результат для обоих вариантов предикатов (b.predicate_col <= 0 vs. b.predicate_col = 0), поэтому, почему разные планы запросов?

EDIT:

На самом деле я не хочу менять индексы или физическую структуру таблицы.Я хотел бы понять, почему он выбирает такой плохой план запроса в случае Q1.Поэтому мой вопрос точно такой: Почему он выбирает такой плохой план запроса в случае Q1 и могу ли я улучшить его без изменения физического плана?

Я проверил оценки результатовв плане запроса, и оба плана запроса имеют точные оценки числа строк каждого оператора!Я проверил структуру памятки результатов (OPTION (QUERYTRACEON 3604, QUERYTRACEON 8615, QUERYTRACEON 8620)) и правила, применяемые во время компиляции (OPTION (QUERYTRACEON 3604, QUERYTRACEON 8619, QUERYTRACEON 8620)), и кажется, что он завершил поиск плана запроса, как только достиг первого плана.Это причина такого поведения?

Ответы [ 2 ]

0 голосов
/ 04 июня 2018

Хорошо, ответ может быть также с Statistics and histogram точки зрения.

Ответ может быть с index structure также с точки зрения договоренности.

Хорошо, я пытаюсь ответить на это с index structure.

Хотя вы получаете один и тот же результат в обоих запросах, потому что нет predicate_col < 0 records

Когда есть Range predicate в composite index, оба индекса не используются.Также может быть много других причин, по которым индекс не используется.

-- Q1
select b.id, b.predicate_col, b.join_col, b.padding
from TestTable b
join TestTable a on b.join_col = a.id
where a.predicate_col = 1 and b.predicate_col <= 0
option (maxdop 1)

Если нам нужен план, как во втором квартале, мы можем создать еще один составной индекс.

-- creating a non-clustered index
CREATE NONCLUSTERED INDEX ix_TestTable_predicate_col_join_col_1
ON TestTable (join_col,predicate_col)
GO

Мы получаем запроспланировать точно так же, как Q2.

Другой способ - определить CHECK constraint в predicate_col

Alter table TestTable ADD check (predicate_col>=0)
GO

Это также даст тот же план запроса, что и Q2.

Хотя в реальномтаблица и данные, можете ли вы создать CHECK Constraint или создать другое composite index или нет - это другое обсуждение.

0 голосов
/ 24 мая 2018

Это вызвано неспособностью SQL Server использовать столбцы индекса справа от поиска неравенства.

Этот код вызывает ту же проблему:

SELECT * FROM TestTable WHERE predicate_col <= 0 and join_col = 1
SELECT * FROM TestTable WHERE predicate_col = 0 and join_col <= 1

Запросы неравенства, такие как> =или <= накладывает ограничение на SQL, Оптимизатор не может использовать остальные столбцы в индексе, поэтому, когда вы помещаете неравенство в [Предикат_кол], вы делаете бесполезным остальную часть индекса, SQL не может сделатьполное использование индекса и выработка альтернативного (плохого) плана.[join_col] - последний столбец в индексе, поэтому во втором запросе SQL все еще может полностью использовать индекс.</p>

Причина, по которой SQL выбирает Hash Match, заключается в том, что он не может гарантировать порядок данных, выходящих из таблицы B. Неравенство делает [join_col] в индексе бесполезным, поэтому SQL должен подготовиться к несортированным данным.в соединении, даже если количество строк одинаково.

Единственный способ решить вашу проблему (даже если она вам не нравится) - это изменить индекс так, чтобы столбцы равенства были перед столбцами неравенства.

...