Quét bất ngờ trong quá trình xóa bằng cách sử dụng WHERE IN


40

Tôi đã có một truy vấn như sau:

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

tblFEStatsBỏ đã có 553 hàng.
tblFEStatsPaperHits đã có 47.974.301 hàng.

tblFEStatsBỏ:

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
)

Có một chỉ mục được nhóm trên tblFEStatsPaperHits không bao gồm BrowserID. Do đó, việc thực hiện truy vấn bên trong sẽ yêu cầu quét toàn bộ bảng tblFEStatsPaperHits - hoàn toàn ổn.

Hiện tại, quá trình quét toàn bộ được thực hiện cho từng hàng trong tblFEStatsBrowsers, nghĩa là tôi đã có 553 lần quét toàn bộ bảng của tblFEStatsPaperHits.

Viết lại thành EXISTS WHERE không thay đổi kế hoạch:

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

Tuy nhiên, như được đề xuất bởi Adam Machanic, việc thêm tùy chọn HASH THAM GIA sẽ dẫn đến kế hoạch thực hiện tối ưu (chỉ một lần quét tblFEStatsPaperHits):

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

Bây giờ đây không phải là câu hỏi về cách khắc phục vấn đề này - tôi có thể sử dụng TÙY CHỌN (HASH THAM GIA) hoặc tạo bảng tạm thời theo cách thủ công. Tôi tự hỏi tại sao trình tối ưu hóa truy vấn sẽ sử dụng gói mà nó hiện đang làm.

Vì QO không có bất kỳ số liệu thống kê nào trên cột BrowserID, tôi đoán rằng nó giả định là tồi tệ nhất - 50 triệu giá trị riêng biệt, do đó đòi hỏi một bàn làm việc trong bộ nhớ / tempdb khá lớn. Như vậy, cách an toàn nhất là thực hiện quét cho từng hàng trong tblFEStatsBrowsers. Không có mối quan hệ khóa ngoài giữa các cột BrowserID trong hai bảng, vì vậy QO không thể khấu trừ bất kỳ thông tin nào từ tblFEStatsBrowsers.

Đây có phải là, đơn giản như nó nghe, lý do?

Cập nhật 1
Để đưa ra một vài số liệu thống kê: TÙY CHỌN (HASH THAM GIA):
208.711 lần đọc logic (12 lần quét)

TÙY CHỌN (LOOP THAM GIA, NHÓM HASH):
11.008.698 lần đọc logic (~ quét trên BrowserID (339))

Không có tùy chọn:
11.008.775 lần đọc logic (~ quét trên BrowserID (339))

Cập nhật 2
câu trả lời tuyệt vời, tất cả các bạn - cảm ơn! Khó khăn để chọn chỉ một. Mặc dù Martin là người đầu tiên và Remus cung cấp một giải pháp tuyệt vời, tôi phải đưa nó cho Kiwi vì đã chú ý đến các chi tiết :)


5
Bạn có thể viết kịch bản thống kê theo thống kê Sao chép từ máy chủ này sang máy chủ khác để chúng tôi có thể sao chép không?
Mark Storey-Smith

2
@ MarkStorey-Smith Chắc chắn - pastebin.com/9HHRPFgK Giả sử bạn chạy tập lệnh trong cơ sở dữ liệu trống, điều này cho phép tôi tạo lại các truy vấn có vấn đề khi bao gồm hiển thị kế hoạch thực hiện. Cả hai truy vấn được bao gồm ở cuối tập lệnh.
Đánh dấu S. Rasmussen

Câu trả lời:


61

"Tôi tự hỏi nhiều hơn tại sao trình tối ưu hóa truy vấn sẽ sử dụng kế hoạch hiện tại."

Nói cách khác, câu hỏi đặt ra là tại sao kế hoạch sau có vẻ rẻ nhất đối với trình tối ưu hóa, so với các lựa chọn thay thế (trong đó có nhiều phương án ).

Kế hoạch ban đầu

Phần bên trong của phép nối về cơ bản là chạy một truy vấn có dạng sau cho mỗi giá trị tương quan của BrowserID:

DECLARE @BrowserID smallint;

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

Quét giấy

Lưu ý rằng số lượng hàng ước tính là 185.220 (không phải là 289.013 ) do việc so sánh đẳng thức hoàn toàn loại trừ NULL(trừ khi ANSI_NULLSOFF). Chi phí ước tính của kế hoạch trên là 206,8 đơn vị.

Bây giờ hãy thêm một TOP (1)mệnh đề:

DECLARE @BrowserID smallint;

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

Với TOP (1)

Chi phí ước tính bây giờ là 0,00452 đơn vị. Việc bổ sung toán tử vật lý hàng đầu đặt mục tiêu hàng là 1 hàng tại toán tử hàng đầu. Sau đó, câu hỏi trở thành cách lấy ra một 'mục tiêu hàng' cho Quét chỉ mục cụm; nghĩa là, quá trình quét cần xử lý bao nhiêu hàng trước khi một hàng khớp với BrowserIDvị từ?

