การสแกนที่ไม่คาดหมายระหว่างการลบโดยใช้ WHERE IN


40

ฉันมีคำถามดังนี้

DELETE FROM tblFEStatsBrowsers WHERE BrowserID NOT IN (
    SELECT DISTINCT BrowserID FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID IS NOT NULL
)

tblFEStatsBrowsers มี 553 แถว
tblFEStatsPaperHits มีแถว 47.974.301

tblFEStatsBrowsers:

CREATE TABLE [dbo].[tblFEStatsBrowsers](
    [BrowserID] [smallint] IDENTITY(1,1) NOT NULL,
    [Browser] [varchar](50) NOT NULL,
    [Name] [varchar](40) NOT NULL,
    [Version] [varchar](10) NOT NULL,
    CONSTRAINT [PK_tblFEStatsBrowsers] PRIMARY KEY CLUSTERED ([BrowserID] ASC)
)

tblFEStatsPaperHits:

CREATE TABLE [dbo].[tblFEStatsPaperHits](
    [PaperID] [int] NOT NULL,
    [Created] [smalldatetime] NOT NULL,
    [IP] [binary](4) NULL,
    [PlatformID] [tinyint] NULL,
    [BrowserID] [smallint] NULL,
    [ReferrerID] [int] NULL,
    [UserLanguage] [char](2) NULL
)

มีดัชนีคลัสเตอร์บน tblFEStatsPaperHits ที่ไม่มี BrowserID การดำเนินการค้นหาภายในจึงต้องใช้การสแกนตารางแบบเต็มรูปแบบของ tblFEStatsPaperHits - ซึ่งใช้ได้ทั้งหมด

ปัจจุบันทำการสแกนเต็มรูปแบบสำหรับแต่ละแถวใน tblFEStatsBrowsers ซึ่งหมายความว่าฉันมีการสแกนตารางเต็มรูปแบบ 553 รายการของ tblFEStatsPaperHits

การเขียนซ้ำเป็นเพียงตำแหน่งที่ไม่ได้เปลี่ยนแผน:

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
)

อย่างไรก็ตามตามคำแนะนำของ Adam Machanic การเพิ่มตัวเลือก HASH JOIN จะส่งผลให้แผนการดำเนินการที่ดีที่สุด (เพียงสแกน tblFEStatsPaperHits เพียงครั้งเดียว):

DELETE FROM tblFEStatsBrowsers WHERE NOT EXISTS (
    SELECT * FROM tblFEStatsPaperHits WITH (NOLOCK) WHERE BrowserID = tblFEStatsBrowsers.BrowserID
) OPTION (HASH JOIN)

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

เนื่องจาก QO ไม่มีสถิติใด ๆ ในคอลัมน์ BrowserID ฉันเดาว่ามันถือว่าเลวร้ายที่สุด - 50 ล้านค่าที่แตกต่างดังนั้นจึงต้องใช้โต๊ะทำงานหน่วยความจำ / tempdb ค่อนข้างใหญ่ ดังนั้นวิธีที่ปลอดภัยที่สุดคือทำการสแกนแต่ละแถวใน tblFEStatsBrowsers ไม่มีความสัมพันธ์กับ foreign key ระหว่างคอลัมน์ BrowserID ในสองตารางดังนั้น QO ไม่สามารถหักข้อมูลใด ๆ จาก tblFEStatsBrowsers

นี่เป็นเหตุผลง่ายๆหรือไม่?

อัปเดต 1
เพื่อให้สถิติสองสามตัวเลือก: ตัวเลือก (HASH JOIN):
208.711 การอ่านเชิงตรรกะ (สแกน 12 ครั้ง)

OPTION (LOOP JOIN, HASH GROUP):
11.008.698 การอ่านตรรกะ (~ สแกนต่อ BrowserID (339))

ไม่มีตัวเลือก:
11.008.775 การอ่านตรรกะ (~ สแกนต่อ BrowserID (339))

อัปเดต 2
คำตอบที่ยอดเยี่ยมพวกคุณทุกคน - ขอบคุณ! เลือกยากเพียงอันเดียว แม้ว่ามาร์ตินจะเป็นคนแรกและรีมัสเป็นวิธีแก้ปัญหาที่ยอดเยี่ยม แต่ฉันต้องมอบมันให้กับกีวีเพื่อไปใส่ใจในรายละเอียด :)


5
คุณสามารถสคริปสถิติตามคัดลอกสถิติจากเซิร์ฟเวอร์หนึ่งไปยังเซิร์ฟเวอร์อื่นเพื่อให้เราสามารถทำซ้ำได้หรือไม่?
Mark Storey-Smith

