Los recuentos de filas 'reales' inexactos en plan paralelo


17

Esta es una pregunta puramente académica, en tanto que no está causando un problema y solo estoy interesado en escuchar cualquier explicación para el comportamiento.

Tomemos un tema estándar de la tabla de conteo CTE de unión cruzada de Itzik Ben-Gan:

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

Emita una consulta que creará una tabla de número de fila de 1 millón:

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

Eche un vistazo al plan de ejecución paralela para esta consulta:

Plan de ejecución paralela

Tenga en cuenta que el recuento de filas 'real' antes del operador de recopilación de secuencias es 1.004.588. Después del operador de recopilar flujos, el recuento de filas es el esperado de 1,000,000. Más extraño aún, el valor no es consistente y variará de una carrera a otra. El resultado de COUNT siempre es correcto.

Emita la consulta nuevamente, forzando un plan no paralelo:

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

Esta vez, todos los operadores muestran los recuentos de filas 'reales' correctos.

Plan de ejecución no paralela

He intentado esto en 2005SP3 y 2008R2 hasta ahora, los mismos resultados en ambos. ¿Alguna idea de lo que podría causar esto?

Respuestas:


12

Las filas se pasan a través de intercambios internamente desde el subproceso productor al consumidor en paquetes (por lo tanto, CXPACKET - paquete de intercambio de clase), en lugar de una fila a la vez. Hay una cierta cantidad de almacenamiento en búfer dentro del intercambio. Además, la llamada para cerrar la tubería desde el lado del consumidor de Gather Streams se debe pasar en un paquete de control a los subprocesos del productor. La programación y otras consideraciones internas significan que los planes paralelos siempre tienen una cierta 'distancia de frenado'.

Como consecuencia, a menudo verá este tipo de diferencia de recuento de filas donde realmente se requiere menos que todo el conjunto de filas potencial de un subárbol. En este caso, el TOP lleva la ejecución a un "final temprano".

Más información:


10

Creo que puedo tener una explicación parcial para esto, pero no dude en derribarlo o publicar cualquier alternativa. @MartinSmith definitivamente está haciendo algo al resaltar el efecto de TOP en el plan de ejecución.

En pocas palabras, 'Recuento real de filas' no es un recuento de las filas que procesa un operador, es la cantidad de veces que se llama al método GetNext () del operador.

Tomado de BOL :

Los operadores físicos se inicializan, recopilan datos y se cierran. Específicamente, el operador físico puede responder las siguientes tres llamadas a métodos:

  • Init (): El método Init () hace que un operador físico se inicialice y configure las estructuras de datos requeridas. El operador físico puede recibir muchas llamadas de Init (), aunque normalmente un operador físico recibe solo una.
  • GetNext (): el método GetNext () hace que un operador físico obtenga la primera fila de datos o la siguiente. El operador físico puede recibir cero o muchas llamadas GetNext ().
  • Cerrar (): el método Cerrar () hace que un operador físico realice algunas operaciones de limpieza y se apague. Un operador físico solo recibe una llamada Close ().

El método GetNext () devuelve una fila de datos, y el número de veces que se llama aparece como ActualRows en el resultado del Showplan que se produce usando SET STATISTICS PROFILE ON o SET STATISTICS XML ON.

En aras de la exhaustividad, es útil un poco de información sobre los operadores paralelos. El trabajo se distribuye a múltiples flujos en un plan paralelo mediante el flujo de repartición o los operadores de flujo de distribución. Estos distribuyen filas o páginas entre hilos usando uno de cuatro mecanismos:

  • Hash distribuye filas en función de un hash de las columnas de la fila.
  • Round-robin distribuye filas iterando a través de la lista de hilos en un bucle
  • Broadcast distribuye todas las páginas o filas a todos los hilos
  • La partición por demanda se usa solo para escaneos. Los subprocesos giran, solicitan una página de datos del operador, los procesan y solicitan una página adicional cuando terminan.

El primer operador de flujo de distribución (más a la derecha en el plan) utiliza la partición de demanda en las filas que se originan a partir de una exploración constante. Hay tres hilos que llaman a GetNext () 6, 4 y 0 veces para un total de 10 'Filas reales':

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

En el siguiente operador de distribución tenemos tres hilos nuevamente, esta vez con 50, 50 y 0 llamadas a GetNext () para 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>

Es en el siguiente operador paralelo que posiblemente aparece la causa y la explicación.

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

Entonces ahora tenemos 11 llamadas a GetNext (), donde esperábamos ver 10.

Editar: 2011-11-13

Atascado en este punto, busqué respuestas con los capítulos en el índice agrupado y @MikeWalsh amablemente dirigió a @SQLKiwi aquí .


7

1,004,588 es una cifra que también aparece mucho en mis pruebas.

También veo esto para el plan algo más simple a continuación.

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

Otras cifras de interés en el plan de ejecución son

+----------------------------------+--------------+--------------+-----------------+
|                                  | 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 |
+----------------------------------+--------------+--------------+-----------------+

Supongo que solo porque las tareas se están procesando en paralelo, una tarea se encuentra en filas de procesamiento a mitad de vuelo cuando la otra entrega la millonésima fila al operador de recopilación de flujos para que se manejen filas adicionales. Además de este artículo, las filas se almacenan en búfer y se entregan en lotes a este iterador, por lo que parece bastante probable que el número de filas que se procesen excedería en lugar de alcanzar exactamente la TOPespecificación en cualquier caso.

Editar

Solo mirando esto con un poco más de detalle. Noté que estaba obteniendo más variedad que solo el 1,004,588recuento de filas citado anteriormente, así que ejecuté la consulta anterior en un bucle durante 1,000 iteraciones y capturé los planes de ejecución reales. Descartando los 81 resultados para los cuales el grado de paralelismo era cero, se obtuvieron las siguientes cifras.

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

Se puede ver que 1,004,588 fue, con mucho, el resultado más común, pero que en 3 ocasiones ocurrió el peor de los casos y se procesaron 100,000,000 filas. El mejor caso observado fue el recuento de 1,000,496 filas, que ocurrió 19 veces.

El script completo para reproducir se encuentra en la parte inferior de la revisión 2 de esta respuesta (necesitará ajustes si se ejecuta en un sistema con más de 2 procesadores).


1

Creo que el problema proviene del hecho de que múltiples flujos pueden procesar la misma fila dependiendo de cómo se separen las filas entre los flujos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.