Requête 100 fois plus lente dans SQL Server 2014, Row Count Spool row estimer le coupable?

13

J'ai une requête qui s'exécute en 800 millisecondes dans SQL Server 2012 et prend environ 170 secondes dans SQL Server 2014 . Je pense que j'ai réduit cela à une mauvaise estimation de cardinalité pour l' Row Count Spoolopérateur. J'ai lu un peu sur les opérateurs de spoule (par exemple, ici et ici ), mais j'ai toujours du mal à comprendre certaines choses:

  • Pourquoi cette requête a-t-elle besoin d'un Row Count Spoolopérateur? Je ne pense pas que ce soit nécessaire pour l'exactitude, alors quelle optimisation spécifique essaie-t-elle de fournir?
  • Pourquoi SQL Server estime-t-il que la jointure à l' Row Count Spoolopérateur supprime toutes les lignes?
  • Est-ce un bogue dans SQL Server 2014? Si oui, je déposerai dans Connect. Mais j'aimerais d'abord une compréhension plus profonde.

Remarque: je peux réécrire la requête en tant que LEFT JOINou ajouter des index aux tables afin d'obtenir des performances acceptables à la fois dans SQL Server 2012 et SQL Server 2014. Cette question concerne donc plus la compréhension de cette requête spécifique et la planification en profondeur et moins comment formuler la requête différemment.


La requête lente

Voir ce Pastebin pour un script de test complet. Voici la requête de test spécifique que je regarde:

-- 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: le plan de requête estimé

SQL Server est d' avis que le Left Anti Semi Joinau Row Count Spoolfiltrera les 10.000 lignes jusqu'à 1 rang. Pour cette raison, il sélectionne a LOOP JOINpour la jointure suivante #existingCustomers.

entrez la description de l'image ici


SQL Server 2014: le plan de requête réel

Comme prévu (par tout le monde sauf SQL Server!), Le Row Count Spooln'a supprimé aucune ligne. Nous bouclons donc 10 000 fois lorsque SQL Server ne devrait boucler qu'une seule fois.

entrez la description de l'image ici


SQL Server 2012: le plan de requête estimé

Lorsque vous utilisez SQL Server 2012 (ou OPTION (QUERYTRACEON 9481)dans SQL Server 2014), le Row Count Spoolne réduit pas le nombre estimé de lignes et une jointure de hachage est choisie, ce qui donne un bien meilleur plan.

entrez la description de l'image ici

La réécriture LEFT JOIN

Pour référence, voici un moyen de réécrire la requête afin d'obtenir de bonnes performances dans tous les SQL Server 2012, 2014 et 2016. Cependant, je suis toujours intéressé par le comportement spécifique de la requête ci-dessus et si elle est un bogue dans le nouvel estimateur de cardinalité 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

entrez la description de l'image ici

Geoff Patterson
la source

Réponses:

10

Pourquoi cette requête nécessite-t-elle un opérateur de nombre de lignes? ... quelle optimisation spécifique tente-t-elle de fournir?

La cust_nbrcolonne dans #existingCustomersest nullable. S'il contient réellement des valeurs nulles, la réponse correcte ici est de retourner zéro ligne ( NOT IN (NULL,...) donnera toujours un jeu de résultats vide.).

Ainsi, la requête peut être considérée comme

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) 

Avec la bobine de nombre de lignes là-bas pour éviter d'avoir à évaluer la

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

Plus d'une fois.

Cela semble juste être un cas où une petite différence d'hypothèses peut faire une différence assez catastrophique dans les performances.

Après avoir mis à jour une seule ligne comme ci-dessous ...

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

... la requête s'est terminée en moins d'une seconde. Le nombre de lignes dans les versions réelles et estimées du plan est maintenant presque exact.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

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

entrez la description de l'image ici

Zéro ligne est sortie comme décrit ci-dessus.

Les histogrammes de statistiques et les seuils de mise à jour automatique dans SQL Server ne sont pas suffisamment granulaires pour détecter ce type de changement de ligne unique. On peut soutenir que si la colonne est nullable, il pourrait être raisonnable de travailler sur la base qu'elle en contient au moins un, NULLmême si l'histogramme des statistiques n'indique pas actuellement qu'il y en a.

Martin Smith
la source
9

Pourquoi cette requête nécessite-t-elle un opérateur de nombre de lignes? Je ne pense pas que ce soit nécessaire pour l'exactitude, alors quelle optimisation spécifique essaie-t-elle de fournir?

