Запрос в 100 раз медленнее в SQL Server 2014, оценка количества строк в буфере строк Оценка виновника?

13

У меня есть запрос, который выполняется в 800 миллисекунд в SQL Server 2012 и занимает около 170 секунд в SQL Server 2014 . Я думаю, что я сузил это до плохой оценки кардинальности для Row Count Spoolоператора. Я немного читал об операторах спула (например, здесь и здесь ), но все еще не могу понять некоторые вещи:

  • Зачем этому запросу нужен Row Count Spoolоператор? Я не думаю, что это необходимо для правильности, так какую конкретную оптимизацию он пытается обеспечить?
  • Почему SQL Server считает, что соединение с Row Count Spoolоператором удаляет все строки?
  • Это ошибка в SQL Server 2014? Если так, я подам в Connect. Но сначала я хотел бы получить более глубокое понимание.

Примечание. Я могу переписать запрос в виде LEFT JOINили добавить индексы в таблицы для достижения приемлемой производительности как в SQL Server 2012, так и в SQL Server 2014. Поэтому этот вопрос больше о понимании этого конкретного запроса и подробном планировании, а не о как сформулировать запрос по-другому.


Медленный запрос

Смотрите этот Pastebin для полного сценария тестирования. Вот конкретный тестовый запрос, на который я смотрю:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: примерный план запроса

SQL Server считает , что Left Anti Semi Joinк Row Count Spoolотфильтрует 10000 строк до 1 строки. По этой причине он выбирает LOOP JOINдля последующего присоединения к #existingCustomers.

введите описание изображения здесь


SQL Server 2014: фактический план запросов

Как и ожидалось (всеми, кроме SQL Server!), Row Count SpoolСтроки не были удалены. Таким образом, мы зациклились 10000 раз, когда SQL Server ожидал зацикливаться только один раз.

введите описание изображения здесь


SQL Server 2012: примерный план запроса

При использовании SQL Server 2012 (или OPTION (QUERYTRACEON 9481)в SQL Server 2014) Row Count Spoolне уменьшается предполагаемое количество строк, и выбирается хеш-соединение, что приводит к гораздо лучшему плану.

введите описание изображения здесь

LEFT JOIN переписать

Для справки, вот способ, которым я могу переписать запрос, чтобы добиться хорошей производительности во всех SQL Server 2012, 2014 и 2016. Однако меня все еще интересует конкретное поведение вышеупомянутого запроса и его ошибка в новом оценщике мощности SQL Server 2014

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

введите описание изображения здесь

Джефф Паттерсон
источник

Ответы:

10

Зачем этому запросу нужен оператор Row Count Spool? ... какую конкретную оптимизацию он пытается обеспечить?

cust_nbrСтолбец в #existingCustomersобнуляемом. Если он действительно содержит какие-либо нули, правильный ответ здесь - вернуть ноль строк ( NOT IN (NULL,...) всегда будет давать пустой набор результатов.).

Таким образом, запрос можно рассматривать как

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

С катушкой rowcount, чтобы избежать необходимости оценивать

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

Больше чем единожды.

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

После обновления одной строки, как показано ниже ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... запрос завершен менее чем за секунду. Количество строк в фактических и оценочных версиях плана теперь почти на месте.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

введите описание изображения здесь

Нулевые строки выводятся, как описано выше.

Гистограммы статистики и пороги автоматического обновления в SQL Server недостаточно детализированы, чтобы обнаружить такого рода изменения одной строки. Возможно, если столбец обнуляем, возможно, было бы разумно работать на том основании, что он содержит хотя бы один, NULLдаже если гистограмма статистики в настоящее время не указывает на наличие таковых.

Мартин Смит
источник
9

Зачем этому запросу нужен оператор Row Count Spool? Я не думаю, что это необходимо для правильности, так какую конкретную оптимизацию он пытается обеспечить?

Смотрите подробный ответ Мартина на этот вопрос. Ключевым моментом является то, что если одна строка внутри NOT INесть NULL, булева логика работает таким образом, что «правильный ответ должен возвращать нулевые строки». Row Count SpoolОператор оптимизации этого (необходимо) логики.

Почему SQL Server оценивает, что объединение с оператором Soul Count Row удаляет все строки?

Microsoft предоставляет отличную техническую документацию по Оценке мощности в SQL 2014 . В этом документе я нашел следующую информацию:

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

Часто такое изменение является очень хорошим; это значительно облегчает проблему восходящего ключа и, как правило, дает более консервативный план запроса (более высокая оценка строки) для значений, выходящих за пределы диапазона на основе статистической гистограммы.

Однако в этом конкретном случае допущение, что NULLзначение будет найдено, приводит к предположению, что присоединение к Row Count Spoolфильтру отфильтрует все строки из #potentialNewCustomers. В случае, когда на самом деле есть NULLстрока, это правильная оценка (как видно из ответа Мартина). Тем не менее, в случае отсутствия NULLстроки эффект может быть разрушительным, поскольку SQL Server создает оценку после объединения в 1 строку независимо от того, сколько появляется входных строк. Это может привести к очень плохим вариантам соединения в оставшейся части плана запроса.

Это ошибка в SQL 2014? Если так, я подам в Connect. Но сначала я хотел бы получить более глубокое понимание.

Я думаю, что она находится в серой области между ошибкой и влиянием на производительность или ограничением нового Оценщика мощности в SQL Server. Однако эта особенность может привести к существенному снижению производительности по сравнению с SQL 2012 в конкретном случае обнуляемого NOT INпредложения, которое не имеет никаких NULLзначений.

Поэтому я подал проблему Connect, чтобы команда SQL знала о потенциальных последствиях этого изменения для Оценщика мощности.

Обновление: мы сейчас на CTP3 для SQL16, и я подтвердил, что проблема там не возникает.

Джефф Паттерсон
источник
5

Ответ Мартина Смита и ваш самоответ правильно затронули все основные моменты, я просто хочу выделить область для будущих читателей:

Таким образом, этот вопрос больше о понимании этого конкретного запроса и глубоком планировании, а не о том, как сформулировать запрос по-другому.

Заявленная цель запроса:

-- Prune any existing customers from the set of potential new customers

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

Выражая логическое требование полностью:

  • Вернуть потенциальных клиентов, которые еще не являются клиентами
  • Укажите каждого потенциального клиента не более одного раза
  • Исключить нулевых потенциальных и существующих клиентов (что бы ни значил нулевой клиент)

Затем мы можем написать запрос, соответствующий этим требованиям, используя любой синтаксис, который мы предпочитаем. Например:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Это создает эффективный план выполнения, который возвращает правильные результаты:

План выполнения

Мы можем выразить NOT INкак <> ALLили NOT = ANYбез влияния на план или результаты:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

Или используя NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

В этом нет ничего волшебного, ничего особенного в использовании IN, ANYили ALL- нам просто нужно правильно написать запрос, чтобы он всегда давал правильные результаты.

Наиболее компактная форма использует EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

Это также дает правильные результаты, хотя план выполнения может быть менее эффективным из-за отсутствия фильтрации растровых изображений:

План выполнения без растрового изображения

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

Пол Уайт 9
источник