Хеш-з'єднання між основними / детальними таблицями дає занадто низьку оцінку кардинальності


9

Як приєднати головну таблицю до таблиці деталей, як я можу заохотити SQL Server 2014 використовувати оцінку кардинальності більшої (детальної) таблиці як оцінку кардинальності виходу об'єднання?

Наприклад, при приєднанні 10K головних рядків до рядків деталей 100K, я хочу, щоб SQL Server оцінював приєднання в 100K рядків - те саме, що і приблизна кількість рядків деталей. Як я повинен структурувати запити та / або таблиці та / або індекси, щоб допомогти оцінювачу 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,1 М проти 0,5 М. Це спричиняє проблеми з продуктивністю (наприклад, розлив на 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 збірки 12.0.5557.0 на Windows Server. Не ввімкнено жодних прапорів слідів. Рівень сумісності баз даних - це SQL Server 2014. Ми спостерігаємо однакову поведінку на декількох різних SQL серверах, тому, мабуть, це не проблема, пов’язана з сервером.

У оцінці кардинальності SQL Server 2014 є оптимізація, саме така поведінка, яку я шукаю:

Новий CE, однак, використовує більш простий алгоритм, який передбачає, що між великою та малою таблицею існує асоціація об'єднання «багато хто». Це передбачає, що кожен ряд у великій таблиці відповідає точно одному рядку в маленькій таблиці. Цей алгоритм повертає передбачуваний розмір більшого введення як об'єднаність.

В ідеалі я міг би отримати таку поведінку, де оцінка кардинальності приєднання була б такою ж, як і оцінка для великої таблиці, хоча моя "маленька" таблиця все одно повернеться понад 100 К рядків!

Відповіді:


6

Якщо припустити, що жодних поліпшень неможливо досягти, зробивши щось із статистикою або скориставшись застарілим СЕ, то найпростіший спосіб вирішити вашу проблему - змінити 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. Зробіть точніші оцінки оптимізатора за допомогою метаданих ).

Можливо, що запит у цій формі не буде добре працювати залежно від того, що ще буде в запиті після з'єднання. Ви можете спробувати скористатися таблицею темпів, щоб провести проміжні результати набору результатів з найважливішою оцінкою кардинальності.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.