อะไรคือวิธีที่แตกต่างในการแทนที่ ISNULL () ในส่วนคำสั่ง WHERE ที่ใช้เฉพาะค่าตัวอักษร?


55

สิ่งนี้ไม่เกี่ยวกับ:

นี่ไม่ใช่คำถามเกี่ยวกับการสืบค้นทั้งหมดที่ยอมรับอินพุตของผู้ใช้หรือใช้ตัวแปร

นี่เป็นเรื่องเกี่ยวกับเคียวรีที่ISNULL()ใช้ในส่วนWHEREคำสั่งเพื่อแทนที่NULLค่าด้วยค่า canary เพื่อเปรียบเทียบกับเพรดิเคตและวิธีต่างๆในการเขียนเคียวรีเหล่านั้นใหม่เพื่อให้SARGableใน SQL Server

ทำไมคุณไม่มีที่นั่งตรงนั้นล่ะ?

แบบสอบถามตัวอย่างของเราขัดต่อสำเนาโลคัลของฐานข้อมูล Stack Overflow บน SQL Server 2016 และค้นหาผู้ใช้ที่NULLมีอายุหรืออายุ <18

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

แผนคิวรีแสดงการสแกนของดัชนีที่ไม่ได้จัดกลุ่มอย่างรอบคอบ

ถั่ว

ตัวดำเนินการสแกนแสดง (ขอบคุณส่วนเพิ่มเติมของ XML แผนการดำเนินการจริงใน SQL Server รุ่นล่าสุด) ที่เราอ่านทุกแถวของ stinkin

ถั่ว

โดยรวมแล้วเราอ่าน 9157 ครั้งและใช้เวลา CPU ประมาณครึ่งวินาที:

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 485 ms,  elapsed time = 483 ms.

คำถาม: มีวิธีใดบ้างที่จะเขียนข้อความค้นหานี้ใหม่เพื่อให้มีประสิทธิภาพมากขึ้นและอาจถึงกับ SARGable?

อย่าลังเลที่จะเสนอคำแนะนำอื่น ๆ ฉันไม่คิดว่าคำตอบของฉันจำเป็นต้องเป็นคำตอบและมีคนฉลาดพอที่จะหาทางเลือกที่อาจจะดีกว่า

หากคุณต้องการที่จะเล่นพร้อมในคอมพิวเตอร์ของคุณตรงไปที่นี่เพื่อดาวน์โหลดฐานข้อมูลดังนั้น

ขอบคุณ!

คำตอบ:


57

ส่วนคำตอบ

มีหลายวิธีในการเขียนสิ่งนี้โดยใช้โครงสร้าง T-SQL ที่แตกต่างกัน เราจะดูข้อดีข้อเสียและทำการเปรียบเทียบโดยรวมด้านล่าง

ก่อนขึ้น : การใช้OR

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

การใช้ORทำให้เรามีแผนค้นหาที่มีประสิทธิภาพมากขึ้นซึ่งจะอ่านจำนวนแถวที่แน่นอนที่เราต้องการอย่างไรก็ตามมันจะเพิ่มสิ่งที่โลกทางเทคนิคเรียกa whole mess of malarkeyไปยังแผนแบบสอบถาม

ถั่ว

โปรดทราบด้วยว่าการค้นหาดำเนินการสองครั้งที่นี่ซึ่งควรเห็นได้ชัดเจนกว่าจากผู้ให้บริการกราฟิก:

ถั่ว

Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 469 ms,  elapsed time = 473 ms.

ลำดับที่สอง : การใช้ตารางที่ได้รับด้วยUNION ALL แบบสอบถามของเราสามารถเขียนใหม่ได้เช่นนี้

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

แผนนี้ให้ผลในรูปแบบเดียวกันโดยมีโรคมาลาเรียน้อยกว่าและมีระดับความซื่อสัตย์ที่ชัดเจนมากขึ้นเกี่ยวกับดัชนีที่ถูกแสวงหา (ค้นหา?) กี่ครั้ง

ถั่ว

มันจะทำการอ่าน (8233) จำนวนเท่ากันกับORเคียวรี แต่ประหยัดเวลาของ CPU ประมาณ 100 มิลลิวินาที

CPU time = 313 ms,  elapsed time = 315 ms.

