Pouvez-vous expliquer ce plan d'exécution?

20

Je cherchais autre chose quand je suis tombé sur cette chose. Je générais des tables de test contenant des données et exécutais différentes requêtes pour découvrir comment les différentes façons d'écrire des requêtes affectaient le plan d'exécution. Voici le script que j'ai utilisé pour générer des données de test aléatoires:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('t') AND type in (N'U'))
DROP TABLE t
GO

CREATE TABLE t 
(
 c1 int IDENTITY(1,1) NOT NULL 
,c2 int NULL
) 
GO

insert into t
select top 1000000 a from
(select t1.number*2048 + t2.number a, newid() b
from [master]..spt_values t1 
cross join  [master]..spt_values t2
where t1.[type] = 'P' and t2.[type] = 'P') a
order by b
GO

update t set c2 = null
where c2 < 2048 * 2048 / 10
GO


CREATE CLUSTERED INDEX pk ON [t] (c1)
GO

CREATE NONCLUSTERED INDEX i ON t (c2)
GO

Maintenant, étant donné ces données, j'ai invoqué la requête suivante:

select * 
from t 
where 
      c2 < 1048576 
   or c2 is null
;

A ma grande surprise, le plan d'exécution qui a été généré pour cette requête, était ce . (Désolé pour le lien externe, il est trop grand pour tenir ici).

Est-ce que quelqu'un peut m'expliquer ce qui se passe avec tous ces " Scans constants " et " Calculer des scalaires "? Que ce passe-t-il?

Plan

  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1010], [Expr1011], [Expr1012]))
       |--Merge Interval
       |    |--Sort(TOP 2, ORDER BY:([Expr1013] DESC, [Expr1014] ASC, [Expr1010] ASC, [Expr1015] DESC))
       |         |--Compute Scalar(DEFINE:([Expr1013]=((4)&[Expr1012]) = (4) AND NULL = [Expr1010], [Expr1014]=(4)&[Expr1012], [Expr1015]=(16)&[Expr1012]))
       |              |--Concatenation
       |                   |--Compute Scalar(DEFINE:([Expr1005]=NULL, [Expr1006]=NULL, [Expr1004]=(60)))
       |                   |    |--Constant Scan
       |                   |--Compute Scalar(DEFINE:([Expr1008]=NULL, [Expr1009]=(1048576), [Expr1007]=(10)))
       |                        |--Constant Scan
       |--Index Seek(OBJECT:([t].[i]), SEEK:([t].[c2] > [Expr1010] AND [t].[c2] < [Expr1011]) ORDERED FORWARD)
Andrew Savinykh
la source

Réponses:

29

Les analyses constantes produisent chacune une seule ligne en mémoire sans colonnes. Le scalaire de calcul supérieur génère une seule ligne avec 3 colonnes

Expr1005    Expr1006    Expr1004
----------- ----------- -----------
NULL        NULL        60

Le scalaire inférieur calcule une seule ligne avec 3 colonnes

Expr1008    Expr1009    Expr1007
----------- ----------- -----------
NULL        1048576        10

L'opérateur de concaténation Rassemble ces 2 lignes et affiche les 3 colonnes mais elles sont maintenant renommées

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

La Expr1012colonne est un ensemble d'indicateurs utilisés en interne pour définir certaines propriétés de recherche pour le moteur de stockage .

Le prochain calcul scalaire produit 2 lignes

Expr1010    Expr1011    Expr1012    Expr1013    Expr1014    Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL        NULL        60          True        4           16            
NULL        1048576     10          False       0           0      

Les trois dernières colonnes sont définies comme suit et sont juste utilisées à des fins de tri avant d'être présentées à l'opérateur d'intervalle de fusion

[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]), 
[Expr1014] = Scalar Operator((4)&[Expr1012]), 
[Expr1015] = Scalar Operator((16)&[Expr1012])

Expr1014et Expr1015juste tester si certains bits sont activés dans le drapeau. Expr1013semble renvoyer une colonne booléenne true si le bit for 4est allumé et l' Expr1010est NULL.

En essayant d'autres opérateurs de comparaison dans la requête, j'obtiens ces résultats

+----------+----------+----------+-------------+----+----+---+---+---+---+
| Operator | Expr1010 | Expr1011 | Flags (Dec) |       Flags (Bin)       |
|          |          |          |             | 32 | 16 | 8 | 4 | 2 | 1 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
| >        | 1048576  | NULL     |           6 |  0 |  0 | 0 | 1 | 1 | 0 |
| >=       | 1048576  | NULL     |          22 |  0 |  1 | 0 | 1 | 1 | 0 |
| <=       | NULL     | 1048576  |          42 |  1 |  0 | 1 | 0 | 1 | 0 |
| <        | NULL     | 1048576  |          10 |  0 |  0 | 1 | 0 | 1 | 0 |
| =        | 1048576  | 1048576  |          62 |  1 |  1 | 1 | 1 | 1 | 0 |
| IS NULL  | NULL     | NULL     |          60 |  1 |  1 | 1 | 1 | 0 | 0 |
+----------+----------+----------+-------------+----+----+---+---+---+---+

D'où je déduis que le bit 4 signifie "a le début de la plage" (par opposition à non illimité) et le bit 16 signifie que le début de la plage est inclusif.