Thông tin thống kê có sẵn cho thấy 166BrowserID giá trị riêng biệt (1 / [Tất cả mật độ] = 1 / 0,006024096 = 166). Chi phí giả định rằng các giá trị riêng biệt được phân phối đồng đều trên các hàng vật lý, do đó, mục tiêu hàng trên Quét chỉ mục cụm được đặt thành 166.302 (chiếm sự thay đổi về số lượng thẻ của bảng kể từ khi thống kê được lấy mẫu).

Chi phí ước tính để quét 166 hàng dự kiến ​​là không lớn (thậm chí được thực hiện 339 lần, một lần cho mỗi thay đổi BrowserID) - Quét chỉ mục cụm cho thấy chi phí ước tính là 1,3219 đơn vị, cho thấy hiệu ứng mở rộng của mục tiêu hàng. Các chi phí điều hành chưa định tỷ lệ cho I / O và CPU được hiển thị như 153,931 , và 52,8698 tương ứng:

Chi phí ước tính theo hàng

Trong thực tế, rất ít khả năng 166 hàng đầu tiên được quét từ chỉ mục (theo bất kỳ thứ tự nào chúng được trả về) sẽ chứa một trong mỗi BrowserIDgiá trị có thể . Tuy nhiên, DELETEkế hoạch được dự trù kinh phí tại 1,40921 đơn vị tổng, và được chọn theo tôi ưu hoa vì lý do đó. Bart Duncan cho thấy một ví dụ khác về loại này trong một bài đăng gần đây có tiêu đề Row Goals Gone Rogue .

Cũng rất thú vị khi lưu ý rằng toán tử Top trong kế hoạch thực hiện không liên quan đến Anti Semi Join (đặc biệt là 'ngắn mạch' Martin đề cập). Chúng ta có thể bắt đầu xem Top đến từ đâu bằng cách vô hiệu hóa quy tắc thăm dò có tên 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 bị vô hiệu hóa

Kế hoạch đó có chi phí ước tính là 364.912 và cho thấy Top đã thay thế một Nhóm theo Tổng hợp (nhóm theo cột tương quan BrowserID). Tổng hợp không phải là do dư thừa DISTINCTtrong văn bản truy vấn: đó là một tối ưu hóa có thể được giới thiệu bởi hai quy tắc thăm dò, LASJNtoLASJNonDistLASJOnLclDist . Vô hiệu hóa cả hai cũng tạo ra kế hoạch này:

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');

Kế hoạch ống chỉ

Kế hoạch đó có chi phí ước tính là 40729,3 đơn vị.

Không có sự chuyển đổi từ Group By thành Top, trình tối ưu hóa 'tự nhiên' chọn một kế hoạch tham gia băm với BrowserIDtổng hợp trước khi chống bán tham gia:

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');

Không có kế hoạch DOP 1 hàng đầu

Và không có hạn chế MAXDOP 1, một kế hoạch song song:

Không có kế hoạch song song hàng đầu

Một cách khác để 'sửa chữa' truy vấn ban đầu sẽ là tạo chỉ mục bị thiếu trên BrowserIDđó báo cáo kế hoạch thực hiện. Các vòng lặp lồng nhau hoạt động tốt nhất với khi bên trong được lập chỉ mục. Ước tính cardinality cho bán tham gia là thách thức ở thời điểm tốt nhất. Không có lập chỉ mục thích hợp (bảng lớn thậm chí không có khóa duy nhất!) Sẽ không giúp ích gì cả.

Tôi đã viết thêm về điều này trong Mục tiêu hàng, Phần 4: Mô hình chống tham gia .


3
Tôi xin chào, bạn vừa giới thiệu cho tôi một số khái niệm mới mà tôi chưa từng gặp trước đây. Chỉ khi bạn cảm thấy bạn biết điều gì đó, một ai đó ngoài kia sẽ đặt bạn xuống - theo cách tốt :) Thêm chỉ số chắc chắn sẽ giúp ích. Tuy nhiên, bên cạnh hoạt động một lần này, trường không bao giờ được truy cập / tổng hợp bởi cột BrowserID và vì vậy tôi muốn lưu các byte đó vì bảng khá lớn (đây chỉ là một trong nhiều cơ sở dữ liệu giống hệt nhau). Không có chìa khóa duy nhất trên bàn vì không có sự độc đáo tự nhiên đối với nó. Tất cả các lựa chọn là bằng PaperID và tùy chọn một khoảng thời gian.
Đánh dấu S. Rasmussen

22

Khi tôi chạy tập lệnh của bạn để tạo cơ sở dữ liệu chỉ thống kê và truy vấn trong câu hỏi tôi nhận được kế hoạch sau.

Kế hoạch

Các Hồng y Bảng được hiển thị trong kế hoạch là

  • tblFEStatsPaperHits: 48063400
  • tblFEStatsBrowsers : 339

