Nombre de lignes «réel» inexact dans le plan parallèle

17

C'est une question purement académique, en ce sens qu'elle ne pose pas de problème et je suis simplement intéressé à entendre des explications sur le comportement.

Prenez un problème standard Itzik Ben-Gan cross-join table de pointage CTE:

USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE FUNCTION [dbo].[TallyTable] 
(   
    @N INT
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN 
(
    WITH 
    E1(N) AS 
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    )                                       -- 1*10^1 or 10 rows
    , E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
    , E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
    , E8(N) AS (SELECT 1 FROM E4 a, E4 b)   -- 1*10^8 or 100,000,000 rows

    SELECT TOP (@N) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS N FROM E8 
)
GO

Émettez une requête qui créera une table de 1 million de numéros de ligne:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt

Jetez un œil au plan d'exécution parallèle pour cette requête:

Plan d'exécution parallèle

Notez que le nombre de lignes «réel» avant l'opérateur de collecte de flux est de 1 004 588. Après l'opérateur de collecte de flux, le nombre de lignes est le 1 000 000 attendu. Plus étrange encore, la valeur n'est pas cohérente et variera d'une exécution à l'autre. Le résultat du COUNT est toujours correct.

Relancez la requête, forçant le plan non parallèle:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt
OPTION (MAXDOP 1)

Cette fois, tous les opérateurs affichent le nombre de lignes «réel» correct.

Plan d'exécution non parallèle

J'ai essayé jusqu'à présent sur 2005SP3 et 2008R2, mêmes résultats sur les deux. Avez-vous des réflexions sur ce qui pourrait provoquer cela?

Mark Storey-Smith
la source

Réponses:

12

Les lignes sont transmises à travers les échanges en interne du producteur au thread consommateur dans des paquets (d'où CXPACKET - paquet d'échange de classe), plutôt qu'une ligne à la fois. Il y a une certaine quantité de tampon à l'intérieur de l'échange. En outre, l'appel à fermer le pipeline du côté consommateur des flux de collecte doit être renvoyé dans un paquet de contrôle aux threads producteurs. La planification et d'autres considérations internes signifient que les plans parallèles ont toujours une certaine «distance d'arrêt».

En conséquence, vous verrez souvent ce type de différence de nombre de lignes où moins que l'ensemble de lignes potentiel d'un sous-arbre est réellement requis. Dans ce cas, le TOP amène l'exécution à une «fin anticipée».

Plus d'information:

Paul White réintègre Monica
la source
10

Je pense que j'ai peut-être une explication partielle à cela, mais n'hésitez pas à l'abattre ou à publier des alternatives. @MartinSmith est définitivement sur quelque chose en mettant en évidence l'effet de TOP dans le plan d'exécution.

En termes simples, le «nombre réel de lignes» n'est pas un nombre de lignes qu'un opérateur traite, c'est le nombre de fois que la méthode GetNext () de l'opérateur est appelée.

Extrait de BOL :

Les opérateurs physiques initialisent, collectent des données et ferment. Plus précisément, l'opérateur physique peut répondre aux trois appels de méthode suivants:

  • Init (): La méthode Init () oblige un opérateur physique à s'initialiser et à configurer toutes les structures de données requises. L'opérateur physique peut recevoir de nombreux appels Init (), bien que généralement un opérateur physique n'en reçoive qu'un.
  • GetNext (): La méthode GetNext () oblige un opérateur physique à obtenir la première ligne de données, ou la suivante. L'opérateur physique peut recevoir zéro ou plusieurs appels GetNext ().
  • Close (): La méthode Close () oblige un opérateur physique à effectuer certaines opérations de nettoyage et à s'arrêter. Un opérateur physique ne reçoit qu'un seul appel Close ().

La méthode GetNext () renvoie une ligne de données et le nombre d'appels apparaît en tant que ActualRows dans la sortie Showplan produite en utilisant SET STATISTICS PROFILE ON ou SET STATISTICS XML ON.

Par souci d'exhaustivité, un petit historique sur les opérateurs parallèles est utile. Le travail est distribué sur plusieurs flux dans un plan parallèle par le flux de répartition ou distribue les opérateurs de flux. Ceux-ci distribuent des lignes ou des pages entre les threads en utilisant l'un des quatre mécanismes:

  • Hacher distribue les lignes en fonction d'un hachage des colonnes de la ligne
  • Round-robin distribue les lignes en itérant dans la liste des threads dans une boucle
  • La diffusion distribue toutes les pages ou lignes à tous les fils
  • Le partitionnement à la demande est utilisé uniquement pour les analyses. Les threads tournent, demandent une page de données à l'opérateur, les traitent et demandent une autre page une fois terminé.