แต่คุณจะต้องมีจริงๆระวังนี่เพราะถ้าแผนนี้พยายามที่จะไปขนานสองแยกCOUNTการดำเนินงานจะได้รับการต่อเนื่องเพราะพวกเขากำลังแต่ละถือว่าเป็นสเกลารวมทั่วโลก หากเราบังคับใช้แผนขนานโดยใช้การติดตามสถานะ 8649 ปัญหาจะชัดเจน

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);

ถั่ว

สิ่งนี้สามารถหลีกเลี่ยงได้โดยเปลี่ยนการค้นหาของเราเล็กน้อย

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

ตอนนี้ทั้งสองโหนดที่ทำการค้นหา Seek จะถูกทำให้เป็นเส้นขนานอย่างสมบูรณ์จนกระทั่งเรากดผู้ดำเนินการเชื่อมต่อ

ถั่ว

สำหรับสิ่งที่มันคุ้มค่ารุ่นขนานอย่างเต็มที่มีประโยชน์ที่ดีบางอย่าง ด้วยค่าใช้จ่ายในการอ่านอีกประมาณ 100 ครั้งและเวลา CPU เพิ่มเติมประมาณ 90ms เวลาที่ผ่านไปจะลดลงเป็น 93 มิลลิวินาที

Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 500 ms,  elapsed time = 93 ms.

CROSS ใช้กับอะไรได้บ้าง ไม่มีคำตอบที่สมบูรณ์หากไม่ได้ใช้เวทย์มนตร์ของCROSS APPLY!

COUNTแต่น่าเสียดายที่เราทำงานเป็นปัญหามากขึ้นด้วย

SELECT SUM(Records)
FROM dbo.Users AS u 
CROSS APPLY 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u2 
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u2 
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

แผนนี้แย่มาก นี่คือแผนที่คุณจะได้เมื่อคุณปรากฏตัวครั้งสุดท้ายในวันเซนต์แพทริก แม้ว่าจะขนานกันอย่างดีด้วยเหตุผลบางอย่างมันสแกน PK / CX Ew แผนมีต้นทุน 2198 เหรียญตัวค้นหา

ถั่ว

Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 29532 ms,  elapsed time = 5828 ms.

ซึ่งเป็นตัวเลือกที่แปลกเพราะถ้าเราบังคับให้ใช้ดัชนีที่ไม่เป็นคลัสเตอร์ค่าใช้จ่ายจะลดลงค่อนข้างมากถึง 1,798 เหรียญค้นหา

SELECT SUM(Records)
FROM dbo.Users AS u 
CROSS APPLY 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

เฮ้ค้นหา! ตรวจสอบคุณที่นั่น นอกจากนี้โปรดทราบว่าด้วยความมหัศจรรย์ของCROSS APPLYเราไม่จำเป็นต้องทำสิ่งที่โง่เขลาเพื่อให้ได้แผนคู่ขนานที่ครบถ้วน

ถั่ว

Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 27625 ms,  elapsed time = 4909 ms.

การใช้ครอสจะจบลงได้ดีขึ้นโดยที่ไม่มีของCOUNTอยู่ในนั้น

SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY 
(
    SELECT 1
    FROM dbo.Users AS u2
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u2
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

แผนดูดี แต่การอ่านและ CPU ไม่ได้ปรับปรุง

ถั่ว

Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 4844 ms,  elapsed time = 863 ms.

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

SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN 
(
    SELECT u.Id
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT u.Id
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x ON x.Id = u.Id;

พีชคณิตเชิงสัมพันธ์ : เพื่อให้ละเอียดและเพื่อป้องกันไม่ให้โจ Celko หลอกหลอนความฝันของเราอย่างน้อยเราต้องลองสิ่งที่เกี่ยวข้องกับความแปลก นี่มันไม่มีอะไรเลย!

ความพยายามด้วย INTERSECT

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
                   INTERSECT
                   SELECT u.Age WHERE u.Age IS NOT NULL );

ถั่ว

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 1094 ms,  elapsed time = 1090 ms.

และนี่คือความพยายามด้วย EXCEPT

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
                   EXCEPT
                   SELECT u.Age WHERE u.Age IS NULL);

ถั่ว

Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 2126 ms,  elapsed time = 376 ms.

