Хеш-соединение между таблицами master / detail дает слишком низкую оценку количества элементов

9

При объединении основной таблицы с подробной таблицей, как я могу побудить 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 тысяч строк!

Джастин Грант
источник

Ответы:

6

Если предположить, что улучшения нельзя добиться, если что-то сделать со статистикой или с помощью устаревшей версии CE, то самым простым способом решения вашей проблемы является замена INNER JOINна LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Если у вас есть внешний ключ между таблицами, вы всегда фильтруете один и тот же SaleDateдиапазон для обеих таблиц и SaleDateвсегда сопоставляете таблицы, тогда результаты вашего запроса не должны изменяться. Может показаться странным использовать внешнее объединение, подобное этому, но подумайте об этом как об информировании оптимизатора запросов о том, что объединение с VisitTargetтаблицей никогда не уменьшит количество строк, возвращаемых запросом. К сожалению, внешние ключи не влияют на оценку количества элементов, поэтому иногда вам нужно прибегнуть к подобным приемам. (Предложение Microsoft Connect: сделайте оценки оптимизатора более точными, используя метаданные ).

Возможно, что написание запроса в этой форме не будет работать хорошо, в зависимости от того, что еще происходит в запросе после объединения. Вы можете попробовать использовать временную таблицу для хранения промежуточных результатов набора результатов с наиболее важной оценкой мощности.

Джо Оббиш
источник