Ce jeu de résultats à 6 colonnes est émis par l' SORTopérateur trié par Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC. En supposant Trueest représenté par 1et Falsepar 0le jeu de résultats précédemment représenté est déjà dans cet ordre.

Sur la base de mes hypothèses précédentes, l'effet net de ce type est de présenter les plages à l'intervalle de fusion dans l'ordre suivant

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

L'opérateur d'intervalle de fusion génère 2 lignes

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

Pour chaque ligne émise, une recherche de plage est effectuée

Seek Keys[1]: Start:[dbo].[t].c2 > Scalar Operator([Expr1010]), 
               End: [dbo].[t].c2 < Scalar Operator([Expr1011])

Il semblerait donc que deux recherches soient effectuées. Un apparemment > NULL AND < NULLet un > NULL AND < 1048576. Cependant, les drapeaux transmis semblent modifier cela respectivement IS NULLet < 1048576. Espérons que @sqlkiwi puisse clarifier cela et corriger toute inexactitude!

Si vous modifiez légèrement la requête en

select *
from t 
where 
      c2 > 1048576 
   or c2 = 0
;

Ensuite, le plan semble beaucoup plus simple avec une recherche d'index avec plusieurs prédicats de recherche.

Le plan montre Seek Keys

Start: c2 >= 0, End: c2 <= 0, 
Start: c2 > 1048576

L'explication pour laquelle ce plan plus simple ne peut pas être utilisé pour le cas dans l'OP est donnée par SQLKiwi dans les commentaires de l' article de blog lié précédent .

Une recherche d'index avec plusieurs prédicats ne peut pas mélanger différents types de prédicat de comparaison (c'est-à-dire Iset Eqdans le cas de l'OP). Il s'agit simplement d'une limitation actuelle du produit (et c'est probablement la raison pour laquelle le test d'égalité dans la dernière requête c2 = 0est implémenté en utilisant >=et <=non pas simplement la recherche d'égalité simple que vous obtenez pour la requête c2 = 0 OR c2 = 1048576.

Martin Smith
la source
Je ne trouve rien dans l'article de Paul qui explique la différence dans les indicateurs pour [Expr1012]. Pouvez-vous déduire ce que signifie le 60/10 ici?
Mark Storey-Smith
@ MarkStorey-Smith - il dit que 62c'est pour une comparaison d'égalité. Je suppose 60que cela doit signifier qu'au lieu de > AND < comme indiqué dans le plan, vous obtenez en fait à >= AND <=moins que ce soit un IS NULLdrapeau explicite peut-être (?) Ou peut-être que le bit 2indique autre chose sans rapport et qu'il 60soit toujours égal à moi quand je le fais set ansi_nulls offet que je le change il c2 = nullreste toujours à60
Martin Smith
2
@MartinSmith 60 est en effet pour une comparaison avec NULL. Les expressions de limites de plage utilisent NULL pour représenter «sans limite» à chaque extrémité. La recherche est toujours exclusive, c'est-à-dire recherche Début:> Expr et Fin: <Expr plutôt qu'inclusif en utilisant> = et <=. Merci pour le commentaire du blog, je posterai une réponse ou un commentaire plus long en réponse le matin (trop tard pour lui rendre justice maintenant).
Paul White dit GoFundMonica
@SQLKiwi - Merci. Ça a du sens. J'espère que j'aurai compris avant quelques-uns des bits manquants.
Martin Smith
Merci beaucoup, j'absorbe toujours cela, mais cela semble bien expliquer les choses, la principale question qui reste est celle que vous posez à @SQLKiwi sur son blog. Je méditerai quelques jours de plus sur votre réponse pour m'assurer que je n'ai pas de questions de suivi et j'accepterai votre réponse. Merci encore, ce fut d'une grande aide.
Andrew Savinykh
13

Les analyses constantes sont un moyen pour SQL Server de créer un compartiment dans lequel il va placer quelque chose plus tard dans le plan d'exécution. J'en ai posté une explication plus approfondie ici . Pour comprendre à quoi sert le balayage constant, vous devez approfondir le plan. Dans ce cas, ce sont les opérateurs de calcul scalaire qui sont utilisés pour remplir l'espace créé par l'analyse constante.

Les opérateurs de calcul scalaire sont chargés avec NULL et la valeur 1045876, ils seront donc clairement utilisés avec la jointure en boucle dans le but de filtrer les données.

La partie vraiment cool est que ce plan est Trivial. Cela signifie qu'il a subi un processus d'optimisation minimal. Toutes les opérations mènent à l'intervalle de fusion. Ceci est utilisé pour créer un ensemble minimal d'opérateurs de comparaison pour une recherche d'index ( détails à ce sujet ici ).

L'idée est de se débarrasser des valeurs qui se chevauchent pour pouvoir ensuite extraire les données avec un minimum de passes. Bien qu'il utilise toujours une opération de boucle, vous remarquerez que la boucle s'exécute exactement une fois, ce qui signifie qu'il s'agit en fait d'une analyse.

ADDENDA: Cette dernière phrase est éteinte. Il y a eu deux recherches. J'ai mal lu le plan. Les autres concepts sont les mêmes et l'objectif, les passes minimales, est le même.

Grant Fritchey
la source