Запит на 100 разів повільніше в SQL Server 2014, рядок Spool Count Spool оцінюють винуватця?


13

У мене є запит, який працює в SQL Server 2012 за 800 мілісекунд і займає близько 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. Отже, це питання стосується більш глибокого розуміння цього конкретного запиту та плану, а менше про як по-різному фразувати запит.


Повільний запит

Повний скрипт тесту див. На пастебіні . Ось конкретний тестовий запит, на який я дивлюсь:

-- 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не зменшується приблизна кількість рядків і вибирається хеш-з'єднання, що призводить до набагато кращого плану.

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

ЛИВО ПРИЄДНАЙТЕСЬ перепишіть

Для довідки, ось такий спосіб я можу переписати запит, щоб досягти хорошої продуктивності у всіх 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

Чому для цього запиту потрібен оператор Spool 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) 

З котушкою рядків там, щоб уникнути необхідності оцінювати значення

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

Чому для цього запиту потрібен оператор Spool Count Spool? Я не думаю, що це потрібно для коректності, тож яку конкретну оптимізацію намагається надати?

Дивіться ґрунтовну відповідь Мартіна на це питання. Ключовим моментом є те, що якщо в одному рядку NOT INє NULL, булева логіка працює таким чином, що "правильна відповідь полягає у поверненні нульових рядків". Row Count SpoolОператор оптимізації цього (необхідно) логіки.

Чому SQL Server оцінює, що приєднання до оператора Spool Count Spool видаляє всі рядки?

Корпорація Майкрософт надає чудовий аркуш паперу на оцінці кардинальності SQL 2014 . У цьому документі я знайшов таку інформацію:

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

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

Однак у цьому конкретному випадку припущення, що NULLзнайдеться значення, призводить до припущення, що приєднання до Row Count Spoolволі відфільтрує всі рядки з #potentialNewCustomers. У випадку, коли насправді є NULLряд, це правильна оцінка (як видно з відповіді Мартіна). Однак у випадку, коли не відбувається NULLрядка, ефект може бути руйнівним, оскільки SQL Server виробляє оцінку після рядкового з'єднання в 1 рядок, незалежно від кількості вхідних рядків. Це може призвести до дуже поганого вибору приєднання до залишку плану запитів.

Це помилка в SQL 2014? Якщо так, я подаю файл у Connect. Але я хотів би спочатку глибшого розуміння.

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

Це також дає правильні результати, хоча план виконання може бути менш ефективним через відсутність растрової фільтрації:

План виконання небітових карт

Оригінальне запитання цікаве тим, що воно розкриває проблеми, що впливають на ефективність, з необхідною реалізацією нульової перевірки. Суть цієї відповіді полягає в тому, що написання запиту правильно уникає і проблеми.

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