2
@ MarkStorey-Smith แน่ใจว่า - pastebin.com/9HHRPFgK สมมติว่าคุณเรียกใช้สคริปต์ในฐานข้อมูลที่ว่างเปล่าสิ่งนี้ทำให้ฉันสามารถสร้างแบบสอบถามที่มีปัญหาเมื่อรวมถึงการแสดงแผนการดำเนินการ แบบสอบถามทั้งสองจะรวมอยู่ในตอนท้ายของสคริปต์
Mark S. Rasmussen

คำตอบ:


61

"ฉันสงสัยว่าทำไมเครื่องมือเพิ่มประสิทธิภาพข้อความค้นหาจึงใช้แผนที่ทำอยู่ในปัจจุบัน"

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

แผนเดิม

ด้านในของการเข้าร่วมเป็นหลักเรียกใช้แบบสอบถามของแบบฟอร์มต่อไปนี้สำหรับแต่ละค่าความสัมพันธ์ของBrowserID:

DECLARE @BrowserID smallint;

SELECT 
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

สแกนฮิตกระดาษ

โปรดทราบว่าจำนวนแถวโดยประมาณคือ185,220 (ไม่ใช่289,013 ) เนื่องจากการเปรียบเทียบความเท่าเทียมไม่รวมโดยนัยNULL(ยกเว้นว่าANSI_NULLSเป็นOFF) ค่าใช้จ่ายโดยประมาณของแผนข้างต้นคือ206.8หน่วย

ตอนนี้เราเพิ่มTOP (1)ประโยค:

DECLARE @BrowserID smallint;

SELECT TOP (1)
    tfsph.BrowserID 
FROM dbo.tblFEStatsPaperHits AS tfsph 
WHERE 
    tfsph.BrowserID = @BrowserID 
OPTION (MAXDOP 1);

กับด้านบน (1)

ตอนนี้ต้นทุนโดยประมาณคือ0.00452หน่วย การเพิ่มตัวดำเนินการทางกายภาพยอดนิยมตั้งเป้าหมายแถวที่ 1 ของแถวที่ตัวดำเนินการสูงสุด คำถามนั้นจะกลายเป็นวิธีการได้รับ 'เป้าหมายแถว' สำหรับการสแกนดัชนีแบบคลัสเตอร์ นั่นคือจำนวนการสแกนที่คาดว่าจะดำเนินการก่อนที่แถวหนึ่งจะตรงกับภาคBrowserIDแสดง?

ข้อมูลสถิติที่มีอยู่แสดง166BrowserIDค่าที่แตกต่าง(1 / [ความหนาแน่นทั้งหมด] = 1 / 0.006024096 = 166) การคิดต้นทุนถือว่ามีการกระจายค่าที่แตกต่างกันอย่างสม่ำเสมอบนแถวทางกายภาพดังนั้นเป้าหมายของแถวบนการสแกนดัชนีแบบคลัสเตอร์ถูกตั้งค่าเป็น166.302 (การบัญชีสำหรับการเปลี่ยนแปลงในความสำคัญเชิงตารางนับตั้งแต่รวบรวมสถิติตัวอย่าง)

ค่าใช้จ่ายโดยประมาณของการสแกนแถวที่คาดหวัง 166 แถวนั้นไม่ใหญ่มาก (แม้จะถูกดำเนินการ 339 ครั้งต่อการเปลี่ยนแปลงแต่ละครั้งBrowserID) - การสแกนดัชนีแบบคลัสเตอร์แสดงค่าใช้จ่ายโดยประมาณ1.3219หน่วยซึ่งแสดงผลการปรับขนาดของเป้าหมายแถว ต้นทุนตัวดำเนินการที่ไม่มีการลดสัดส่วนสำหรับ I / O และ CPU แสดงเป็น153.931และ52.8698ตามลำดับ:

ค่าใช้จ่ายโดยประมาณของแถวเป้าหมาย

ในทางปฏิบัติมันเป็นไปได้ยากมากที่ 166 แถวแรกที่สแกนจากดัชนี (ในลำดับใดก็ตามที่พวกเขาจะถูกส่งคืน) จะมีBrowserIDค่าที่เป็นไปได้แต่ละค่า อย่างไรก็ตามDELETEแผนดังกล่าวมีต้นทุนรวมทั้งสิ้น1.40921หน่วยและถูกเลือกโดยเครื่องมือเพิ่มประสิทธิภาพด้วยเหตุผลดังกล่าว บาร์ตดันแคนแสดงตัวอย่างของประเภทนี้อีกในโพสต์ล่าสุดบรรดาศักดิ์เป้าหมายแถว Gone Rogue

