Почему оптимизатор запросов выбирает совершенно разные планы запросов?
Давайте дадим следующую таблицу в 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), я получил совершенно разные планы.
-- 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)
Если я посмотрю на планы запросов, то ясно, что он сначала хочет присоединиться к поиску ключа вместе с некластеризованным поиском индекса, а затем он делает окончательное соединение с некластеризованным индексом в случае Q1 (что плохо). Гораздо лучшее решение в случае Q2: он сначала присоединяется к некластеризованным индексам, а затем выполняет окончательный поиск ключа.
Вопрос в том, почему это так, и я могу как-то улучшить его?
В моем интуитивном понимании гистограмм должно быть легко оценить правильный результат для обоих вариантов предикатов (b.predicate_col <= 0 vs. b.predicate_col = 0
), поэтому почему разные планы запросов?
РЕДАКТИРОВАТЬ:
На самом деле, я не хочу менять индексы или физическую структуру таблицы. Я хотел бы понять, почему он выбирает такой плохой план запроса в случае Q1. Поэтому мой вопрос в точности такой: почему он выбирает такой плохой план запроса в случае Q1 и могу ли я улучшить, не изменяя физический дизайн?
Я проверил оценки результатов в плане запроса, и оба плана запроса имеют точные оценки числа строк для каждого оператора! Я проверил структуру памятки результатов (OPTION (QUERYTRACEON 3604, QUERYTRACEON 8615, QUERYTRACEON 8620)
) и правила, применяемые во время компиляции (OPTION (QUERYTRACEON 3604, QUERYTRACEON 8619, QUERYTRACEON 8620)
), и кажется, что он завершает поиск плана запроса один раз он ударил по первому плану. Это причина такого поведения?
Ответы
Ответ 1
Это связано с тем, что 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, оптимизатор не может использовать остальные столбцы в индексе, поэтому, когда вы помещаете неравенство в [predicate_col], вы оказываете остальную часть индекса бесполезной, SQL не может в полной мере использовать индекс и создает альтернативный (плохой) план. [join_col] - последний столбец в индексе, поэтому во втором запросе SQL все еще может полностью использовать индекс.
Причина, по которой SQL выбирает Hash Match, заключается в том, что она не может гарантировать порядок данных, выходящих из таблицы B. Неравенство делает [join_col] в индексе бесполезным, поэтому SQL должен готовиться к несортированным данным в соединении, хотя количество строк одинаково.
Единственный способ исправить вашу проблему (даже если вам это не нравится) - это изменить Индекс, чтобы столбцы равенства попадали перед столбцами неравенства.
Ответ 2
Ответ Ok может быть также из Statistics and histogram
точки Statistics and histogram
.
Ответ может быть также с точки зрения index structure
.
Хорошо, я пытаюсь ответить на это из index structure
.
Хотя вы получаете одинаковый результат в обоих запросах, потому что нет predicate_col < 0 records
Когда в composite index
есть Range predicate
, оба индекса не используются. Также может быть так много других причин, по которым индекс не используется.
-- 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, тогда мы можем создать еще один составной индекс.
-- 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
или нет, это другое обсуждение.