Неточный «фактический» счетчик строк в параллельном плане

17

Это чисто академический вопрос, поскольку он не вызывает проблем, и мне просто интересно услышать какие-либо объяснения поведения.

Возьмите стандартный выпуск Итцика Бен-Гана для таблицы 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

Выполните запрос, который создаст таблицу номеров строк в 1 миллион:

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

Посмотрите на план параллельного выполнения для этого запроса:

План параллельного выполнения

Обратите внимание, что «фактическое» количество строк перед оператором сбора потоков составляет 1 004 588. После оператора сбора потоков число строк составляет ожидаемый 1 000 000. Еще более странно, что значение не соответствует и будет варьироваться от запуска к запуску. Результат COUNT всегда верен.

Выполните запрос еще раз, форсируя непараллельный план:

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

На этот раз все операторы показывают правильное «фактическое» количество строк.

Непараллельный план выполнения

Я пробовал это на 2005SP3 и 2008R2, одинаковые результаты на обоих. Есть мысли о том, что может вызвать это?

Марк Стори-Смит
источник

Ответы:

12

Строки передаются через обмены внутренне от производителя к потоку потребителя в пакетах (отсюда CXPACKET - пакет обмена классами), а не по строкам за раз. Внутри биржи есть определенное количество буферов. Кроме того, вызов для отключения конвейера со стороны потребителя потоков сбора должен быть передан в управляющем пакете обратно потокам производителя. Планирование и другие внутренние соображения означают, что параллельные планы всегда имеют определенный «тормозной путь».

Как следствие, вы часто будете видеть такого рода разницу в количестве строк, когда фактически требуется меньше, чем весь потенциальный набор строк поддерева. В этом случае TOP приводит исполнение к «раннему концу».

Дополнительная информация:

Пол Уайт восстановил Монику
источник
10

Я думаю, что у меня может быть частичное объяснение этому, но, пожалуйста, не стесняйтесь сбить его или выложить любые альтернативы. @MartinSmith определенно подходит к чему-то, подчеркивая эффект TOP в плане выполнения.

Проще говоря, 'Actual Row Count' - это не количество строк, которые обрабатывает оператор, а количество вызовов метода GetNext () оператора.

Взято с BOL :

Физические операторы инициализируют, собирают данные и закрывают. В частности, физический оператор может ответить на следующие три вызова метода:

  • Init (): метод Init () заставляет физический оператор инициализировать себя и устанавливать любые необходимые структуры данных. Физический оператор может получить много вызовов Init (), хотя обычно физический оператор получает только один.
  • GetNext (): метод GetNext () заставляет физический оператор получать первую или последующую строку данных. Физический оператор может получить ноль или много вызовов GetNext ().
  • Close (): метод Close () заставляет физического оператора выполнять некоторые операции очистки и выключаться. Физический оператор получает только один вызов Close ().

Метод GetNext () возвращает одну строку данных, и число его вызовов отображается как ActualRows в выходных данных Showplan, созданных с помощью SET STATISTICS PROFILE ON или SET STATISTICS XML ON.

Для полноты картины полезен небольшой обзор параллельных операторов. Работа распределяется на несколько потоков в параллельном плане с помощью потока перераспределения или распределения операторов потока. Они распределяют строки или страницы между потоками, используя один из четырех механизмов:

  • Хеш распределяет строки на основе хеша столбцов в строке
  • Round-robin распределяет строки путем перебора списка потоков в цикле
  • Broadcast распределяет все страницы или строки по всем потокам
  • Разделение по требованию используется только для сканирования. Потоки раскручиваются, запрашивают страницу данных у оператора, обрабатывают ее и запрашивают дополнительную страницу, когда это будет сделано.

Первый оператор потока распределения (самый правый в плане) использует разбиение по требованию на строки, полученные в результате постоянного сканирования. Есть три потока, которые вызывают GetNext () 6, 4 и 0 раз, всего 10 «фактических строк»:

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

У следующего оператора распределения у нас снова три потока, на этот раз с 50, 50 и 0 вызовами GetNext () для общего количества 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>

При следующем параллельном операторе возможно возникновение причины и объяснения.

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

Итак, теперь у нас есть 11 вызовов GetNext (), где мы ожидали увидеть 10.

Изменить: 2011-11-13

Застряв в этой точке, я пошел искать ответы с главами в кластерном индексе и @MikeWalsh любезно направил сюда @SQLKiwi .

Марк Стори-Смит
источник
7

1,004,588 это фигура, которая часто всплывает в моих тестах.

Я также вижу это для более простого плана ниже.

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

Строить планы

Другие цифры, представляющие интерес в плане выполнения

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

Я думаю, просто потому, что задачи обрабатываются параллельно, одна задача находится в середине строк обработки полета, когда другая доставляет миллионную строку оператору сбора потоков, поэтому обрабатываются дополнительные строки. Кроме того, из этой статьи строки буферизуются и доставляются пакетами в этот итератор, поэтому вполне вероятно, что число обрабатываемых строк превысит, а не точно достигнет TOPспецификации в любом случае.

редактировать

Просто рассмотрим это немного подробнее. Я заметил, что получаю больше разнообразия, чем просто 1,004,588количество строк, указанное выше, поэтому запустил приведенный выше запрос в цикле на 1000 итераций и зафиксировал фактические планы выполнения. Отбросив 81 результат, для которого степень параллелизма была равна нулю, дали следующие цифры.

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

Можно видеть, что 1 004 588 были, безусловно, наиболее распространенным результатом, но в 3 случаях произошел наихудший случай, и было обработано 100 000 000 строк. В лучшем случае было подсчитано 1 000 496 строк, что происходило 19 раз.

Полный сценарий для воспроизведения находится в нижней части 2-й редакции этого ответа (его потребуется настроить, если он работает в системе с более чем 2 процессорами).

Мартин Смит
источник
1

Я считаю, что проблема заключается в том факте, что несколько потоков могут обрабатывать одну и ту же строку в зависимости от того, как строки распределяются между потоками.

mrdenny
источник