Tại sao truy vấn CHỌN DISTINCT TOP N của tôi quét toàn bộ bảng?


27

Tôi đã chạy vào một vài SELECT DISTINCT TOP Ntruy vấn dường như được tối ưu hóa kém bởi trình tối ưu hóa truy vấn SQL Server. Hãy bắt đầu bằng cách xem xét một ví dụ tầm thường: một bảng hàng triệu với hai giá trị xen kẽ. Tôi sẽ sử dụng hàm GetNums để tạo dữ liệu:

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;

Đối với truy vấn sau:

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

SQL Server có thể tìm thấy hai giá trị riêng biệt chỉ bằng cách quét trang dữ liệu đầu tiên của bảng nhưng thay vào đó, nó quét tất cả dữ liệu . Tại sao SQL Server không quét cho đến khi tìm thấy số lượng giá trị riêng biệt được yêu cầu?

Đối với câu hỏi này, vui lòng sử dụng dữ liệu thử nghiệm sau có chứa 10 triệu hàng với 10 giá trị riêng biệt được tạo trong các khối:

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;

Câu trả lời cho một bảng có chỉ mục được nhóm cũng được chấp nhận:

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;

Truy vấn sau đây quét tất cả 10 triệu hàng từ bảng . Làm thế nào tôi có thể nhận được một cái gì đó không quét toàn bộ bảng? Tôi đang sử dụng SQL Server 2016 SP1.

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

Một con trỏ thậm chí có thể hoạt động trong 10
paparazzo

Câu trả lời:


29

Có ba quy tắc tối ưu hóa khác nhau có thể thực hiện DISTINCTthao tác trong truy vấn trên. Truy vấn sau đây đưa ra một lỗi cho thấy danh sách này là đầy đủ:

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

Msg 8622, Cấp 16, Bang 1, Dòng 1

Bộ xử lý truy vấn không thể tạo ra một kế hoạch truy vấn vì các gợi ý được xác định trong truy vấn này. Gửi lại truy vấn mà không chỉ định bất kỳ gợi ý nào và không sử dụng SET FORCEPLAN.

GbAggToSortthực hiện tổng hợp theo nhóm (riêng biệt) như một loại phân biệt. Đây là một toán tử chặn sẽ đọc tất cả dữ liệu từ đầu vào trước khi tạo bất kỳ hàng nào. GbAggToStrmthực hiện tổng hợp theo nhóm dưới dạng tổng hợp luồng (cũng yêu cầu sắp xếp đầu vào trong trường hợp này). Đây cũng là một toán tử chặn. GbAggToHSthực hiện dưới dạng khớp băm, đó là những gì chúng ta đã thấy trong kế hoạch xấu từ câu hỏi, nhưng nó có thể được thực hiện dưới dạng khớp băm (tổng hợp) hoặc khớp băm (phân biệt luồng).

Toán tử băm khớp ( dòng phân biệt ) là một cách để giải quyết vấn đề này bởi vì nó không bị chặn. SQL Server sẽ có thể dừng quét khi tìm thấy đủ các giá trị riêng biệt.

Toán tử lôgic Flow Distinc quét đầu vào, loại bỏ các bản sao. Trong khi toán tử Phân biệt tiêu thụ tất cả đầu vào trước khi tạo ra bất kỳ đầu ra nào, toán tử Flow Distinc trả về mỗi hàng khi nó được lấy từ đầu vào (trừ khi hàng đó là một bản sao, trong trường hợp đó là loại bỏ).

