При объединении основной таблицы с подробной таблицей, как я могу побудить SQL Server 2014 использовать оценку мощности таблицы большего размера (подробная) в качестве оценки мощности вывода соединения?
Например, при объединении 10К мастер-строк в 100КБ подробных строк я хочу, чтобы SQL Server оценивал объединение в 100К строк - так же, как и предполагаемое количество подробных строк. Как мне структурировать свои запросы и / или таблицы и / или индексы, чтобы помочь оценщику SQL Server использовать тот факт, что каждая строка сведений всегда имеет соответствующую главную строку? (Это означает, что объединение между ними никогда не должно уменьшать оценку количества элементов.)
Вот больше подробностей. В нашей базе данных есть пара таблиц «главный / подробный»: VisitTarget
одна строка для каждой транзакции продаж и VisitSale
одна строка для каждого продукта в каждой транзакции. Это отношение один ко многим: одна строка VisitTarget в среднем на 10 строк VisitSale.
Таблицы выглядят так: (я упрощаю только соответствующие столбцы для этого вопроса)
-- "master" table
CREATE TABLE VisitTarget
(
VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
SaleDate date NOT NULL,
StoreId int NOT NULL
-- other columns omitted for clarity
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate
ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);
-- "detail" table
CREATE TABLE VisitSale
(
VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
VisitTargetId int NOT NULL,
SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
StoreId int NOT NULL, -- denormalized; copied from VisitTarget
ItemId int NOT NULL,
SaleQty int NOT NULL,
SalePrice decimal(9,2) NOT NULL
-- other columns omitted for clarity
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate
ON VisitSale (SaleDate)
INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale
WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId
FOREIGN KEY (VisitTargetId)
REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
CHECK CONSTRAINT FK_VisitSale_VisitTargetId;
По соображениям производительности мы частично денормализовали, скопировав наиболее распространенные фильтрующие столбцы (например SaleDate
) из основной таблицы в строки каждой таблицы сведений, а затем добавили закрывающие индексы в обеих таблицах, чтобы лучше поддерживать запросы с фильтрацией по дате. Это прекрасно работает для уменьшения количества операций ввода-вывода при выполнении запросов с фильтрацией по дате, но я думаю, что этот подход вызывает проблемы оценки количества элементов при объединении основных и подробных таблиц.
Когда мы объединяем эти две таблицы, запросы выглядят так:
SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt
JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
vs.SaleDate BETWEEN '20170101' and '20171231'
and vt.SaleDate BETWEEN '20170101' and '20171231'
-- more filtering goes here, e.g. by store, by product, etc.
Фильтр даты в таблице сведений ( VisitSale
) является избыточным. Он предназначен для включения последовательного ввода-вывода (он же оператор поиска индекса) в таблице сведений для запросов, отфильтрованных по диапазону дат.
План для таких запросов выглядит следующим образом:
Фактический план запроса с той же проблемой можно найти здесь .
Как вы можете видеть, оценка количества элементов для объединения (всплывающая подсказка в левом нижнем углу на рисунке) слишком мала в 4 раза: фактическое значение 2.1M против предполагаемого 0.5M. Это вызывает проблемы с производительностью (например, разлив в базу данных tempdb), особенно когда этот запрос является подзапросом, который используется в более сложном запросе.
Но оценки количества строк для каждой ветви объединения близки к фактическому количеству строк. Верхняя половина соединения составляет 100K против 164K. Нижняя половина соединения составляет 2,1 млн строк против 3,7 млн по оценкам. Распределение хэш-ведра также выглядит хорошо. Эти наблюдения показывают мне, что статистика для каждой таблицы в порядке, и что проблема заключается в оценке количества соединений.
Сначала я думал, что проблема в том, что SQL Server ожидает, что столбцы SaleDate в каждой таблице независимы, тогда как на самом деле они идентичны. Поэтому я попытался добавить сравнение равенства для дат продажи в условие соединения или предложение WHERE, например
ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate
или
WHERE vt.SaleDate = vs.SaleDate
Это не сработало. Это даже ухудшило показатели кардинальности! Так что либо SQL Server не использует эту подсказку о равенстве, либо что-то еще является основной причиной проблемы.
Есть какие-нибудь идеи о том, как устранить неполадки и, надеюсь, исправить эту проблему оценки кардинальности? Моя цель состоит в том, чтобы количество элементов основного / подробного объединения оценивалось так же, как и оценка для более крупного ("таблица подробностей") входного соединения.
Если это имеет значение, мы запускаем SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 на Windows Server. Флаги трассировки не включены. Уровень совместимости базы данных - SQL Server 2014. Мы наблюдаем одинаковое поведение на нескольких разных серверах SQL, поэтому вряд ли это будет проблема, специфичная для сервера.
В Оценщике мощности SQL Server 2014 есть оптимизация, которая именно то поведение, которое я ищу:
Новый CE, однако, использует более простой алгоритм, который предполагает, что существует связь «один ко многим» между большой таблицей и маленькой таблицей. Это предполагает, что каждая строка в большой таблице соответствует ровно одной строке в маленькой таблице. Этот алгоритм возвращает приблизительный размер большего ввода как количество элементов соединения.
В идеале я мог бы получить такое поведение, когда оценка количества элементов для соединения была бы такой же, как оценка для большой таблицы, даже если моя «маленькая» таблица все равно будет возвращать более 100 тысяч строк!