並列プランの不正確な「実際の」行カウント


17

これは純粋にアカデミックな質問であり、問​​題を引き起こしていないので、その振る舞いの説明を聞きたいだけです。

Itzik Ben-Ganのクロス結合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

100万行のテーブルを作成するクエリを発行します。

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

行は、一度に1行ずつではなく、プロデューサーからコンシューマースレッドにパケットで内部的に交換を介して渡されます(そのため、CXPACKET-クラス交換パケット)。取引所内には一定量のバッファリングがあります。また、Gather Streamsのコンシューマー側からパイプラインをシャットダウンする呼び出しは、制御パケットでプロデューサースレッドに返される必要があります。スケジューリングおよびその他の内部的な考慮事項により、並行計画には常に特定の「停止距離」があります。

結果として、サブツリーの潜在的な行セット全体が実際に必要とされるよりも少ないこの種の行カウントの違いがよく見られます。この場合、TOPは実行を「早期終了」します。

詳しくは:


10

私はこれについて部分的な説明があるかもしれませんが、それを撃shootするか、代替案を投稿してください。@MartinSmithは、実行計画におけるTOPの効果を強調することで、間違いなく何かに取り組んでいます。

簡単に言えば、「実際の行カウント」は、オペレーターが処理する行のカウントではなく、オペレーターのGetNext()メソッドが呼び出される回数です。

BOLから取得

物理演算子は、初期化、データ収集、および終了します。具体的には、物理​​オペレーターは次の3つのメソッド呼び出しに応答できます。

  • Init():Init()メソッドにより、物理演算子はそれ自体を初期化し、必要なデータ構造をセットアップします。物理オペレーターは多くのInit()呼び出しを受け取ることがありますが、通常、物理オペレーターは1つだけを受け取ります。
  • GetNext():GetNext()メソッドにより、物理演算子はデータの最初の行または後続の行を取得します。物理オペレーターは、ゼロまたは多数のGetNext()呼び出しを受け取る場合があります。
  • Close():Close()メソッドにより、物理オペレーターはいくつかのクリーンアップ操作を実行し、それ自体をシャットダウンします。物理オペレーターは、Close()呼び出しを1つだけ受け取ります。

GetNext()メソッドは1行のデータを返し、呼び出された回数は、SET STATISTICS PROFILE ONまたはSET STATISTICS XML ONを使用して生成されるShowplan出力のActualRowsとして表示されます。

完全を期すために、並列演算子の背景を少し説明しておくと便利です。作業は、再パーティションストリームまたはストリームオペレータの配信によって、並列プランで複数のストリームに配信されます。これらは、4つのメカニズムのいずれかを使用してスレッド間で行またはページを分散します。

  • ハッシュは、行の列のハッシュに基づいて行を分散します
  • ラウンドロビンは、ループ内のスレッドのリストを反復処理して行を分散します
  • ブロードキャストは、すべてのページまたは行をすべてのスレッドに配布します
  • デマンドパーティション分割はスキャンにのみ使用されます。スレッドがスピンアップし、オペレーターにデータのページを要求し、それを処理し、完了したらさらにページを要求します。

最初のディストリビュートストリーム演算子(プランの一番右)は、一定のスキャンから発生した行でデマンドパーティション分割を使用します。GetNext()を6、4、0回呼び出して合計10個の「実際の行」を生成する3つのスレッドがあります。

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

次の分散演算子では、3つのスレッドがあります。今回はGetNext()の呼び出しが50、50、0で合計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>

したがって、GetNext()の呼び出しは11になり、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 |
+----------------------------------+--------------+--------------+-----------------+

私の推測では、タスクは並行して処理されているため、1つのタスクはフライト処理中の行にあり、もう1つのタスクは100万番目の行をストリーム収集オペレーターに配信し、追加の行が処理されているためです。さらに、この記事から行はバッファリングされ、バッチでこのイテレータに配信されるためTOP、いずれにしてもイベントが仕様に正確に達するのではなく、処理される行の数が超過する可能性が高いようです。

編集

これをもう少し詳しく見てみましょう。1,004,588上記で引用した行カウントよりも多様性に気づいたので、1,000回の繰り返しで上記のクエリをループで実行し、実際の実行プランをキャプチャしました。並列度がゼロであった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の最後にあります(3つ以上のプロセッサを搭載したシステムで実行する場合は、調整が必要です)。


1

この問題は、ストリーム間で行がどのように切り分けられるかに応じて、複数のストリームが同じ行を処理できるという事実に起因すると考えています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.