Tại sao truy vấn trong câu hỏi sử dụng khớp băm (tổng hợp) thay vì khớp băm (phân biệt luồng)? Vì số lượng giá trị riêng biệt thay đổi trong bảng, tôi sẽ hy vọng chi phí của truy vấn khớp băm (phân biệt luồng) sẽ giảm vì ước tính số lượng hàng cần quét vào bảng sẽ giảm. Tôi hy vọng chi phí của kế hoạch băm (tổng hợp) sẽ tăng lên vì bảng băm mà nó cần xây dựng sẽ lớn hơn. Một cách để điều tra điều này là bằng cách tạo một hướng dẫn kế hoạch . Nếu tôi tạo hai bản sao của dữ liệu nhưng áp dụng hướng dẫn kế hoạch cho một trong số chúng, tôi sẽ có thể so sánh khớp băm (tổng hợp) với khớp băm (khác biệt) cạnh nhau với cùng một dữ liệu. Lưu ý rằng tôi không thể làm điều này bằng cách vô hiệu hóa các quy tắc tối ưu hóa truy vấn vì cùng một quy tắc áp dụng cho cả hai gói ( GbAggToHS).

Đây là một cách để có được hướng dẫn kế hoạch mà tôi đang theo đuổi:

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)

Nhận xử lý kế hoạch và sử dụng nó để tạo một hướng dẫn kế hoạch:

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

Hướng dẫn kế hoạch chỉ hoạt động trên văn bản truy vấn chính xác, vì vậy hãy sao chép lại từ hướng dẫn kế hoạch:

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

Đặt lại dữ liệu:

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

Nhận một kế hoạch truy vấn cho truy vấn với hướng dẫn kế hoạch được áp dụng:

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

Điều này có toán tử băm khớp (dòng phân biệt) mà chúng tôi muốn với dữ liệu thử nghiệm của chúng tôi. Lưu ý rằng SQL Server dự kiến ​​sẽ đọc tất cả các hàng từ bảng và chi phí ước tính là chính xác tương tự như đối với gói có khớp băm (tổng hợp). Thử nghiệm mà tôi đã đề xuất rằng chi phí cho hai kế hoạch là giống nhau khi mục tiêu hàng cho kế hoạch lớn hơn hoặc bằng số lượng giá trị riêng biệt mà SQL Server mong đợi từ bảng, trong trường hợp này có thể được lấy từ đơn giản số liệu thống kê. Thật không may (đối với truy vấn của chúng tôi) trình tối ưu hóa chọn kết hợp băm (tổng hợp) so với kết hợp băm (phân biệt luồng) khi chi phí là như nhau. Vì vậy, chúng tôi 0,00001 đơn vị tối ưu hóa ma thuật ra khỏi kế hoạch mà chúng tôi muốn.

Một cách để tấn công vấn đề này là bằng cách giảm mục tiêu hàng. Nếu mục tiêu hàng từ điểm của chế độ xem là trình tối ưu hóa nhỏ hơn số lượng hàng khác biệt, có thể chúng tôi sẽ nhận được kết quả khớp băm (phân biệt luồng). Điều này có thể được thực hiện vớiOPTIMIZE FOR gợi ý truy vấn:

DECLARE @j INT = 10;

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

Đối với truy vấn này, trình tối ưu hóa tạo ra một kế hoạch như thể truy vấn chỉ cần hàng đầu tiên nhưng khi truy vấn được thực thi, nó sẽ quay trở lại 10 hàng đầu tiên. Trên máy của tôi, truy vấn này quét 892800 hàng từX_10_DISTINCT_HEAP và hoàn thành trong 299 ms với 250 ms thời gian CPU và 2537 lần đọc logic.

Lưu ý rằng kỹ thuật này sẽ không hoạt động nếu báo cáo thống kê chỉ có một giá trị riêng biệt, điều này có thể xảy ra đối với thống kê được lấy mẫu đối với dữ liệu sai lệch. Tuy nhiên, trong trường hợp đó, không chắc là dữ liệu của bạn được đóng gói đủ dày để biện minh cho việc sử dụng các kỹ thuật như thế này. Bạn có thể không mất nhiều bằng cách quét tất cả dữ liệu trong bảng, đặc biệt nếu điều đó có thể được thực hiện song song.