Vì vậy, nó ước tính rằng nó sẽ cần phải thực hiện quét trên tblFEStatsPaperHits339 lần. Mỗi lần quét có biến vị ngữ tương quan tblFEStatsBrowsers.BrowserID=tblFEStatsPaperHits.BrowserID AND tblFEStatsPaperHits.BrowserID IS NOT NULLđược đẩy xuống toán tử quét.

Kế hoạch không có nghĩa là sẽ có 339 lần quét đầy đủ. Vì nó nằm dưới toán tử chống bán tham gia ngay khi hàng khớp đầu tiên trên mỗi lần quét được tìm thấy, nó có thể làm chập mạch phần còn lại của nó. Chi phí cây con ước tính cho nút này là 1.32603và toàn bộ kế hoạch được tính theo 1.41337.

Đối với Hash Tham gia, nó đưa ra kế hoạch dưới đây

Hash tham gia

Kế hoạch tổng thể có chi phí 418.415(đắt hơn khoảng 300 lần so với kế hoạch các vòng lặp lồng nhau) với việc quét chỉ mục toàn cụm duy nhất trên tblFEStatsPaperHitschi phí 206.8một mình. So sánh điều này với 1.32603ước tính cho 339 lần quét một phần được đưa ra trước đó (Chi phí ước tính quét một phần trung bình = 0.003911592).

Vì vậy, điều này sẽ chỉ ra rằng chi phí cho mỗi lần quét một phần là rẻ hơn 53.000 lần so với quét toàn bộ. Nếu các chi phí được chia tỷ lệ tuyến tính với số hàng thì điều đó có nghĩa là giả định rằng trung bình chỉ cần xử lý 900 hàng trên mỗi lần lặp trước khi tìm thấy một hàng khớp và có thể ngắn mạch.

Tôi không nghĩ rằng chi phí làm quy mô theo cách tuyến tính đó tuy nhiên. Tôi nghĩ họ cũng kết hợp một số yếu tố của chi phí khởi động cố định. Thử các giá trị khác nhau TOPtrong truy vấn sau

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

147đưa ra chi phí phụ ước tính gần nhất với 0.003911592tại 0.0039113. Dù bằng cách nào thì rõ ràng là dựa trên chi phí giả định rằng mỗi lần quét sẽ chỉ phải xử lý một tỷ lệ nhỏ của bảng, theo thứ tự hàng trăm hàng thay vì hàng triệu.

Tôi không chắc chính xác những gì toán học dựa trên giả định này và nó không thực sự cộng với ước tính số hàng trong phần còn lại của kế hoạch (236 hàng ước tính xuất phát từ các vòng lặp lồng nhau sẽ ngụ ý rằng có 236 trường hợp hoàn toàn không tìm thấy hàng phù hợp và cần phải quét toàn bộ). Tôi cho rằng đây chỉ là một trường hợp trong đó các giả định mô hình được thực hiện giảm xuống một chút và để lại kế hoạch các vòng lặp lồng nhau đáng kể với chi phí.


20

Trong cuốn sách của tôi, ngay cả một lần quét 50 triệu hàng là không thể chấp nhận được ... Thủ thuật thông thường của tôi là cụ thể hóa các giá trị riêng biệt và ủy thác cho động cơ luôn cập nhật:

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

Điều này cung cấp cho bạn một chỉ mục cụ thể hóa một hàng trên mỗi BrowserID, loại bỏ nhu cầu quét các hàng 50M. Công cụ sẽ duy trì nó cho bạn và QO sẽ sử dụng nó 'như hiện trạng' trong tuyên bố bạn đã đăng (không có bất kỳ gợi ý hoặc viết lại truy vấn nào).

Nhược điểm của khóa học là tất nhiên. Bất kỳ thao tác chèn hoặc xóa nào trong tblFEStatsPaperHits(và tôi đoán là bảng ghi nhật ký có chèn nặng) sẽ phải tuần tự hóa quyền truy cập vào BrowserID đã cho. Có nhiều cách làm cho điều này khả thi (cập nhật chậm, đăng nhập theo giai đoạn, v.v.) nếu bạn sẵn sàng mua nó.


Tôi nghe thấy bạn, bất kỳ quét lớn như vậy chắc chắn là không thể chấp nhận được. Trong trường hợp này, đối với một số hoạt động dọn dẹp dữ liệu một lần, vì vậy tôi chọn không tạo các chỉ mục bổ sung (và không thể làm như vậy tạm thời vì nó làm gián đoạn hệ thống). Tôi không có EE nhưng cho rằng đây là một lần, gợi ý sẽ ổn. Sự tò mò chính của tôi là về cách QO đã lên kế hoạch mặc dù :) Bảng là một bảng ghi nhật ký và có những phần chèn nặng. Có một bảng ghi nhật ký không đồng bộ riêng biệt mặc dù sau đó cập nhật các hàng trong tblFEStatsPaperHits để tôi có thể tự quản lý nó, nếu cần.
Đánh dấu S. Rasmussen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.