ทำไมแบบสอบถาม SELECT DISTINCT TOP N ของฉันจึงสแกนทั้งตาราง


27

ฉันใช้SELECT DISTINCT TOP Nแบบสอบถามน้อยซึ่งดูเหมือนว่าจะเพิ่มประสิทธิภาพไม่ดีโดยเพิ่มประสิทธิภาพแบบสอบถาม SQL Server เริ่มจากการพิจารณาตัวอย่างเล็ก ๆ น้อย ๆ : ตารางหนึ่งล้านแถวที่มีสองค่าสลับกัน ฉันจะใช้ฟังก์ชันGetNumsเพื่อสร้างข้อมูล:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

สำหรับแบบสอบถามต่อไปนี้:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server สามารถหาสองค่าที่แตกต่างกันเพียงโดยการสแกนข้อมูลหน้าแรกของตาราง แต่มันสแกนข้อมูลทั้งหมดแทน เหตุใด SQL Server จึงไม่สแกนจนกว่าจะพบจำนวนค่าที่ต้องการแตกต่างกันตามที่ร้องขอ

สำหรับคำถามนี้โปรดใช้ข้อมูลทดสอบต่อไปนี้ที่มี 10 ล้านแถวพร้อม 10 ค่าที่ต่างกันที่สร้างขึ้นในบล็อก:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

คำตอบสำหรับตารางที่มีดัชนีคลัสเตอร์ยังยอมรับได้:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

แบบสอบถามต่อไปนี้จะสแกนทั้ง 10 ล้านแถวจากตาราง ฉันจะหาสิ่งที่ไม่สแกนทั้งตารางได้อย่างไร ฉันใช้ SQL Server 2016 SP1

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

เคอร์เซอร์อาจใช้งานได้ 10
paparazzo

คำตอบ:


29

มีกฎของเครื่องมือเพิ่มประสิทธิภาพที่แตกต่างกันสามข้อที่สามารถทำการดำเนินDISTINCTการในแบบสอบถามด้านบนได้ แบบสอบถามต่อไปนี้จะโยนข้อผิดพลาดที่แนะนำว่ารายการหมดจด:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

ข่าวสารเกี่ยวกับ 8622 ระดับ 16 สถานะ 1 บรรทัด 1

ตัวประมวลผลแบบสอบถามไม่สามารถสร้างแผนแบบสอบถามได้เนื่องจากคำแนะนำที่กำหนดไว้ในแบบสอบถามนี้ ส่งแบบสอบถามอีกครั้งโดยไม่ระบุคำแนะนำใด ๆ และไม่ใช้ SET FORCEPLAN

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

ตัวดำเนินการจับคู่แฮช ( การไหลที่ต่างกัน ) เป็นวิธีหนึ่งในการแก้ปัญหานี้เนื่องจากไม่มีการบล็อก SQL Server ควรจะสามารถหยุดการสแกนได้เมื่อพบว่ามีค่าที่แตกต่างกันเพียงพอ