Một cách khác để tấn công vấn đề này là bằng cách thổi phồng số lượng giá trị khác biệt ước tính mà SQL Server mong đợi nhận được từ bảng cơ sở. Điều này khó hơn dự kiến. Áp dụng một hàm xác định có thể có thể làm tăng số lượng kết quả khác biệt. Nếu trình tối ưu hóa truy vấn nhận thức được thực tế toán học đó (một số thử nghiệm cho thấy ít nhất là cho mục đích của chúng tôi) thì việc áp dụng các hàm xác định ( bao gồm tất cả các hàm chuỗi ) sẽ không làm tăng số lượng hàng khác nhau ước tính.

Nhiều chức năng không xác định cũng không hoạt động, bao gồm cả các lựa chọn rõ ràng NEWID()RAND(). Tuy nhiên, LAG()không có mẹo cho truy vấn này. Trình tối ưu hóa truy vấn mong đợi 10 triệu giá trị khác biệt so với LAGbiểu thức sẽ khuyến khích gói băm (phân biệt luồng) :

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

Trên máy của tôi, truy vấn này quét 892800 hàng từ X_10_DISTINCT_HEAPvà hoàn thành trong 1165 ms với 1109 ms thời gian CPU và 2537 lần đọc logic, do đó, LAG()thêm khá nhiều chi phí tương đối. @Paul White đề nghị thử xử lý chế độ hàng loạt cho truy vấn này. Trên SQL Server 2016, chúng tôi có thể xử lý chế độ hàng loạt ngay cả với MAXDOP 1. Một cách để có được xử lý chế độ hàng loạt cho bảng lưu trữ hàng là tham gia vào CCI trống như sau:

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

Mã đó dẫn đến kế hoạch truy vấn này .

Paul chỉ ra rằng tôi phải thay đổi truy vấn để sử dụng LAG(..., 1)LAG(..., 0)dường như không đủ điều kiện để tối ưu hóa Tổng hợp cửa sổ. Thay đổi này đã giảm thời gian trôi qua xuống còn 520 ms và thời gian CPU xuống còn 454 ms.

Lưu ý rằng LAG()cách tiếp cận không phải là cách ổn định nhất. Nếu Microsoft thay đổi giả định duy nhất đối với chức năng thì nó có thể không còn hoạt động. Nó có một ước tính khác với di sản CE. Ngoài ra loại tối ưu hóa này chống lại một đống không phải là một ý tưởng tốt. Nếu bảng được xây dựng lại, có thể kết thúc trong trường hợp xấu nhất trong đó hầu hết các hàng cần phải được đọc từ bảng.

Đối với một bảng có một cột duy nhất (chẳng hạn như ví dụ chỉ mục được nhóm trong câu hỏi), chúng ta có các tùy chọn tốt hơn. Ví dụ: chúng ta có thể lừa trình tối ưu hóa bằng cách sử dụng SUBSTRINGbiểu thức luôn trả về một chuỗi rỗng. SQL Server không nghĩ rằng SUBSTRINGsẽ thay đổi số lượng giá trị riêng biệt vì vậy nếu chúng tôi áp dụng nó cho một cột duy nhất, chẳng hạn như PK, thì số lượng hàng khác nhau ước tính là 10 triệu. Truy vấn sau đây có được toán tử băm khớp (dòng phân biệt):

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

Trên máy của tôi, truy vấn này quét 900000 hàng từ X_10_DISTINCT_CIvà hoàn thành trong 333 ms với 297 ms thời gian CPU và 3011 lần đọc logic.

