Неточні рядки "Фактичні" враховуються паралельним планом


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 - пакет обміну класом), а не строки за часом. Всередині обміну існує певна кількість буферизації. Крім того, заклик відключити трубопровід із споживчої сторони Gather Streams повинен бути переданий у контрольний пакет назад до потоків виробника. Планування та інші внутрішні міркування означають, що паралельні плани завжди мають певну "зупинку".

Як наслідок, ви часто будете бачити подібну різницю в кількості рядків, де насправді потрібно менше, ніж весь потенційний набір рядків під-дерева. У цьому випадку ТОП приводить виконання до "раннього кінця".

Більше інформації:


10

Я думаю, що я можу отримати часткове пояснення цього, але, будь ласка, не соромтесь знімати його або розміщувати будь-які альтернативи. @MartinSmith, безумовно, досягає чогось, виділяючи ефект TOP у плані виконання.

Простіше кажучи, "Фактична кількість рядків" - це не число рядків, які оператор обробляє, це кількість разів, яку викликає метод GetNext () оператора.

Взято з BOL :

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

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

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

Для повноти корисний невеликий фон на паралельних операторах. Робота розподіляється на кілька потоків у паралельному плані потоком переділу або операторами розподілу потоків. Вони розподіляють рядки або сторінки між потоками, використовуючи один з чотирьох механізмів:

  • Хеш розподіляє рядки на основі хеша стовпців у рядку
  • Круглий робін розподіляє рядки, повторюючи список ниток у циклі
  • Трансляція поширює всі сторінки або рядки по всіх потоках
  • Розбиття попиту використовується лише для сканування. Нитки розкручуються, запитують сторінку даних у оператора, обробляють її та запитують додаткову сторінку, коли буде зроблено.

Оператор першого потоку розподілу (найбільше право в плані) використовує розподіл попиту на рядки, що походить від постійного сканування. Є три потоки, які викликають 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 044 588 є найпоширенішим результатом, але в 3 випадках трапився найгірший можливий випадок і було оброблено 100 000 000 рядків. Найкращий спостережуваний випадок - 1000 496 рядків, що відбувся 19 разів.

Повний сценарій для відтворення знаходиться в нижній частині версії 2 цієї відповіді (його потрібно буде налаштувати, якщо працювати в системі з більш ніж двома процесорами).


1

Я вважаю, що проблема випливає з того, що кілька потоків можуть обробляти один і той же рядок залежно від того, як рядки розташовані між потоками.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.