นอกจากนี้ยังเป็นที่น่าสนใจที่จะต้องทราบว่าตัวดำเนินการยอดนิยมในแผนการดำเนินการไม่เกี่ยวข้องกับการเข้าร่วมกึ่งต่อต้าน เราสามารถเริ่มดูว่า Top มาจากไหนก่อนโดยปิดการใช้งานกฎการสำรวจที่เรียกว่าGbAggToConstScanOrTop :

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

GbAggToConstScanOrTop ปิดใช้งาน

แผนนั้นมีค่าใช้จ่ายประมาณ364.912และแสดงว่า Top แทนที่กลุ่มตามการรวม (จัดกลุ่มตามคอลัมน์ที่มีความสัมพันธ์กันBrowserID) ซึ่งรวมกันเป็นไม่ได้เนื่องจากซ้ำซ้อนDISTINCTในข้อความสอบถาม: มันคือการเพิ่มประสิทธิภาพที่สามารถนำมาใช้โดยสองกฎสำรวจLASJNtoLASJNonDistและLASJOnLclDist การปิดใช้งานทั้งสองนั้นเช่นกันสร้างแผนนี้

DBCC RULEOFF ('LASJNtoLASJNonDist');
DBCC RULEOFF ('LASJOnLclDist');
DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, LOOP JOIN, RECOMPILE);
GO
DBCC RULEON ('LASJNtoLASJNonDist');
DBCC RULEON ('LASJOnLclDist');
DBCC RULEON ('GbAggToConstScanOrTop');

Spool Plan

แผนนั้นมีราคาประมาณ40729.3หน่วย

หากไม่มีการเปลี่ยนแปลงจาก Group By to Top เครื่องมือเพิ่มประสิทธิภาพ 'อย่างเป็นธรรมชาติ' จะเลือกแผนการเข้าร่วมแฮชโดยมีการBrowserIDรวมก่อนการต่อต้านกึ่งเข้าร่วม:

DBCC RULEOFF ('GbAggToConstScanOrTop');
GO
DELETE FROM tblFEStatsBrowsers 
WHERE BrowserID NOT IN 
(
    SELECT DISTINCT BrowserID 
    FROM tblFEStatsPaperHits WITH (NOLOCK) 
    WHERE BrowserID IS NOT NULL
) OPTION (MAXDOP 1, RECOMPILE);
GO
DBCC RULEON ('GbAggToConstScanOrTop');

ไม่มีแผน DOP ยอดนิยม 1

และไม่มีข้อ จำกัด MAXDOP 1 แผนคู่ขนาน:

ไม่มีแผนคู่ขนานอันดับต้น ๆ

อีกวิธีในการ 'แก้ไข' แบบสอบถามต้นฉบับจะเป็นการสร้างดัชนีที่หายไปในBrowserIDรายงานแผนปฏิบัติการ การวนซ้ำซ้อนกันทำงานได้ดีที่สุดเมื่อมีการทำดัชนีด้านใน การประมาณความสำคัญของการรวมกึ่งมีความท้าทายในเวลาที่ดีที่สุด ไม่มีการจัดทำดัชนีที่เหมาะสม (ตารางขนาดใหญ่ไม่มีแม้แต่คีย์ที่ไม่ซ้ำกัน!) จะไม่ช่วยเลย

พอล


3
คุณคำนับคุณคุณเพิ่งแนะนำให้ฉันรู้จักกับแนวคิดใหม่ ๆ ที่ฉันไม่เคยพบมาก่อน เมื่อคุณรู้สึกว่าคุณรู้อะไรบางอย่างออกมาจะทำให้คุณผิดหวัง - การเพิ่มดัชนีจะช่วยได้แน่นอน อย่างไรก็ตามนอกเหนือจากการดำเนินการเพียงครั้งเดียวฟิลด์นี้จะไม่ถูกเข้าถึง / รวมโดยคอลัมน์ BrowserID และดังนั้นฉันจึงควรบันทึกไบต์เหล่านั้นเนื่องจากตารางมีขนาดค่อนข้างใหญ่ (นี่เป็นเพียงหนึ่งในฐานข้อมูลที่เหมือนกัน) ไม่มีคีย์เฉพาะบนโต๊ะเนื่องจากไม่มีความเป็นเอกลักษณ์ตามธรรมชาติ การเลือกทั้งหมดมาจาก PaperID และเลือกระยะเวลา
Mark S. Rasmussen

22

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

วางแผน

ตารางสำคัญที่แสดงในแผนคือ

  • tblFEStatsPaperHits: 48063400
  • tblFEStatsBrowsers : 339

ดังนั้นจึงประมาณว่าจะต้องทำการสแกนtblFEStatsPaperHits339 ครั้ง การสแกนแต่ละครั้งจะมีเพรดิเคตที่สัมพันธ์กันtblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULLซึ่งถูกดันลงไปในตัวดำเนินการสแกน

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