Tóm lại, trình tối ưu hóa truy vấn dường như cho rằng tất cả các hàng sẽ được đọc từ bảng cho SELECT DISTINCT TOP Ncác truy vấn khi N> = số lượng các hàng riêng biệt ước tính từ bảng. Toán tử băm khớp (tổng hợp) có thể có cùng chi phí với toán tử khớp băm (phân biệt luồng) nhưng trình tối ưu hóa luôn chọn toán tử tổng hợp. Điều này có thể dẫn đến việc đọc logic không cần thiết khi đủ các giá trị khác biệt được đặt gần khi bắt đầu quét bảng. Hai cách để lừa trình tối ưu hóa bằng cách sử dụng toán tử băm khớp (dòng phân biệt) là hạ thấp mục tiêu hàng bằng cách sử dụng OPTIMIZE FORgợi ý hoặc để tăng số lượng hàng khác nhau ước tính bằng cách sử dụng LAG()hoặc SUBSTRINGtrên một cột duy nhất.


12

Bạn đã trả lời chính xác câu hỏi của bạn.

Tôi chỉ muốn thêm một quan sát rằng cách hiệu quả nhất thực sự là quét toàn bộ bảng - nếu nó có thể được tổ chức dưới dạng một cột 'heap' :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

Truy vấn đơn giản:

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

sau đó đưa ra:

Kế hoạch thực hiện

Bảng 'X_10_DISTINCT_HEAP'. Quét số 1,
 logic đọc 0, đọc vật lý 0, đọc trước đọc 0, 
 lob logic đọc 66 , lob vật lý đọc 0, lob đọc trước đọc 0.
Bảng 'X_10_DISTINCT_HEAP'. Phân đoạn đọc 13, phân đoạn bỏ qua 0.

 Thời gian thực thi máy chủ SQL:
   Thời gian CPU = 0 ms, thời gian trôi qua = 11 ms.

Hash Match (Flow Distinc) hiện không thể thực thi trong chế độ hàng loạt. Các phương pháp sử dụng phương pháp này chậm hơn nhiều do quá trình chuyển đổi đắt tiền (vô hình) từ xử lý hàng loạt sang xử lý hàng. Ví dụ:

SET ROWCOUNT 10;

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

SET ROWCOUNT 0;

Cung cấp:

Kế hoạch thực hiện dòng chảy

Bảng 'X_10_DISTINCT_HEAP'. Quét số 1,
 logic đọc 0, đọc vật lý 0, đọc trước đọc 0, 
 lob logic đọc 20 , lob vật lý đọc 0, lob đọc trước đọc 0.
Bảng 'X_10_DISTINCT_HEAP'. Phân đoạn đọc 4 , phân đoạn bỏ qua 0.

 Thời gian thực thi máy chủ SQL:
   Thời gian CPU = 640 ms, thời gian trôi qua = 680 ms.

Điều này chậm hơn so với khi bảng được tổ chức như một đống hàng.


4

Đây là một nỗ lực để mô phỏng quét một phần lặp lại (tương tự nhưng không giống như quét bỏ qua) bằng cách sử dụng CTE đệ quy. Mục đích - vì chúng tôi không có chỉ mục trên (id)- là để tránh các loại và nhiều lần quét trên bàn.

Nó thực hiện một số thủ thuật để bỏ qua một số hạn chế CTE đệ quy:

  • Không TOPđược phép trong phần đệ quy. Chúng tôi sử dụng một truy vấn con và ROW_NUMBER()thay vào đó.
  • Chúng ta không thể có nhiều tham chiếu đến phần không đổi hoặc sử dụng LEFT JOINhoặc sử dụng NOT IN (SELECT id FROM cte)từ phần đệ quy. Để bỏ qua, chúng tôi xây dựng một VARCHARchuỗi tích lũy tất cả các idgiá trị, tương tự STRING_AGGhoặc với phân cấpID và sau đó so sánh với LIKE.

Đối với Heap (giả sử cột được đặt tên id) test-1 trên rextester.com .

Điều này - như các thử nghiệm đã chỉ ra - không tránh được nhiều lần quét nhưng hoạt động tốt khi các giá trị khác nhau được tìm thấy trong một vài trang đầu tiên. Tuy nhiên, nếu các giá trị không được phân phối đồng đều, nó có thể thực hiện nhiều lần quét trên các phần lớn của bảng - điều mà khóa học pf dẫn đến hiệu suất kém.

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 ;

