Как намекнуть присоединение «многие ко многим» в 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 миллионов строк. Новый CE оценивает 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 Cardinality Estimator .

Например, добавление 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;
папараццо
источник