แถว 'จริง' ไม่ถูกต้องนับเป็นแผนขนาน


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

ออกแบบสอบถามที่จะสร้างตารางหมายเลข 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 จะต้องถูกส่งผ่านในแพ็กเก็ตควบคุมกลับไปยังเธรดผู้ผลิต การพิจารณากำหนดเวลาและการพิจารณาภายในอื่น ๆ หมายความว่าแผนขนานจะมี 'ระยะการหยุด' ที่แน่นอนเสมอ

เป็นผลให้คุณมักจะเห็นความแตกต่างของการนับแถวนี้ซึ่งน้อยกว่าความจำเป็นของ rowset ทั้งหมดของทรีย่อย ในกรณีนี้ TOP นำการประหารชีวิตไปสู่

ข้อมูลมากกว่านี้:


10

ฉันคิดว่าฉันอาจมีคำอธิบายบางส่วนสำหรับเรื่องนี้ แต่โปรดอย่าลังเลที่จะยิงมันหรือโพสต์ทางเลือกใด ๆ @MartinSmith มีบางสิ่งบางอย่างที่แน่นอนโดยเน้นผลของ TOP ในแผนการดำเนินการ

กล่าวง่ายๆว่า 'จำนวนแถวจริง' ไม่ใช่จำนวนแถวที่ผู้ดำเนินการดำเนินการ แต่เป็นจำนวนครั้งที่เรียกใช้เมธอด GetNext () ของผู้ประกอบการ

นำมาจากBOL :

ผู้ประกอบการทางกายภาพเริ่มต้นรวบรวมข้อมูลและปิด โดยเฉพาะผู้ประกอบการทางกายภาพสามารถรับสายสามวิธีต่อไปนี้:

  • Init (): เมธอด Init () ทำให้ตัวดำเนินการทางกายภาพเริ่มต้นเองและตั้งค่าโครงสร้างข้อมูลที่ต้องการ ตัวดำเนินการทางกายภาพอาจรับการเรียก Init () จำนวนมากถึงแม้ว่าโดยทั่วไปแล้วตัวดำเนินการทางกายภาพจะได้รับเพียงครั้งเดียว
  • GetNext (): เมธอด GetNext () ทำให้ตัวดำเนินการทางกายภาพได้รับแถวแรกหรือแถวที่ตามมาของข้อมูล ตัวดำเนินการทางกายภาพอาจได้รับการโทรเป็นศูนย์หรือหลายสาย GetNext ()
  • ปิด (): วิธีการปิด () ทำให้ผู้ปฏิบัติงานทางกายภาพดำเนินการล้างข้อมูลและปิดตัวลง ตัวดำเนินการทางกายภาพได้รับการเรียกปิด () เพียงครั้งเดียวเท่านั้น

เมธอด GetNext () ส่งคืนข้อมูลหนึ่งแถวและจำนวนครั้งที่เรียกว่าปรากฏเป็น ActualRows ในเอาต์พุต Showplan ที่สร้างขึ้นโดยใช้ SET STATISTICS PROFILE หรือเปิด XML สถิติของตลาดหลักทรัพย์

เพื่อประโยชน์ของความสมบูรณ์แบ็คกราวน์เล็ก ๆ ของตัวดำเนินการคู่ขนานจึงมีประโยชน์ งานถูกแจกจ่ายไปยังสตรีมจำนวนมากในแผนคู่ขนานโดยสตรีม repartition หรือกระจายตัวดำเนินการสตรีม เหล่านี้กระจายแถวหรือหน้าระหว่างหัวข้อโดยใช้หนึ่งในสี่กลไก:

  • แฮกระจายแถวตามแฮชของคอลัมน์ในแถว
  • Round-robinกระจายแถวด้วยการวนซ้ำผ่านรายการของเธรดในลูป
  • ออกอากาศกระจายหน้าหรือทุกแถวไปยังกระทู้ทั้งหมด
  • การแบ่งความต้องการใช้สำหรับการสแกนเท่านั้น เธรดหมุนขึ้นร้องขอหน้าข้อมูลจากโอเปอเรเตอร์ประมวลผลและร้องขอหน้าต่อไปเมื่อเสร็จสิ้น

ผู้ให้บริการกระจายกระแสรายแรก (ขวาสุดในแผน) ใช้การแบ่งความต้องการบนแถวที่เกิดจากการสแกนค่าคงที่ มีสามเธรดที่เรียกใช้ 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

ติดอยู่ที่จุดนี้ผมไปเร่ขายหาคำตอบกับ Chaps ในดัชนีคลัสเตอร์และ @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จำนวนแถวที่ยกมาด้านบนดังนั้นจึงเรียกใช้คิวรีข้างบนเป็นวนซ้ำ 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 เป็นผลลัพธ์ที่พบบ่อยที่สุด แต่ในบางครั้งมีกรณีที่เลวร้ายที่สุดเกิดขึ้นและมีการประมวลผล 100,000,000 แถว กรณีที่ดีที่สุดที่สังเกตได้คือ 1,000,496 จำนวนแถวซึ่งเกิดขึ้น 19 ครั้ง

สคริปต์เต็มรูปแบบในการทำซ้ำอยู่ที่ด้านล่างของการแก้ไข 2 ของคำตอบนี้ (จะต้อง tweaking หากทำงานบนระบบที่มีโปรเซสเซอร์มากกว่า 2 ตัว)


1

ฉันเชื่อว่าปัญหามาจากข้อเท็จจริงที่ว่าสตรีมจำนวนมากสามารถประมวลผลแถวเดียวกันได้โดยขึ้นอยู่กับการเรียงแถวระหว่างสตรีม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.