và khi bảng được nhóm (CI bật unique_key), test-2 trên rextester.com .

Điều này sử dụng chỉ mục được nhóm ( WHERE x.unique_key > ct.unique_key) để tránh nhiều lần quét:

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 ;

Có một vấn đề hiệu suất khá tinh tế với giải pháp này. Nó kết thúc bằng cách tìm kiếm thêm trên bàn sau khi tìm thấy giá trị Nth. Vì vậy, nếu có 10 giá trị riêng biệt cho top 10, nó sẽ tìm giá trị thứ 11 không có ở đó. Bạn kết thúc với một lần quét đầy đủ bổ sung và 10 triệu phép tính ROW_NUMBER () thực sự cộng lại. Tôi có một cách giải quyết ở đây giúp tăng tốc truy vấn 20X trên máy của tôi. Bạn nghĩ sao? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish

2

Để hoàn thiện, một cách khác để tiếp cận vấn đề này là sử dụng OUTER ỨNG DỤNG . Chúng ta có thể thêm một OUTER APPLYtoán tử cho mỗi giá trị riêng biệt mà chúng ta cần tìm. Khái niệm này tương tự như cách tiếp cận đệ quy của ypercube, nhưng thực sự có đệ quy được viết bằng tay. Một lợi thế là chúng ta có thể sử dụng TOPtrong các bảng dẫn xuất thay vì ROW_NUMBER()cách giải quyết. Một nhược điểm lớn là văn bản truy vấn sẽ dài hơn khi Ntăng.

Đây là một triển khai cho truy vấn chống lại heap:

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;

Đây là kế hoạch truy vấn thực tế cho truy vấn trên. Trên máy của tôi, truy vấn này hoàn thành trong 713 ms với 625 ms thời gian CPU và 12605 lần đọc logic. Chúng tôi nhận được một giá trị khác biệt mới cho mỗi 100k hàng, vì vậy tôi mong muốn truy vấn này sẽ quét khoảng 900000 * 10 * 0,5 = 4500000 hàng. Về lý thuyết, truy vấn này sẽ thực hiện năm lần đọc logic của truy vấn này từ câu trả lời khác:

DECLARE @j INT = 10;

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

Truy vấn đó đã đọc 2537 logic. 2537 * 5 = 12685, khá gần với 12605.

Đối với bảng có chỉ số cụm chúng ta có thể làm tốt hơn. Điều này là do chúng ta có thể chuyển giá trị khóa được nhóm cuối cùng vào bảng dẫn xuất để tránh quét cùng một hàng hai lần. Một cách thực hiệ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 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;

Đây là kế hoạch truy vấn thực tế cho truy vấn trên. Trên máy của tôi, truy vấn này hoàn thành trong 154 ms với 140 ms thời gian CPU và 3203 lần đọc logic. Điều này dường như chạy nhanh hơn một chút so với OPTIMIZE FORtruy vấn đối với bảng chỉ mục được nhóm. Tôi không mong đợi điều đó nên tôi đã cố gắng đo hiệu suất cẩn thận hơn. Phương pháp của tôi là chạy mỗi truy vấn mười lần mà không có tập kết quả và xem xét các số tổng hợp từ sys.dm_exec_sessionssys.dm_exec_session_wait_stats. Phiên 56 là APPLYtruy vấn và phiên 63 là OPTIMIZE FORtruy vấn.

Đầu ra của sys.dm_exec_sessions:

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

Dường như có một lợi thế rõ ràng trong cpu_time và elapsed_time cho APPLYtruy vấn.

Đầu ra của 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 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

Các OPTIMIZE FORtruy vấn có một loại chờ đợi thêm, RESERVED_MEMORY_ALLOCATION_EXT . Tôi không biết chính xác điều này có nghĩa là gì. Nó có thể chỉ là một phép đo chi phí trong toán tử băm khớp (dòng phân biệt). Trong mọi trường hợp, có lẽ không đáng lo ngại về sự khác biệt 70 ms về thời gian CPU.