สำหรับการเข้าร่วมแฮชจะให้แผนด้านล่าง

เข้าร่วมแฮช

แผนโดยรวมนั้นมีค่าใช้จ่าย418.415(แพงกว่าแผนลูปซ้อนกันประมาณ 300 เท่า) ด้วยการสแกนดัชนีแบบคลัสเตอร์เดี่ยวทั้งหมดที่tblFEStatsPaperHitsคิดต้นทุน206.8เพียงอย่างเดียว เปรียบเทียบสิ่งนี้กับ1.32603ค่าประมาณสำหรับการสแกนบางส่วนที่ได้รับ 339 ครั้งก่อนหน้านี้ (ค่าสแกนโดยประมาณบางส่วนโดยเฉลี่ยต้นทุน = 0.003911592)

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

ฉันไม่คิดว่าการคิดต้นทุนจะปรับตามวิธีการเชิงเส้นนั้น ฉันคิดว่าพวกเขายังรวมองค์ประกอบของค่าใช้จ่ายเริ่มต้นบางอย่าง ลองใช้ค่าต่าง ๆ ของTOPในแบบสอบถามต่อไปนี้

SELECT TOP 147 BrowserID 
FROM [dbo].[tblFEStatsPaperHits] 

147จะช่วยให้ค่าใช้จ่ายในทรีย่อยประมาณใกล้เคียงกับที่0.003911592 0.0039113ไม่ว่าด้วยวิธีใดเป็นที่ชัดเจนว่ามีการคิดต้นทุนตามสมมติฐานที่ว่าการสแกนแต่ละครั้งจะต้องประมวลผลสัดส่วนของตารางเพียงเล็กน้อยตามลำดับของแถวหลายร้อยแทนที่จะเป็นล้าน

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


20

ในหนังสือของฉันแม้แต่หนึ่งสแกนแถว 50M เป็นที่ยอมรับ ... เคล็ดลับปกติของฉันคือการเป็นตัวเป็นตนค่าที่แตกต่างและมอบหมายเครื่องยนต์ที่มีการเก็บรักษาไว้ถึงวันที่:

create view [dbo].[vwFEStatsPaperHitsBrowserID]
with schemabinding
as
select BrowserID, COUNT_BIG(*) as big_count
from [dbo].[tblFEStatsPaperHits]
group by [BrowserID];
go

create unique clustered index [cdxVwFEStatsPaperHitsBrowserID] 
  on [vwFEStatsPaperHitsBrowserID]([BrowserID]);
go

สิ่งนี้จะให้ดัชนีที่เป็นรูปธรรมแก่คุณหนึ่งแถวต่อ BrowserID ซึ่งไม่จำเป็นต้องสแกนแถว 50M เอ็นจิ้นจะเก็บรักษาไว้ให้คุณและ QO จะใช้ 'ตามที่เป็นอยู่' ในคำสั่งที่คุณโพสต์ (โดยไม่มีคำแนะนำหรือเขียนคำสืบค้น)

ข้อเสียคือการต่อสู้ของหลักสูตร การดำเนินการแทรกหรือลบใด ๆ ในtblFEStatsPaperHits(และฉันคิดว่าเป็นตารางการบันทึกที่มีส่วนแทรกขนาดใหญ่) จะต้องทำการเข้าใช้งาน BrowserID ที่เป็นอนุกรม มีหลายวิธีที่ทำให้สามารถใช้งานได้ (การอัปเดตล่าช้า, การบันทึก 2 ขั้นตอนและอื่น ๆ ) หากคุณยินดีที่จะซื้อ


ฉันได้ยินคุณการสแกนใด ๆ ที่มีขนาดใหญ่โดยทั่วไปไม่สามารถยอมรับได้ ในกรณีนี้เป็นการดำเนินการล้างข้อมูลแบบครั้งเดียวดังนั้นฉันจึงเลือกที่จะไม่สร้างดัชนีเพิ่มเติม (และไม่สามารถทำได้ชั่วคราวเนื่องจากจะขัดจังหวะระบบ) ฉันไม่มี EE แต่ระบุว่านี่เป็นเพียงครั้งเดียวคำแนะนำจะไม่เป็นไร ความอยากรู้หลักของฉันคือเมื่อ QO ลุกขึ้นมาพร้อมกับแผนว่า :) ตารางเป็นตารางการบันทึกและมีเม็ดมีดขนาดใหญ่ มีตารางการบันทึกแบบอะซิงโครนัสแยกต่างหากแม้ว่าภายหลังการปรับปรุงแถวใน tblFEStatsPaperHits ดังนั้นฉันสามารถจัดการได้ด้วยตนเองถ้าจำเป็น
Mark S. Rasmussen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.