Як натякнути приєднанню багатьох до багатьох у SQL Server?


9

У мене є 3 "великих" таблиці, які об'єднуються на пару стовпців (обидві int).

  • Таблиця1 має ~ 200 мільйонів рядків
  • Таблиця2 має ~ 1,5 мільйона рядків
  • Таблиця3 має ~ 6 мільйонів рядків

Кожна таблиця має кластерний індекс Key1, Key2і потім ще один стовпець. Key1має низьку кардинальність і дуже перекошений. На це завжди посилається в WHEREпункті. Key2ніколи не згадується в WHEREпункті. Кожне приєднання - багато-до-багатьох.

Проблема полягає в оцінці кардинальності. Оцінка виходу кожного з'єднання стає меншою замість більшої . Це призводить до остаточних оцінок низьких сотень, коли фактичний результат перетворюється на мільйони.

Чи є якийсь спосіб, щоб я домігся СЕ робити кращі оцінки?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Я спробував рішення:

  • Створення статистики для багатьох стовпців Key1,Key2
  • Створення тонн відфільтрованої статистики Key1(Це дуже допомагає, але я закінчую тисячами створених користувачем статистичних даних у базі даних.)

План маскування виконання (вибачте за погану маскування)

У випадку, коли я дивлюся, результат має 9 мільйонів рядків. Новий СЕ оцінює 180 рядів; спадщина CE оцінює 6100 рядків.

Ось відтворюваний приклад:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Відповіді:


5

Щоб зрозуміти, оптимізатор вже знає, що це об'єднання багатьох до багатьох. Якщо ви змушуєте злиття приєднується та дивитесь на передбачуваний план, ви можете побачити властивість оператора приєднання, яка повідомляє, чи може об'єднання бути багато-багато-багато. Проблема, яку вам тут потрібно вирішити, - це зібрання оцінок кардинальності, імовірно, таким чином, ви отримаєте більш ефективний план запитів для тієї частини запиту, яку ви залишили.

Перше, що я б спробував, - це помістити результати з’єднання з Object3та Object5в тимчасову таблицю. Для плану, який ви опублікували, це лише один стовпець на 51393 рядках, тому він навряд чи повинен займати пробіл у tempdb. Ви можете зібрати повну статистику на темп-таблиці, що тільки для цього може бути достатньо, щоб отримати достатньо точну остаточну оцінку кардинальності. Також Object1може допомогти збір повної статистики . Оцінки кардинальності часто погіршуються, коли ви переходите від плану справа наліво.

Якщо це не працює, ви можете спробувати ENABLE_QUERY_OPTIMIZER_HOTFIXESпідказку запиту, якщо у вас його ще немає на рівні бази даних або сервера. Microsoft за цим параметром блокує виправлення, що впливають на плани для продуктивності SQL Server 2016. Деякі з них стосуються оцінок кардинальності, тому, можливо, вам пощастить, і одне з виправлень допоможе у вашому запиті. Ви також можете спробувати використати застарілий оцінювач кардинальності з FORCE_LEGACY_CARDINALITY_ESTIMATIONпідказкою на запит. Деякі набори даних можуть отримати кращі оцінки за старим CE.

В крайньому випадку ви можете вручну збільшити оцінку кардинальності за будь-яким фактором, який вам подобається, використовуючи MANY()функцію Адама Маханіка . Я говорю про це в іншій відповіді, але схоже, що посилання мертва. Якщо вам цікаво, я можу спробувати щось викопати.


make_parallelФункція Адама звикає, щоб допомогти пом'якшити проблему. Я буду дивитись many. Здається, це досить груба допомога.
Стівен Хіббл

2

Статистика SQL Server містить лише гістограму для провідного стовпця об'єкта статистики. Тому ви можете створити відфільтровану статистику, яка надає гістограму значень для Key2, але лише серед рядків із Key1 = 1. Створення цих відфільтрованих статистичних даних у кожній таблиці фіксує оцінки та призводить до поведінки, яку ви очікуєте на тестовий запит: кожне нове приєднання не впливає на остаточну оцінку кардинальності (підтверджено в SQL 2016 SP1 та SQL 2017).

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

Без цих відфільтрованих статистичних даних SQL Server буде використовувати більш евристичний підхід до оцінки кардинальності вашого приєднання. Наступний документ містить хороші описи на високому рівні деяких евристик, які використовує SQL Server: Оптимізація Ваших Планів запитів за допомогою Оцінювача кардинальності SQL Server 2014 .

Наприклад, додавання USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')підказки до вашого запиту змінить евристичний вміст стримування, щоб припустити деяку кореляцію (а не незалежність) між Key1предикатом і Key2предикатом приєднання, що може бути корисним для вашого запиту. Для остаточного тестового запиту ця підказка збільшує оцінку кардинальності від 1,175до 7,551, але все ще трохи соромляться правильної 20,000оцінки рядків, отриманої з відфільтрованою статистикою.

Інший підхід, який ми використовували в подібних ситуаціях, полягає в витягуванні відповідного підмножини даних у #temp таблиці. Особливо зараз, коли новіші версії SQL Server більше не охоче записують таблиці #temp на диск , у нас були хороші результати при такому підході. Опис Вашого приєднання багатьох до багатьох означає, що кожна окрема таблиця #temp у Вашому випадку буде порівняно невеликою (або принаймні меншою, ніж кінцевий набір результатів), тому такий підхід, можливо, варто спробувати.

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2

Ми широко використовуємо відфільтровану статистику, але ми робимо їх по одному Key1на кожну таблицю. Зараз у нас їх тисячі.
Стівен Хіббл

2
@StevenHibble Хороший момент, що тисячі відфільтрованих статистичних даних можуть ускладнити управління. (Ми також бачили, що це негативно впливає на час складання плану.) Це може не відповідати вашому випадку використання, але я також додав ще один підхід таблиці #temp, який ми успішно використовували кілька разів.
Джефф Паттерсон

-1

Досяжність. Немає реальної основи, крім спроб.

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.