ตัวดำเนินการเชิงตรรกะของ Flow Distinct สแกนอินพุตโดยลบข้อมูลที่ซ้ำกัน ในขณะที่โอเปอเรเตอร์ Distinct จะใช้อินพุตทั้งหมดก่อนที่จะสร้างเอาต์พุตใด ๆ โอเปอเรเตอร์ Flow Distinct จะส่งกลับแต่ละแถวตามที่ได้รับจากอินพุต (เว้นแต่แถวนั้นจะซ้ำกัน

เหตุใดแบบสอบถามในคำถามจึงใช้การจับคู่แฮช (รวม) แทนการจับคู่แฮช (การไหลของข้อมูลต่างกัน) เนื่องจากจำนวนค่าที่แตกต่างกันมีการเปลี่ยนแปลงในตารางฉันคาดว่าค่าใช้จ่ายของแบบสอบถามแฮช (ความแตกต่างของการไหล) จะลดลงเนื่องจากการประมาณจำนวนแถวที่ต้องการสแกนไปยังตารางควรลดลง ฉันคาดว่าค่าใช้จ่ายในการวางแผนการจับคู่แฮช (รวม) จะเพิ่มขึ้นเนื่องจากตารางแฮชที่ต้องสร้างจะใหญ่ขึ้น วิธีหนึ่งในการตรวจสอบเรื่องนี้โดยการสร้างคู่มือแผน ถ้าฉันสร้างสำเนาของข้อมูลสองชุด แต่ใช้คำแนะนำแผนกับหนึ่งชุดนั้นฉันควรจะสามารถเปรียบเทียบการจับคู่แบบแฮช (รวม) กับการจับคู่แบบแฮช (ชัดเจน) แบบเคียงข้างกับข้อมูลเดียวกัน โปรดทราบว่าฉันไม่สามารถทำได้โดยการปิดใช้งานกฎเครื่องมือเพิ่มประสิทธิภาพคิวรีเนื่องจากกฎเดียวกันนี้ใช้กับทั้งสองแผน ( GbAggToHS)

ต่อไปนี้เป็นวิธีหนึ่งในการรับคู่มือวางแผนที่ฉันตามอยู่:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

รับหมายเลขอ้างอิงของแผนและใช้เพื่อสร้างคำแนะนำแผน:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

คำแนะนำการวางแผนใช้ได้กับข้อความค้นหาที่ถูกต้องเท่านั้นดังนั้นให้คัดลอกกลับมาจากคำแนะนำแผน:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

รีเซ็ตข้อมูล:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

รับแผนคิวรีสำหรับเคียวรีที่มีการนำแผนไปใช้:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

นี่คือตัวดำเนินการจับคู่แฮช (การไหลที่ต่างกัน) ที่เราต้องการด้วยข้อมูลการทดสอบของเรา โปรดทราบว่า SQL Server คาดว่าจะอ่านแถวทั้งหมดจากตารางและค่าใช้จ่ายโดยประมาณนั้นเหมือนกันกับแผนที่มีแฮชตรงกัน (รวม) การทดสอบที่ฉันแนะนำว่าค่าใช้จ่ายสำหรับแผนสองแผนนั้นเหมือนกันเมื่อเป้าหมายแถวสำหรับแผนมากกว่าหรือเท่ากับจำนวนของค่าที่แตกต่าง SQL Server คาดหวังจากตารางซึ่งในกรณีนี้สามารถได้มาจาก สถิติ. น่าเสียดายที่ (สำหรับการค้นหาของเรา) เครื่องมือเพิ่มประสิทธิภาพเลือกการจับคู่แฮช (รวม) เหนือการจับคู่แฮช (การไหลเวียนต่างกัน) เมื่อต้นทุนเท่ากัน ดังนั้นเราจึงใช้เครื่องมือเพิ่มประสิทธิภาพเวทย์ 0.0000001 หน่วยห่างจากแผนที่เราต้องการ

วิธีหนึ่งในการโจมตีปัญหานี้คือการลดเป้าหมายแถว หากเป้าหมายแถวจากมุมมองเป็นเครื่องมือเพิ่มประสิทธิภาพน้อยกว่าจำนวนแถวที่แตกต่างกันเราอาจได้รับการจับคู่แฮช (โฟลว์ต่างกัน) สิ่งนี้สามารถทำได้ด้วยOPTIMIZE FORคำใบ้แบบสอบถาม:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

สำหรับเคียวรีนี้เครื่องมือเพิ่มประสิทธิภาพจะสร้างแผนราวกับว่าคิวรีต้องการแถวแรก แต่เมื่อเรียกใช้คิวรีแบบสอบถามจะได้รับแถว 10 แถวแรกกลับมา ในเครื่องของฉันเคียวรีนี้จะสแกน 892800 แถวจากX_10_DISTINCT_HEAPและเสร็จใน 299 ms กับเวลา CPU 250 มิลลิวินาทีและอ่านตรรกะ 2537 ครั้ง

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

อีกวิธีหนึ่งในการโจมตีปัญหานี้คือการขยายจำนวนค่าที่แตกต่างกันโดยประมาณ SQL Server คาดว่าจะได้รับจากตารางฐาน มันยากกว่าที่คิด การใช้ฟังก์ชั่นที่กำหนดขึ้นอาจไม่สามารถเพิ่มจำนวนผลลัพธ์ที่แตกต่างได้ หากเครื่องมือเพิ่มประสิทธิภาพการสืบค้นทราบถึงความจริงทางคณิตศาสตร์ (การทดสอบบางอย่างชี้ให้เห็นว่าอย่างน้อยก็เพื่อจุดประสงค์ของเรา) จากนั้นการใช้ฟังก์ชั่นที่กำหนด (ซึ่งรวมถึงฟังก์ชั่นสตริงทั้งหมด ) จะไม่เพิ่มจำนวนแถว

หลายฟังก์ชั่น nondeterministic ไม่ได้ทำงานอย่างใดอย่างหนึ่งรวมทั้งตัวเลือกที่ชัดเจนของและNEWID() RAND()อย่างไรก็ตามLAG()ไม่หลอกลวงสำหรับแบบสอบถามนี้ แบบสอบถามเพิ่มประสิทธิภาพคาดว่า 10 ล้านค่าที่แตกต่างกันกับLAGการแสดงออกซึ่งจะกระตุ้นให้เกิดการแข่งขันแฮช (ไหลที่แตกต่างกัน) แผน :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

ในเครื่องของฉันเคียวรีนี้จะสแกน 892800 แถวจากX_10_DISTINCT_HEAPและทำให้เสร็จใน 1165 ms ด้วยเวลา CPU 1109 ms และการอ่านโลจิคัล 2537 ดังนั้นการLAG()เพิ่มค่าใช้จ่ายค่อนข้างสัมพันธ์ @Paul White แนะนำให้ลองประมวลผลโหมดแบตช์สำหรับแบบสอบถามนี้ บน SQL Server 2016 MAXDOP 1เราจะได้รับการประมวลผลโหมดแบทช์แม้จะมี วิธีหนึ่งในการรับการประมวลผลโหมดแบตช์สำหรับตาราง rowstore คือการเข้าร่วม CCI ที่ว่างเปล่าดังนี้:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

โค้ดดังกล่าวส่งผลในแผนคิวรีนี้

พอลชี้ให้เห็นว่าฉันต้องเปลี่ยนแบบสอบถามที่จะใช้LAG(..., 1)เพราะLAG(..., 0)ดูเหมือนจะไม่มีสิทธิ์ได้รับการเพิ่มประสิทธิภาพ Window Aggregate การเปลี่ยนแปลงนี้ลดเวลาที่ผ่านไปเป็น 520 ms และเวลา CPU เป็น 454 ms

โปรดทราบว่าLAG()วิธีการไม่ใช่วิธีที่เสถียรที่สุด หาก Microsoft เปลี่ยนสมมติฐานที่ไม่ซ้ำกับฟังก์ชันแล้วอาจไม่ทำงานอีกต่อไป มันมีการประเมินที่แตกต่างกับ CE ดั้งเดิม นอกจากนี้การเพิ่มประสิทธิภาพประเภทนี้กับฮีปก็ไม่จำเป็นต้องเป็นความคิดที่ดี หากตารางถูกสร้างใหม่เป็นไปได้ที่จะจบลงในสถานการณ์กรณีที่เลวร้ายที่สุดซึ่งต้องอ่านแถวเกือบทั้งหมดจากตาราง

เทียบกับตารางที่มีคอลัมน์ที่ไม่ซ้ำกัน (เช่นตัวอย่างดัชนีกลุ่มในคำถาม) เรามีตัวเลือกที่ดีกว่า ตัวอย่างเช่นเราสามารถหลอกลวงเครื่องมือเพิ่มประสิทธิภาพโดยใช้SUBSTRINGนิพจน์ที่ส่งคืนสตริงว่างเสมอ SQL Server ไม่คิดว่าSUBSTRINGจะเปลี่ยนจำนวนค่าที่แตกต่างดังนั้นถ้าเราใช้กับคอลัมน์ที่ไม่ซ้ำเช่น PK จำนวนแถวที่แตกต่างกันโดยประมาณคือ 10 ล้าน แบบสอบถามต่อไปนี้ได้รับตัวดำเนินการแฮช (การไหลที่ต่างกัน):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

ในเครื่องของฉันเคียวรีนี้จะสแกนแถว 900,000 แถวX_10_DISTINCT_CIและเสร็จใน 333 ms กับเวลา CPU 297 ms และอ่านโลจิคัล 3011 ครั้ง

โดยสรุปเคียวรีเครื่องมือเพิ่มประสิทธิภาพดูเหมือนจะสมมติว่าแถวทั้งหมดจะถูกอ่านจากตารางสำหรับSELECT DISTINCT TOP Nคิวรีเมื่อN> = จำนวนแถวที่แตกต่างกันโดยประมาณจากตาราง ตัวดำเนินการจับคู่แฮช (รวม) อาจมีค่าใช้จ่ายเหมือนกับตัวดำเนินการจับคู่แฮช (การไหลที่ต่างกัน) แต่เครื่องมือเพิ่มประสิทธิภาพจะเลือกตัวดำเนินการรวมเสมอ สิ่งนี้สามารถนำไปสู่การอ่านแบบลอจิคัลที่ไม่จำเป็นเมื่อค่าที่ต่างกันมากพออยู่ใกล้กับจุดเริ่มต้นของการสแกนตาราง สองวิธีในการหลอกลวงเครื่องมือเพิ่มประสิทธิภาพให้ใช้ตัวดำเนินการจับคู่แฮช (การไหลที่ต่างกัน) คือการลดเป้าหมายแถวโดยใช้OPTIMIZE FORคำใบ้หรือเพื่อเพิ่มจำนวนแถวที่แตกต่างกันโดยประมาณโดยใช้LAG()หรือSUBSTRINGในคอลัมน์ที่ไม่ซ้ำกัน


12

คุณได้ตอบคำถามของคุณเองอย่างถูกต้องแล้ว

ฉันแค่ต้องการเพิ่มการสังเกตว่าวิธีที่มีประสิทธิภาพมากที่สุดคือการสแกนทั้งตาราง - หากสามารถจัดเรียงเป็น'heap' ในคอลัมน์ที่เก็บได้ :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

แบบสอบถามง่ายๆ:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

จากนั้นให้:

แผนการดำเนินการ

ตาราง 'X_10_DISTINCT_HEAP' สแกนจำนวน 1,
 ตรรกะอ่าน 0, ฟิสิคัลอ่าน 0, อ่านล่วงหน้าอ่าน 0, 
 lob ลอจิคัลอ่าน 66 , lob ฟิสิคัลอ่าน 0, lob อ่านล่วงหน้าอ่าน 0
ตาราง 'X_10_DISTINCT_HEAP' ส่วนอ่าน 13 ส่วนที่ข้าม 0

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

Hash Match (Flow Distinct) ไม่สามารถดำเนินการได้ในโหมดแบตช์ วิธีการที่ใช้สิ่งนี้ช้ากว่ามากเนื่องจากมีการเปลี่ยนแปลงราคาแพง (มองไม่เห็น) จากการประมวลผลแบบแบทช์เป็นแถว ตัวอย่างเช่น:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

ให้:

แผนการดำเนินงานที่แตกต่างของโฟล

ตาราง 'X_10_DISTINCT_HEAP' สแกนจำนวน 1,
 ตรรกะอ่าน 0, ฟิสิคัลอ่าน 0, อ่านล่วงหน้าอ่าน 0, 
 lob ลอจิกอ่าน 20 , lob ฟิสิคัลอ่าน 0, lob อ่านล่วงหน้าอ่าน 0
ตาราง 'X_10_DISTINCT_HEAP' ส่วนอ่าน 4ส่วนข้าม 0

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

นี่คือช้ากว่าเมื่อตารางถูกจัดเป็นกอง rowstore


4

นี่คือความพยายามจำลองการสแกนบางส่วนซ้ำ (คล้ายกับ แต่ไม่เหมือนกับการสแกนข้าม) โดยใช้ CTE แบบเรียกซ้ำ จุดมุ่งหมาย - เนื่องจากเราไม่มีดัชนี(id)- เพื่อหลีกเลี่ยงการเรียงลำดับและการสแกนหลายครั้งบนโต๊ะ

มีเทคนิคเล็กน้อยที่จะข้ามข้อ จำกัด CTE แบบเรียกซ้ำได้:

  • ไม่TOPอนุญาตในส่วนวนซ้ำ เราใช้แบบสอบถามย่อยและROW_NUMBER()แทน
  • เราไม่สามารถมีการอ้างอิงหลายส่วนกับส่วนคงที่หรือใช้LEFT JOINหรือใช้NOT IN (SELECT id FROM cte)จากส่วนซ้ำ เพื่อหลีกเลี่ยงการที่เราสร้างVARCHARสตริงที่สะสมทั้งหมดidค่าคล้ายกับSTRING_AGGหรือ hierarchyID LIKEแล้วเปรียบเทียบกับ

สำหรับกอง (สมมติคอลัมน์ชื่อid) ทดสอบ 1 rextester.com

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

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

และเมื่อตารางคลัสเตอร์ (CI บนunique_key) ทดสอบ 2 rextester.com

สิ่งนี้ใช้ดัชนีคลัสเตอร์ ( WHERE x.unique_key > ct.unique_key) เพื่อหลีกเลี่ยงการสแกนหลายครั้ง:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

มีปัญหาประสิทธิภาพการทำงานที่ละเอียดอ่อนด้วยวิธีนี้ มันลงเอยด้วยการค้นหาเพิ่มเติมบนโต๊ะหลังจากพบค่า Nth ดังนั้นหากมี 10 ค่าที่แตกต่างกันสำหรับ 10 อันดับแรกมันจะค้นหาค่าที่ 11 ซึ่งไม่มีอยู่ คุณจบลงด้วยการสแกนเต็มรูปแบบเพิ่มเติมและการคำนวณ 10 ล้าน ROW_NUMBER () เพิ่มขึ้นจริงๆ ฉันมีวิธีแก้ปัญหาที่นี่ซึ่งเพิ่มความเร็วในการสืบค้น 20X ในเครื่องของฉัน คุณคิดอย่างไร? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish

2

เพื่อความสมบูรณ์อีกวิธีหนึ่งในการแก้ไขปัญหานี้คือการใช้งานภายนอก เราสามารถเพิ่มOUTER APPLYโอเปอเรเตอร์สำหรับแต่ละค่าที่แตกต่างที่เราต้องการค้นหา นี่เป็นแนวคิดที่คล้ายกับวิธีการเรียกซ้ำของ ypercube แต่มีการเรียกซ้ำด้วยมืออย่างมีประสิทธิภาพ ข้อดีอย่างหนึ่งคือเราสามารถใช้TOPในตารางที่ได้รับแทนROW_NUMBER()วิธีแก้ปัญหา ข้อเสียอย่างหนึ่งที่สำคัญคือข้อความค้นหาจะได้รับนานNขึ้นเมื่อเพิ่มขึ้น

นี่คือการใช้งานหนึ่งสำหรับแบบสอบถามกับฮีป:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

นี่คือแผนแบบสอบถามจริงสำหรับแบบสอบถามด้านบน ในเครื่องของฉันการค้นหานี้เสร็จสมบูรณ์ใน 713 ms ด้วยเวลา CPU 625 ms และการอ่านแบบลอจิคัล 12605 เราได้รับค่าที่แตกต่างใหม่ทุก ๆ 100k แถวดังนั้นฉันคาดหวังว่าแบบสอบถามนี้จะสแกนประมาณ 900,000 * 10 * 0.5 = 4500000 แถว ในทางทฤษฎีแล้วแบบสอบถามนี้ควรทำห้าครั้งที่ตรรกะอ่านของแบบสอบถามนี้จากคำตอบอื่น:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

แบบสอบถามนั้นทำการอ่านเชิงตรรกะ 2537 ครั้ง 2537 * 5 = 12685 ซึ่งค่อนข้างใกล้เคียงกับ 12605

สำหรับตารางที่มีดัชนีคลัสเตอร์เราสามารถทำได้ดีกว่า นี่เป็นเพราะเราสามารถส่งผ่านค่าคีย์คลัสเตอร์ล่าสุดลงในตารางที่ได้รับเพื่อหลีกเลี่ยงการสแกนแถวเดียวกันสองครั้ง การใช้งานหนึ่ง:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

นี่คือแผนแบบสอบถามจริงสำหรับแบบสอบถามด้านบน ในเครื่องของฉันการค้นหานี้เสร็จสมบูรณ์ใน 154 ms ด้วยเวลา 140 ms ของ CPU และการอ่านแบบลอจิคัล 3203 ดูเหมือนว่าจะทำงานได้เร็วกว่าสักเล็กน้อยOPTIMIZE FORแบบสอบถามสำหรับตารางดัชนีคลัสเตอร์ ฉันไม่ได้คาดหวังดังนั้นฉันจึงพยายามวัดประสิทธิภาพอย่างระมัดระวังมากขึ้น วิธีการของผมคือการทำงานในแต่ละแบบสอบถามสิบครั้งโดยไม่ต้องชุดผลลัพธ์และดูที่ตัวเลขรวมจากและsys.dm_exec_sessions sys.dm_exec_session_wait_statsเซสชันที่ 56 เป็นAPPLYคิวรีและเซสชัน 63 เป็นOPTIMIZE FORคิวรี

ผลผลิตของ sys.dm_exec_sessions :

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

มีข้อได้เปรียบที่ชัดเจนใน cpu_time และ elapsed_time สำหรับการAPPLYสืบค้น

ผลผลิตของ sys.dm_exec_session_wait_stats :

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FORแบบสอบถามมีชนิดการรอเพิ่มเติมRESERVED_MEMORY_ALLOCATION_EXT ฉันไม่รู้ว่ามันหมายถึงอะไรกันแน่ อาจเป็นการวัดค่าใช้จ่ายในตัวดำเนินการจับคู่แฮช (การไหลที่ต่างกัน) ไม่ว่าในกรณีใด ๆ อาจไม่คุ้มค่าที่จะกังวลเกี่ยวกับความแตกต่างของ 70 มิลลิวินาทีในเวลา CPU


1

ฉันคิดว่าคุณมีคำตอบว่าทำไม
นี่อาจเป็นวิธีแก้ปัญหาที่
ฉันรู้ว่ามันดูยุ่งเหยิง แต่แผนการดำเนินการบอกว่าชัดเจนที่สุด 2 คือ 84% ของราคา

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

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

-4

ฉันคิดว่าคุณต้องยืนดูคำถามของคุณอย่างเป็นกลางเพื่อทำความเข้าใจกับสิ่งที่คุณเห็น

เป็นไปได้อย่างไรที่เครื่องมือเพิ่มประสิทธิภาพข้อความค้นหาจะเลือกค่าที่แตกต่าง 10 อันดับแรกโดยไม่ระบุรายการที่สมบูรณ์ของค่าที่แตกต่างกันก่อน

เลือก Distinct ต้องสแกนแบบเต็มตาราง (หรือดัชนีครอบคลุม) เพื่อระบุชุดผลลัพธ์ ลองคิดดู - แถวสุดท้ายในตารางอาจมีค่าที่ไม่เคยเห็นมาก่อน

Select Distinct เป็นอาวุธทื่อมาก


2
ไม่ได้จริงๆ หากฉันสแกนตารางและ 20 แถวแรกมีค่าแตกต่างกัน 10 ค่าเหตุใดฉันจึงต้องสแกนส่วนที่เหลือต่อไป
ypercubeᵀᴹ

2
ทำไมถึงต้องมองหาเมื่อฉันขอเพียง 10 เท่านั้น พบค่าที่แตกต่างกัน 10 ค่าแล้วควรหยุด นั่นคือปัญหาของคำถาม
ypercubeᵀᴹ

3
เหตุใดการค้นหา N อันดับแรกจึงต้องดูชุดผลลัพธ์ทั้งหมดก่อน หากมันมีค่าที่แตกต่างกัน 10 ค่าและนั่นคือทั้งหมดที่คุณสนใจสามารถหยุดหาค่าอื่น ๆ หากต้องจัดเรียงชุดผลลัพธ์ทั้งหมดเพื่อให้ทราบว่า "เป็น" 10 รายการแรกเป็นอีกเรื่องหนึ่ง แต่ถ้าคุณต้องการเพียง 10 ค่าที่แตกต่างโดยไม่สนใจว่าชุดใดที่ 10 ไม่มีข้อกำหนดเชิงตรรกะในการรับชุดผลลัพธ์ทั้งหมด
Tom V - Team Monica

2
แค่คิดว่าตัวเองมอบหมายให้คืนชุดที่ร้องขอ คุณถูกขอให้ให้ค่าสิบอันดับแรกที่แตกต่างจากหลายสิบล้านและไม่ได้รับคำสั่งให้ทำตามคำสั่งการเรียงลำดับใด ๆ คุณจะรู้สึกว่าตัวเองจำเป็นต้องทำตามชุดค่าทั้งหมดหรือไม่ถ้าคุณไปถึงผลลัพธ์หลังจากดูแล้วพูดว่า 100 รายการแรก? นั่นอาจจะไม่มีจุดหมายตอนนี้การใช้ตรรกะนั้นในผลิตภัณฑ์ฐานข้อมูลเป็นอีกเรื่องหนึ่ง แต่คุณดูเหมือนจะแนะนำว่าจำเป็นต้องมีเหตุผลในการสแกนตารางทั้งหมดสำหรับปัญหานี้ซึ่งไม่ใช่
Andriy M

4
@Marco: ฉันไม่เห็นด้วยนี่เป็นคำตอบ มันเกิดขึ้นเพียงอย่างเดียวเมื่อผู้ตอบไม่เห็นด้วยกับคำถามและตอบสิ่งที่เขา / เธอเห็นว่าเป็นการเข้าใจผิดของ OP
Andriy M
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.