1

Tôi nghĩ rằng bạn đã trả lời về lý do tại sao
Đây có thể là một cách để giải quyết nó
Tôi biết nó có vẻ lộn xộn nhưng kế hoạch thực hiện cho biết top 2 khác biệt là 84% chi phí

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;

Mã này mất 5 giây trên máy của tôi. Có vẻ như các phép nối với biến bảng thêm khá nhiều chi phí. Trong truy vấn cuối cùng, biến bảng được quét 892800 lần. Truy vấn đó mất 1359 ms thời gian CPU và 1374 ms thời gian trôi qua. Chắc chắn nhiều hơn tôi mong đợi. Thêm khóa chính vào biến bảng có vẻ hữu ích, mặc dù tôi không chắc tại sao. Có thể có tối ưu hóa khác có thể.
Joe Obbish

-4

Tôi nghĩ bạn cần đứng lại và nhìn vào câu hỏi của bạn một cách khách quan, để hiểu những gì bạn đang thấy.

Làm cách nào để trình tối ưu hóa truy vấn có thể chọn 10 giá trị riêng biệt hàng đầu mà không cần xác định danh sách đầy đủ các giá trị riêng biệt trước?

Chọn Phân biệt yêu cầu quét toàn bộ bảng (hoặc chỉ mục che) để xác định tập kết quả của nó. Hãy suy nghĩ về nó - hàng cuối cùng trong bảng có thể chứa một giá trị mà nó chưa từng thấy trước đây.

Chọn Phân biệt là một vũ khí rất cùn.


2
Không hẳn vậy. Nếu tôi quét một bảng và 20 hàng đầu tiên có 10 giá trị riêng biệt, tại sao tôi cần tiếp tục quét phần còn lại của bảng?
ypercubeᵀᴹ

2
Tại sao nó phải tiếp tục tìm kiếm khi tôi chỉ yêu cầu 10? Nó đã tìm thấy 10 giá trị riêng biệt, nó nên dừng lại. Đó là vấn đề của câu hỏi.
ypercubeᵀᴹ

3
Tại sao một tìm kiếm N hàng đầu cần phải xem toàn bộ kết quả đầu tiên? Nếu nó có 10 giá trị riêng biệt và đó là tất cả những gì bạn quan tâm, nó có thể ngừng tìm kiếm các giá trị khác. Nếu nó phải sắp xếp toàn bộ tập kết quả để biết "10" đầu tiên đó là một câu chuyện khác, nhưng nếu bạn chỉ muốn 10 giá trị riêng biệt mà không quan tâm đến 10 thì không có yêu cầu logic nào để có được toàn bộ tập kết quả.
Tom V - Đội Monica

2
Chỉ cần tưởng tượng mình có nhiệm vụ trả lại bộ yêu cầu. Bạn được yêu cầu đưa ra mười giá trị hàng đầu khác biệt trong số hàng chục triệu và không được hướng dẫn tuân theo bất kỳ thứ tự sắp xếp nào. Bạn có cảm thấy mình bắt buộc phải trải qua toàn bộ tập hợp các giá trị nếu bạn đạt được kết quả sau khi nhìn vào, nói, 100 trong số chúng đầu tiên không? Điều đó sẽ là vô nghĩa. Làm thế nào để triển khai logic đó trong một sản phẩm cơ sở dữ liệu là một vấn đề khác, nhưng dường như bạn đang đề xuất rằng cần phải quét toàn bộ bảng cho vấn đề này, điều đó không phải là logic .
Andriy M

4
@Marco: Tôi không đồng ý, đây câu trả lời. Nó chỉ xảy ra đến nỗi người trả lời không đồng ý với tiền đề của câu hỏi và trả lời những gì anh ấy / cô ấy cho là quan niệm sai lầm của OP.
Andriy M
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.