Voir la réponse complète de Martin pour cette question. Le point clé est que si une seule ligne dans le NOT INest NULL, la logique booléenne fonctionne de telle sorte que "la bonne réponse est de retourner zéro lignes". L' Row Count Spoolopérateur optimise cette logique (nécessaire).

Pourquoi SQL Server estime-t-il que la jointure à l'opérateur de spouleur de nombre de lignes supprime toutes les lignes?

Microsoft fournit un excellent livre blanc sur l'estimateur de cardinalité SQL 2014 . Dans ce document, j'ai trouvé les informations suivantes:

Le nouveau CE suppose que les valeurs interrogées existent dans l'ensemble de données même si la valeur se situe en dehors de la plage de l'histogramme. Le nouvel EC dans cet exemple utilise une fréquence moyenne qui est calculée en multipliant la cardinalité de la table par la densité.

Souvent, un tel changement est très bon; il atténue considérablement le problème clé croissant et donne généralement un plan de requête plus conservateur (estimation de ligne plus élevée) pour les valeurs qui sont hors limites sur la base de l'histogramme des statistiques.

Cependant, dans ce cas spécifique, supposer qu'une NULLvaleur sera trouvée conduit à l'hypothèse que la jointure à Row Count Spoolfiltrera toutes les lignes de #potentialNewCustomers. Dans le cas où il y a en fait une NULLrangée, c'est une estimation correcte (comme on le voit dans la réponse de Martin). Toutefois, dans le cas où il se trouve qu'il n'y a pas de NULLligne, l'effet peut être dévastateur car SQL Server produit une estimation post-jointure de 1 ligne, quel que soit le nombre de lignes d'entrée qui apparaissent. Cela peut conduire à de très mauvais choix de jointure dans le reste du plan de requête.

Est-ce un bogue dans SQL 2014? Si oui, je déposerai dans Connect. Mais j'aimerais d'abord une compréhension plus profonde.

Je pense que c'est dans la zone grise entre un bogue et une hypothèse ou une limitation affectant les performances du nouvel estimateur de cardinalité de SQL Server. Cependant, cette bizarrerie peut entraîner des régressions substantielles des performances par rapport à SQL 2012 dans le cas spécifique d'une NOT INclause nullable qui ne possède aucune NULLvaleur.

Par conséquent, j'ai déposé un problème de connexion afin que l'équipe SQL soit consciente des implications potentielles de cette modification pour l'estimateur de cardinalité.

Mise à jour: nous sommes maintenant sur CTP3 pour SQL16 et j'ai confirmé que le problème ne se produit pas là-bas.

Geoff Patterson
la source
5

La réponse de Martin Smith et votre auto-réponse ont abordé correctement tous les points principaux, je veux juste souligner un domaine pour les futurs lecteurs:

Cette question concerne donc davantage la compréhension approfondie de cette requête et de ce plan spécifiques et moins la manière de formuler la requête différemment.

Le but déclaré de la requête est:

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

Cette exigence est facile à exprimer en SQL, de plusieurs manières. Laquelle est choisie est autant une question de style que toute autre chose, mais la spécification de la requête doit toujours être écrite pour retourner des résultats corrects dans tous les cas. Cela inclut la prise en compte des valeurs nulles.

Exprimer pleinement l'exigence logique:

  • Renvoyer des clients potentiels qui ne sont pas déjà clients
  • Répertoriez chaque client potentiel au plus une fois
  • Exclure les clients potentiels et existants nuls (quoi que signifie un client nul)

Nous pouvons ensuite écrire une requête correspondant à ces exigences en utilisant la syntaxe que nous préférons. Par exemple:

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
    );

Cela produit un plan d'exécution efficace, qui renvoie des résultats corrects:

Plan d'exécution

Nous pouvons exprimer le NOT INcomme <> ALLou NOT = ANYsans affecter le plan ou les résultats:

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
    );

Ou en utilisant 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
    );

Il n'y a rien de magique à cela, ni rien de particulièrement répréhensible à propos de l'utilisation de IN, ANYou ALL- nous avons juste besoin d'écrire la requête correctement, donc cela produira toujours les bons résultats.

La forme la plus compacte utilise 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;

Cela produit également des résultats corrects, bien que le plan d'exécution puisse être moins efficace en raison de l'absence de filtrage bitmap:

Plan d'exécution non bitmap

La question d'origine est intéressante car elle expose un problème affectant les performances avec l'implémentation de vérification nulle nécessaire. Le point de cette réponse est que l'écriture correcte de la requête évite également le problème.

Paul White 9
la source