Làm thế nào để kiểm tra hiệu quả EXISTS trên nhiều cột?


26

Đây là một vấn đề tôi đưa ra chống lại định kỳ và chưa tìm thấy một giải pháp tốt cho.

Giả sử cấu trúc bảng sau

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

và yêu cầu là xác định xem một trong hai cột có thể rỗng Bhay Cthực sự có chứa bất kỳ NULLgiá trị nào không (và nếu đó là một (các)).

Cũng giả sử bảng chứa hàng triệu hàng (và không có số liệu thống kê cột nào có thể được xem qua vì tôi quan tâm đến một giải pháp chung hơn cho lớp truy vấn này).

Tôi có thể nghĩ ra một vài cách tiếp cận điều này nhưng tất cả đều có điểm yếu.

Hai EXISTStuyên bố riêng biệt . Điều này sẽ có lợi thế là cho phép các truy vấn dừng quét sớm ngay khi NULLtìm thấy. Nhưng nếu cả hai cột trong thực tế không chứa NULLs thì sẽ có hai lần quét đầy đủ.

Truy vấn tổng hợp đơn

SELECT 
    MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T

Điều này có thể xử lý cả hai cột cùng một lúc để có trường hợp xấu nhất là quét toàn bộ. Nhược điểm là ngay cả khi nó gặp một NULLtrong cả hai cột từ rất sớm trên truy vấn vẫn sẽ quét toàn bộ phần còn lại của bảng.

Biến người dùng

Tôi có thể nghĩ ra cách thứ ba để làm việc này

BEGIN TRY
DECLARE @B INT, @C INT, @D INT

SELECT 
    @B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
    @C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
    /*Divide by zero error if both @B and @C are 1.
    Might happen next row as no guarantee of order of
    assignments*/
    @D = 1 / (2 - (@B + @C))
FROM T  
OPTION (MAXDOP 1)       
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
    BEGIN
    SELECT 'B,C both contain NULLs'
    RETURN;
    END
ELSE
    RETURN;
END CATCH

SELECT ISNULL(@B,0),
       ISNULL(@C,0)

nhưng điều này không phù hợp với mã sản xuất vì hành vi chính xác cho truy vấn nối ghép tổng hợp không được xác định. và chấm dứt quá trình quét bằng cách ném một lỗi là một giải pháp khá kinh khủng.

Có lựa chọn nào khác kết hợp các điểm mạnh của các phương pháp trên không?

Chỉnh sửa

Chỉ cần cập nhật kết quả này với kết quả tôi nhận được về các lần đọc cho các câu trả lời được gửi cho đến nay (sử dụng dữ liệu kiểm tra của @ ypercube)

+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          | 2 * EXISTS | CASE | Kejser  |  Kejser  |        Kejser        | ypercube |       8kb        |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
|          |            |      |         | MAXDOP 1 | HASH GROUP, MAXDOP 1 |          |                  |
| No Nulls |      15208 | 7604 |    8343 | 7604     | 7604                 |    15208 | 8346 (8343+3)    |
| One Null |       7613 | 7604 |    8343 | 7604     | 7604                 |     7620 | 7630 (25+7602+3) |
| Two Null |         23 | 7604 |    8343 | 7604     | 7604                 |       30 | 30 (18+12)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Đối với câu trả lời @ Thomas của tôi đã thay đổi TOP 3để TOP 2cho có khả năng cho phép nó để thoát trước đó. Tôi đã có một kế hoạch song song theo mặc định cho câu trả lời đó vì vậy cũng đã thử nó với một MAXDOP 1gợi ý để làm cho số lần đọc tương đương với các kế hoạch khác. Tôi hơi ngạc nhiên với kết quả như trong thử nghiệm trước đây của tôi, tôi đã thấy truy vấn đó ngắn mạch mà không đọc toàn bộ bảng.

Kế hoạch cho dữ liệu thử nghiệm của tôi rằng ngắn mạch dưới đây

Ngắn mạch

Kế hoạch cho dữ liệu của ypercube là

Không phải Shortcircuit

Vì vậy, nó thêm một toán tử sắp xếp chặn vào kế hoạch. Tôi cũng đã thử với HASH GROUPgợi ý nhưng cuối cùng vẫn đọc được tất cả các hàng

Không phải Shortcircuit