Le premier opérateur de flux de distribution (le plus à droite dans le plan) utilise le partitionnement de la demande sur les lignes provenant d'une analyse constante. Il y a trois threads qui appellent GetNext () 6, 4 et 0 fois pour un total de 10 «lignes réelles»:

<RunTimeInformation>
       <RunTimeCountersPerThread Thread="2" ActualRows="6" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="1" ActualRows="4" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
 </RunTimeInformation>

Au prochain opérateur de distribution, nous avons à nouveau trois threads, cette fois avec 50, 50 et 0 appels à GetNext () pour un total de 100:

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>

C'est à l'opérateur parallèle suivant que la cause et l'explication peuvent apparaître.

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="1" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="10" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>

Nous avons donc maintenant 11 appels à GetNext (), où nous nous attendions à voir 10.

Modifier: 2011-11-13

Coincé à ce stade, je suis allé chercher des réponses avec les chaps dans l'index clusterisé et @MikeWalsh a gentiment dirigé @SQLKiwi ici .

Mark Storey-Smith
la source
7

1,004,588 est un chiffre qui revient souvent dans mes tests.

Je vois également cela pour le plan un peu plus simple ci-dessous.

WITH 
E1(N) AS 
(
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)                                       -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
SELECT * INTO #E4 FROM E4;

WITH E8(N) AS (SELECT 1 FROM #E4 a, #E4 b),
Nums(N) AS (SELECT  TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) FROM E8 )
SELECT COUNT(N) FROM Nums

DROP TABLE #E4

Plan

D'autres chiffres d'intérêt dans le plan d'exécution sont

+----------------------------------+--------------+--------------+-----------------+
|                                  | Table Scan A | Table Scan B | Row Count Spool |
+----------------------------------+--------------+--------------+-----------------+
| Number Of Executions             | 2            |            2 |             101 |
| Actual Number Of Rows - Total    | 101          |        20000 |         1004588 |
| Actual Number Of Rows - Thread 0 | -            |              |                 |
| Actual Number Of Rows - Thread 1 | 95           |        10000 |          945253 |
| Actual Number Of Rows - Thread 2 | 6            |        10000 |           59335 |
| Actual Rebinds                   | 0            |            0 |               2 |
| Actual Rewinds                   | 0            |            0 |              99 |
+----------------------------------+--------------+--------------+-----------------+

Je suppose que parce que les tâches sont traitées en parallèle, une tâche se trouve dans les lignes de traitement en cours de vol lorsque l'autre livre la millionième ligne à l'opérateur de collecte de flux, de sorte que des lignes supplémentaires sont traitées. De plus, à partir de cet article, les lignes sont mises en mémoire tampon et livrées par lots à cet itérateur, il semble donc très probable que le nombre de lignes en cours de traitement dépasserait plutôt que d'atteindre exactement la TOPspécification dans tous les cas.

Éditer

Je regarde cela un peu plus en détail. J'ai remarqué que j'obtenais plus de variété que le 1,004,588nombre de lignes cité ci-dessus, j'ai donc exécuté la requête ci-dessus en boucle pendant 1000 itérations et capturé les plans d'exécution réels. La suppression des 81 résultats pour lesquels le degré de parallélisme était nul a donné les chiffres suivants.

count       Table Scan A: Total Actual Row Spool - Total Actual Rows
----------- ------------------------------ ------------------------------
352         101                            1004588
323         102                            1004588
72          101                            1003565
37          101                            1002542
35          102                            1003565
29          101                            1001519
18          101                            1000496
13          102                            1002542
5           9964                           99634323
5           102                            1001519
4           9963                           99628185
3           10000                          100000000
3           9965                           99642507
2           9964                           99633300
2           9966                           99658875
2           9965                           99641484
1           9984                           99837989
1           102                            1000496
1           9964                           99637392
1           9968                           99671151
1           9966                           99656829
1           9972                           99714117
1           9963                           99629208
1           9985                           99847196
1           9967                           99665013
1           9965                           99644553
1           9963                           99623626
1           9965                           99647622
1           9966                           99654783
1           9963                           99625116

On peut voir que 1 004 588 était de loin le résultat le plus courant mais qu'à 3 reprises le pire cas possible s'est produit et 100 000 000 de lignes ont été traitées. Le meilleur cas observé était 1 000 496 dénombrements de lignes, survenus 19 fois.

Le script complet à reproduire se trouve au bas de la révision 2 de cette réponse (il devra être modifié s'il est exécuté sur un système avec plus de 2 processeurs).

Martin Smith
la source
1

Je crois que le problème vient du fait que plusieurs flux peuvent traiter la même ligne en fonction de la façon dont les lignes sont découpées entre les flux.

mrdenny
la source