อาจมีวิธีอื่นในการเขียนสิ่งเหล่านี้ แต่ฉันจะทิ้งเรื่องนั้นไว้กับคนที่อาจจะใช้EXCEPTและINTERSECTบ่อยกว่าที่ฉันทำ

หากคุณต้องการจำนวนที่ ฉันใช้COUNTในการสอบถามของฉันเป็นแบบสั้น ๆ (อ่าน: ฉันขี้เกียจเกินไปที่จะเกิดสถานการณ์ที่เกี่ยวข้องมากขึ้นในบางครั้ง) หากคุณต้องการการนับคุณสามารถใช้CASEนิพจน์เพื่อทำสิ่งเดียวกัน

SELECT SUM(CASE WHEN u.Age < 18 THEN 1
                WHEN u.Age IS NULL THEN 1
                ELSE 0 END) 
FROM dbo.Users AS u

SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
                ELSE 0 END) 
FROM dbo.Users AS u

สิ่งเหล่านี้ทั้งสองได้รับแผนเดียวกันและมี CPU เหมือนกันและคุณสมบัติการอ่าน

ถั่ว

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 719 ms,  elapsed time = 719 ms.

ผู้ชนะ? ในการทดสอบของฉันแผนขนานที่บังคับใช้กับ SUM บนตารางที่ได้รับนั้นทำได้ดีที่สุด และใช่แบบสอบถามเหล่านี้จำนวนมากสามารถได้รับความช่วยเหลือโดยการเพิ่มดัชนีที่กรองแล้วสองสามรายการลงในบัญชีของภาคแสดงทั้งสอง แต่ฉันต้องการที่จะปล่อยให้การทดลองอื่น ๆ

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

ขอบคุณ!


1
NOT EXISTS ( INTERSECT / EXCEPT )คำสั่งสามารถทำงานได้โดยไม่ต้องINTERSECT / EXCEPTส่วนWHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );อีกวิธีหนึ่ง - ที่ใช้EXCEPT: SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ; (ที่ UserID เป็น PK หรือที่ไม่ซ้ำกันในคอลัมน์ไม่เป็นโมฆะ (s))
ypercubeᵀᴹ

ผ่านการทดสอบแล้วหรือยัง SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;ขออภัยถ้าฉันพลาดรุ่นล้านรุ่นที่คุณได้ทดสอบ!
ypercubeᵀᴹ

@ ypercubeᵀᴹ นี่คือแผนสำหรับสิ่งนั้น มันแตกต่างกันเล็กน้อย แต่มีลักษณะคล้ายกับUNION ALLแผน (CPU 360ms, 11k อ่าน)
Erik Darling

เฮ้เอริคเพิ่งท่องไปในโลกของ sql และโผล่เข้ามาเพื่อพูดว่า "คอลัมน์ที่คำนวณ" เพียงเพื่อรบกวนคุณ <3
เบ้าหลอม

17

ผมก็ไม่ได้เป็นเกมที่จะเรียกคืนฐานข้อมูล 110 GB ในราคาเพียงตารางเดียวเพื่อให้ฉันสร้างข้อมูลของตัวเอง การแจกแจงอายุควรตรงกับสิ่งที่อยู่ใน Stack Overflow แต่แน่นอนว่าตารางจะไม่ตรงกัน ฉันไม่คิดว่ามันจะเป็นปัญหามากนักเพราะข้อความค้นหาจะเข้าสู่ดัชนี ฉันกำลังทดสอบคอมพิวเตอร์ซีพียู 4 ตัวด้วย SQL Server 2016 SP1 สิ่งหนึ่งที่ควรทราบคือสำหรับแบบสอบถามที่เสร็จสิ้นอย่างรวดเร็วสิ่งสำคัญคือไม่ต้องรวมแผนดำเนินการจริง ที่สามารถชะลอสิ่งต่าง ๆ ลงได้เล็กน้อย

ฉันเริ่มต้นด้วยการแก้ปัญหาด้วยคำตอบที่ยอดเยี่ยมของ Erik สำหรับอันนี้:

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

ฉันได้ผลลัพธ์ต่อไปนี้จากsys.dm_exec_sessionsมากกว่า 10 การทดลอง (โดยปกติแบบสอบถามจะขนานกันสำหรับฉัน):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

ข้อความค้นหาที่ทำงานได้ดีขึ้นสำหรับ Erik นั้นทำงานได้แย่ลงจริง ๆ ในเครื่องของฉัน:

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