Vì vậy, chìa khóa dường như là để có được một hash match (flow distinct)nhà điều hành cho phép kế hoạch này bị đoản mạch vì các giải pháp thay thế khác sẽ chặn và tiêu thụ tất cả các hàng. Tôi không nghĩ có gợi ý để ép buộc điều này một cách cụ thể nhưng rõ ràng "nói chung, trình tối ưu hóa chọn Dòng phân biệt trong đó xác định rằng cần ít hàng đầu ra hơn so với có các giá trị riêng biệt trong bộ đầu vào." .

Dữ liệu của @ ypercube chỉ có 1 hàng trong mỗi cột với NULLcác giá trị (cardinality = 30300) và các hàng ước tính đi vào và ra khỏi toán tử là cả hai 1. Bằng cách làm cho vị từ mờ hơn một chút đối với trình tối ưu hóa, nó tạo ra một kế hoạch với toán tử Flow Distinc.

SELECT TOP 2 *
FROM (SELECT DISTINCT 
        CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT 

Chỉnh sửa 2

Một điều chỉnh cuối cùng xảy ra với tôi là truy vấn ở trên vẫn có thể xử lý nhiều hàng hơn mức cần thiết trong trường hợp hàng đầu tiên mà nó gặp phải NULLcó NULL trong cả hai cột BC. Nó sẽ tiếp tục quét chứ không thoát ra ngay lập tức. Một cách để tránh điều này sẽ là hủy bỏ các hàng khi chúng được quét. Vì vậy, sửa đổi cuối cùng của tôi cho câu trả lời của Thomas Kejers bên dưới

SELECT DISTINCT TOP 2 NullExists
FROM test T 
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
                   (CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL

Có lẽ sẽ tốt hơn cho vị từ WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLnhưng ngược lại với dữ liệu thử nghiệm trước đó mà người ta không đưa ra cho tôi một kế hoạch với Dòng chảy riêng biệt, trong khi đó thì NullExists IS NOT NULLcó (kế hoạch bên dưới).

Không có trục

Câu trả lời:


20

Làm thế nào về:

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS B
        , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS C
  FROM T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT

Tôi thích cách tiếp cận này. Có một vài vấn đề có thể xảy ra mà tôi giải quyết trong các chỉnh sửa cho câu hỏi của mình. Theo văn bản TOP 3chỉ có thể là TOP 2hiện vì nó sẽ quét cho đến khi nó tìm thấy một trong mỗi điều sau đây (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). Bất kỳ 2 trong số 3 sẽ là đủ - và nếu nó tìm thấy (NULL,NULL)đầu tiên thì thứ hai cũng sẽ không cần thiết. Ngoài ra, để đoản mạch, kế hoạch sẽ cần triển khai sự khác biệt thông qua một hash match (flow distinct)nhà điều hành thay vì hash match (aggregate)hoặcdistinct sort
Martin Smith

6

Theo tôi hiểu câu hỏi, bạn muốn biết liệu null có tồn tại trong bất kỳ giá trị cột nào không so với thực tế trả về các hàng trong đó B hoặc C là null. Nếu đó là trường hợp, tại sao không:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

Trên giàn thử nghiệm của tôi với SQL 2008 R2 và một triệu hàng, tôi đã nhận được các kết quả sau trong ms từ tab Thống kê khách hàng:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Nếu bạn thêm gợi ý nolock, kết quả thậm chí còn nhanh hơn:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Để tham khảo, tôi đã sử dụng Trình tạo SQL của Red-gate để tạo dữ liệu. Trong số một triệu hàng của tôi, 9,886 hàng có giá trị B không và 10.019 có giá trị C không.

Trong loạt thử nghiệm này, mỗi hàng trong cột B có một giá trị:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Trước mỗi bài kiểm tra (cả hai bộ) tôi đã chạy CHECKPOINTDBCC DROPCLEANBUFFERS.

Dưới đây là kết quả khi không có null trong bảng. Lưu ý rằng 2 giải pháp tồn tại được cung cấp bởi ypercube gần giống với tôi về mặt đọc và thời gian thực hiện. Tôi (chúng tôi) tin rằng điều này là do những lợi thế của phiên bản Enterprise / Developer có sử dụng Quét nâng cao . Nếu bạn chỉ sử dụng phiên bản Tiêu chuẩn hoặc thấp hơn, giải pháp của Kejers rất có thể là giải pháp nhanh nhất.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278

4

Được IFtuyên bố cho phép?

Điều này sẽ cho phép bạn xác nhận sự tồn tại của B hoặc C trên một lần đi qua bảng:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      

4

Đã thử nghiệm trong SQL-Fiddle trong các phiên bản: 2008 r22012 với 30K hàng.

  • Các EXISTStruy vấn hiển thị một lợi ích rất lớn trong hiệu quả khi nó tìm thấy Nulls đầu - đó là mong đợi.
  • Tôi có hiệu suất tốt hơn với EXISTStruy vấn - trong mọi trường hợp vào năm 2012, điều mà tôi không thể giải thích.
  • Trong 2008R2, khi không có Null, nó chậm hơn 2 truy vấn khác. Càng tìm thấy Null càng sớm, tốc độ nhận được càng nhanh và khi cả hai cột đều có giá trị sớm, thì nó nhanh hơn nhiều so với 2 truy vấn khác.
  • Truy vấn của Thomas Kejers dường như hoạt động nhẹ nhưng liên tục tốt hơn vào năm 2012 và tồi tệ hơn trong năm 2008R2, so với CASEtruy vấn của Martin .
  • Phiên bản 2012 dường như có hiệu suất tốt hơn nhiều. Nó có thể phải thực hiện với các cài đặt của máy chủ SQL-Fiddle và không chỉ với các cải tiến về trình tối ưu hóa.

Truy vấn và thời gian. Thời gian thực hiện:

  • Đầu tiên không có Nulls
  • Thứ 2 với cột Bcó một NULLcái nhỏ id.
  • 3nd với cả hai cột có một cột NULLở id nhỏ.

Ở đây chúng tôi đi (có một vấn đề với các kế hoạch, tôi sẽ thử lại sau. Thực hiện theo các liên kết bây giờ):


Truy vấn với 2 truy vấn con EXISTS

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Truy vấn tổng hợp đơn của Martin Smith

SELECT 
    MAX(CASE WHEN b IS NULL THEN 1 ELSE 0 END) AS B,
    MAX(CASE WHEN c IS NULL THEN 1 ELSE 0 END) AS C
FROM test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Truy vấn của Thomas Kejers

SELECT TOP 3 *
FROM (SELECT DISTINCT 
        CASE WHEN B IS NULL THEN NULL ELSE 'foo' END AS b
      , CASE WHEN C IS NULL THEN NULL ELSE 'bar' END AS c
  FROM test T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Đề nghị của tôi (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Nó cần một số đánh bóng trên đầu ra nhưng hiệu quả tương tự như EXISTStruy vấn. Tôi nghĩ rằng nó sẽ tốt hơn khi không có null nhưng thử nghiệm cho thấy nó không phải.


Gợi ý (2)

Cố gắng đơn giản hóa logic:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Nó dường như hoạt động tốt hơn trong năm 2008R2 so với đề xuất trước đó nhưng tồi tệ hơn vào năm 2012 (có lẽ lần thứ 2 INSERTcó thể được viết lại bằng cách sử dụng IF, như câu trả lời của @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29

0

Khi bạn sử dụng EXISTS, SQL Server biết bạn đang thực hiện kiểm tra sự tồn tại. Khi tìm thấy giá trị khớp đầu tiên, nó trả về TRUE và dừng tìm kiếm.

khi bạn kết nối 2 cột và nếu có thì kết quả sẽ là null

ví dụ

null + 'a' = null

vì vậy hãy kiểm tra mã này

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null

-3

Làm thế nào về:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Nếu điều này hoạt động (tôi chưa thử nghiệm), nó sẽ mang lại một bảng một hàng với 2 cột, mỗi cột là TRUE hoặc FALSE. Tôi đã không kiểm tra hiệu quả.


2
Ngay cả khi điều này là hợp lệ trong bất kỳ DBMS nào khác, tôi nghi ngờ nó có ngữ nghĩa chính xác. Giả sử rằng T.B is nullđược coi là kết quả boolean sau đó EXISTS(SELECT true)EXISTS(SELECT false)cả hai sẽ trả về đúng. Ví dụ về MySQL này chỉ ra rằng cả hai cột đều chứa NULL khi thực tế không có
Martin Smith
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.