ผลลัพธ์จากการทดลอง 10 ครั้ง:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

ฉันไม่สามารถอธิบายได้ทันทีว่าทำไมมันถึงแย่ขนาดนั้น แต่ก็ไม่ชัดเจนว่าทำไมเราจึงต้องการบังคับให้ผู้ดำเนินการเกือบทุกรายในแผนแบบสอบถามให้ทำงานแบบขนาน AGE < 18ในแผนเดิมที่เรามีโซนอนุกรมที่พบแถวทั้งหมดที่มี มีเพียงไม่กี่พันแถว บนเครื่องของฉันฉันได้รับการอ่านแบบลอจิคัล 9 ครั้งสำหรับส่วนของแบบสอบถามนั้นและ 9 ms ของเวลา CPU ที่รายงานและเวลาที่ผ่านไป นอกจากนี้ยังมีโซนอนุกรมสำหรับการรวมส่วนกลางสำหรับแถวด้วยAGE IS NULLแต่จะประมวลผลเพียงหนึ่งแถวต่อ DOP บนเครื่องของฉันนี่แค่สี่แถว

Takeaway ของฉันคือว่ามันเป็นสิ่งที่สำคัญที่สุดในการเพิ่มประสิทธิภาพการมีส่วนร่วมของแบบสอบถามที่พบแถวที่มีที่NULLสำหรับAgeเพราะมีล้านแถวเหล่านั้น ฉันไม่สามารถสร้างดัชนีที่มีหน้าเว็บน้อยลงซึ่งครอบคลุมข้อมูลมากกว่าหน้าเว็บที่ถูกบีบอัดอย่างง่ายในคอลัมน์ ฉันคิดว่ามีขนาดดัชนีขั้นต่ำต่อแถวหรือว่าพื้นที่ดัชนีจำนวนมากไม่สามารถหลีกเลี่ยงได้ด้วยเทคนิคที่ฉันพยายาม ดังนั้นถ้าเราติดลอจิกจำนวนเท่าเดิมเพื่อรับข้อมูลดังนั้นวิธีเดียวที่จะทำให้เร็วขึ้นคือการทำให้คิวรีขนานกันมากขึ้น แต่สิ่งนี้ต้องทำในวิธีที่แตกต่างจากเคียวรีของ Erik ที่ใช้ TF 8649 ในแบบสอบถามข้างต้นเรามีอัตราส่วน 3.62 สำหรับเวลา CPU ต่อเวลาที่ผ่านไปซึ่งค่อนข้างดี อุดมคติจะเป็นอัตราส่วน 4.0 ในเครื่องของฉัน

สิ่งหนึ่งที่เป็นไปได้ในการปรับปรุงคือการแบ่งงานให้เท่ากันระหว่างเธรด ในภาพด้านล่างเราจะเห็นได้ว่าหนึ่งในซีพียูของฉันตัดสินใจที่จะหยุดพักเล็กน้อย:

ด้ายขี้เกียจ

การสแกนดัชนีเป็นหนึ่งในตัวดำเนินการไม่กี่ตัวที่สามารถนำมาใช้ในแบบคู่ขนานและเราไม่สามารถทำอะไรเกี่ยวกับวิธีกระจายแถวไปยังเธรด มีองค์ประกอบของโอกาสเช่นกัน แต่ค่อนข้างสม่ำเสมอฉันเห็นหัวข้อที่ไม่ทำงาน วิธีหนึ่งในการแก้ไขปัญหานี้คือการทำขนานวิธีที่ยาก: ในส่วนด้านในของการเข้าร่วมลูปซ้อนกัน สิ่งใดในส่วนด้านในของลูปที่ซ้อนกันจะถูกนำไปใช้ในทางอนุกรม แต่หลาย ๆ เธรดอนุกรมสามารถทำงานพร้อมกันได้ ตราบใดที่เราได้รับวิธีการแจกแจงแบบขนานที่ดี (เช่น round robin) เราสามารถควบคุมจำนวนการส่งแถวไปยังแต่ละ thread ได้อย่างแม่นยำ

ฉันกำลังเรียกใช้คิวรีกับ DOP 4 ดังนั้นฉันต้องแบ่งNULLแถวในตารางให้เท่ากันเป็นสี่ถัง วิธีหนึ่งในการทำเช่นนี้คือการสร้างกลุ่มดัชนีในคอลัมน์จากการคำนวณ:

ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);

CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);

ฉันไม่แน่ใจว่าทำไมดัชนีแยกต่างหากสี่ตัวจึงเร็วกว่าดัชนีหนึ่งเล็กน้อย แต่นั่นคือสิ่งที่ฉันพบในการทดสอบ

ที่จะได้รับการวางแผนห่วงขนานซ้อนกันฉันจะไปใช้ที่ไม่มีเอกสารร่องรอยธง 8649 ฉันจะเขียนรหัสแปลก ๆ เล็กน้อยเพื่อกระตุ้นให้เครื่องมือเพิ่มประสิทธิภาพไม่ประมวลผลแถวเกินความจำเป็น ด้านล่างเป็นหนึ่งในการติดตั้งที่ใช้งานได้ดี:

SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM 
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY 
(
    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);

ผลการทดลองสิบครั้ง:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

จากการสืบค้นนั้นเรามีซีพียูต่ออัตราส่วนเวลาที่ผ่านไป 3.85! เราได้ตัดออกจากรันไทม์ 17 มิลลิวินาทีและใช้คอลัมน์และดัชนีที่คำนวณได้ 4 อันเท่านั้น! แต่ละเธรดจะประมวลผลใกล้เคียงกับจำนวนแถวโดยรวมเนื่องจากแต่ละดัชนีมีจำนวนแถวที่ใกล้เคียงกันมากและแต่ละเธรดจะสแกนเพียงดัชนีเดียวเท่านั้น:

แบ่งงานได้ดี

ในบันทึกสุดท้ายเรายังสามารถกดปุ่มง่ายและเพิ่ม CCI ที่ไม่ใช่แบบคลัสเตอร์ไปยังAgeคอลัมน์:

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

แบบสอบถามต่อไปนี้เสร็จใน 3 ms บนเครื่องของฉัน:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

มันจะยากที่จะเอาชนะ


7

แม้ว่าฉันจะไม่มีสำเนาของฐานข้อมูลสแต็คโอเวอร์โฟลว์ในเครื่อง แต่ฉันสามารถลองค้นหาได้สองสามข้อ ความคิดของฉันคือการได้รับจำนวนผู้ใช้จากมุมมองแคตตาล็อกระบบ (ตรงข้ามกับการรับจำนวนแถวโดยตรงจากตารางต้นแบบ) จากนั้นรับจำนวนแถวที่ไม่ตรงกับเกณฑ์ของ Erik และทำคณิตศาสตร์ง่ายๆ

ฉันใช้Stack Exchange Data Explorer (พร้อมด้วยSET STATISTICS TIME ON;และSET STATISTICS IO ON;) เพื่อทดสอบการสืบค้น สำหรับจุดอ้างอิงนี่คือแบบสอบถามและสถิติ CPU / IO:

ข้อความค้นหา 1

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 0 ms (1 แถวส่งคืน)

ตาราง 'ผู้ใช้' จำนวนการสแกน 17, การอ่านเชิงตรรกะ 201567, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 2740, lob การอ่านเชิงตรรกะ 0, lob ทางกายภาพอ่าน 0, lob การอ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 1829 ms, เวลาที่ผ่านไป = 296 ms

ข้อความค้นหา 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 0 ms (1 แถวส่งคืน)

ตาราง 'ผู้ใช้' จำนวนการสแกน 17, อ่านโลจิคัล 201567, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, lob อ่านโลจิคัล 0, lob อ่านฟิสิคัล 0, lob อ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 2500 ms, เวลาที่ผ่านไป = 147 ms

ข้อความค้นหา 3

--Erik's derived tables/UNION ALL query.
SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 0 ms (1 แถวส่งคืน)

ตาราง 'ผู้ใช้' จำนวนการสแกน 34, อ่านโลจิคัล 403134, ฟิสิคัลอ่าน 0, อ่านล่วงหน้าอ่าน 0, โลปลอจิกอ่าน 0, lob ฟิสิคัลอ่าน 0, lob อ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 3156 ms, เวลาที่ผ่านไป = 215 ms

ความพยายามครั้งที่ 1

นี่ช้ากว่าข้อความค้นหาทั้งหมดของ Erik ที่ฉันระบุไว้ที่นี่ ... อย่างน้อยก็ในแง่ของเวลาที่ผ่านไป

SELECT SUM(p.Rows)  -
  (
    SELECT COUNT(*)
    FROM dbo.Users AS u
    WHERE u.Age >= 18
  ) 
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 0 ms (1 แถวส่งคืน)

ตาราง 'โต๊ะทำงาน' จำนวนการสแกน 0, อ่านโลจิคัล 0, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, ลอจิคัลอ่าน 0, ลูกเทนนิสอ่าน 0, ลูกเทนนิสอ่านล่วงหน้าอ่าน 0 ตาราง 'sysrowsets' จำนวนการสแกน 2, อ่านโลจิคัล 10, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, โลจิคัลอ่าน 0 จำนวนการสแกน 1, การอ่านเชิงตรรกะ 4, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 0, การอ่านตรรกะล่วงหน้า lob 0, lob ทางกายภาพอ่าน 0, lob การอ่านล่วงหน้าอ่าน 0 ตาราง 'ผู้ใช้' จำนวนการสแกน 1, การอ่านเชิงตรรกะ 201567, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 0, lob ตรรกะอ่าน 0, lob การอ่านทางกายภาพ 0, lob การอ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 593 ms, เวลาที่ผ่านไป = 598 ms

2nd พยายาม

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

DECLARE @Total INT;

SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 0 ms ตาราง 'โต๊ะทำงาน' จำนวนการสแกน 0, อ่านโลจิคัล 0, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, โลจิคัลอ่าน 0 จำนวนการสแกน 2, อ่านโลจิคัล 10, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, โลจิคัลอ่าน 0 จำนวนการสแกน 1, การอ่านเชิงตรรกะ 4, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 0, lob การอ่านตรรกะ 0, lob ทางกายภาพอ่าน 0, lob การอ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 0 ms, เวลาที่ผ่านไป = 1 ms (1 แถวส่งคืน)

ตาราง 'ผู้ใช้' จำนวนการสแกน 17, อ่านโลจิคัล 201567, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, lob อ่านโลจิคัล 0, lob อ่านฟิสิคัล 0, lob อ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server: เวลา CPU = 1471 ms, เวลาที่ผ่านไป = 98 ms

หมายเหตุอื่น ๆ : ไม่อนุญาตให้ใช้ DBCE TRACEON ใน Stack Exchange Data Explorer ดังที่ระบุไว้ด้านล่าง:

ผู้ใช้ 'STACKEXCHANGE \ svc_sede' ไม่ได้รับอนุญาตให้เรียกใช้ DBCC TRACEON


1
พวกเขาอาจไม่มีดัชนีเดียวกันกับที่ฉันทำดังนั้นความแตกต่าง แล้วใครจะไปรู้ล่ะ บางทีเซิร์ฟเวอร์บ้านของฉันอยู่บนฮาร์ดแวร์ที่ดีกว่า;) คำตอบที่ดีแม้ว่า!
Erik Darling

คุณควรใช้แบบสอบถามต่อไปนี้สำหรับ attemp แรกของคุณ (จะเร็วกว่ามากเนื่องจาก rids ของ sys.objects-overhead): SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Thomas Franz

PS: โปรดทราบว่าดัชนีในหน่วยความจำ (NONCLUSTERED HASH) ไม่มีดัชนี id = 0/1 เป็นดัชนีกอง / คลัสเตอร์ทั่วไปจะมี)
โทมัสฟรานซ์

1

ใช้ตัวแปรหรือไม่

declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;

ตามความคิดเห็นสามารถข้ามตัวแปร

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);

3
ด้วย:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ

3
อาจต้องการลองในขณะที่ตรวจสอบ CPU & IO คำแนะนำ: มันเหมือนกับคำตอบหนึ่งของ Erik
Brent Ozar

0

ใช้อย่างดี SET ANSI_NULLS OFF;

SET ANSI_NULLS OFF; 
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18

Table 'Users'. Scan count 17, logical reads 201567

 SQL Server Execution Times:
 CPU time = 2344 ms,  elapsed time = 166 ms.

นี่คือสิ่งที่เพิ่งผุดขึ้นในใจของฉันเพียงแค่ดำเนินการในhttps://data.stackexchange.com

แต่ไม่มีประสิทธิภาพเท่า @ blitz_erik


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