thứ tự các mệnh đề trong phiên bản EXISTS (tầm) HOẶC EXISTS (tầm)


11

Tôi có một lớp các truy vấn kiểm tra sự tồn tại của một trong hai điều. Nó là của hình thức

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

Câu lệnh thực tế được tạo bằng C và được thực hiện dưới dạng truy vấn đặc biệt qua kết nối ODBC.

Gần đây, người ta đã phát hiện ra rằng CHỌN thứ hai có thể sẽ nhanh hơn CHỌN thứ nhất trong hầu hết các trường hợp và việc chuyển đổi thứ tự của hai mệnh đề EXISTS đã gây ra sự tăng tốc mạnh mẽ trong ít nhất một trường hợp thử nghiệm lạm dụng mà chúng tôi vừa tạo.

Điều rõ ràng cần làm là chỉ cần tiếp tục và chuyển đổi hai mệnh đề, nhưng tôi muốn xem liệu ai đó quen thuộc hơn với SQL Server có quan tâm đến việc này không. Cảm giác như tôi đang dựa vào sự trùng hợp và một "chi tiết thực hiện".

(Cũng có vẻ như nếu SQL Server thông minh hơn, nó sẽ thực thi song song cả hai mệnh đề EXISTS và cho phép bất kỳ cái nào hoàn thành ngắn mạch đầu tiên.

Có cách nào tốt hơn để SQL Server luôn cải thiện thời gian chạy của một truy vấn như vậy không?

Cập nhật

Cảm ơn bạn đã dành thời gian và quan tâm đến câu hỏi của tôi. Tôi không mong đợi câu hỏi về kế hoạch truy vấn thực tế, nhưng tôi sẵn sàng chia sẻ chúng.

Đây là một thành phần phần mềm hỗ trợ SQL Server 2008R2 trở lên. Hình dạng của dữ liệu có thể khá khác nhau tùy thuộc vào cấu hình và cách sử dụng. Đồng nghiệp của tôi đã nghĩ đến việc thực hiện thay đổi này cho truy vấn vì bảng (trong ví dụ) dbf_1162761$z$rv$1257927703sẽ luôn có số lượng lớn hơn hoặc bằng số hàng trong dbf_1162761$z$dd$1257927703bảng so với bảng - đôi khi nhiều hơn đáng kể (thứ tự cường độ).

Dưới đây là trường hợp lạm dụng tôi đã đề cập. Truy vấn đầu tiên là truy vấn chậm và mất khoảng 20 giây. Truy vấn thứ hai hoàn thành ngay lập tức.

Để biết giá trị của nó, bit "TỐI ƯU HÓA CHO UNKNOWN" cũng đã được thêm vào gần đây vì việc đánh hơi tham số đã làm hỏng một số trường hợp nhất định.

Truy vấn gốc:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Kế hoạch ban đầu:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Đã sửa lỗi truy vấn:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Gói cố định:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)

Câu trả lời:


11

Theo nguyên tắc chung, SQL Server sẽ thực thi các phần của CASEcâu lệnh theo thứ tự nhưng được tự do sắp xếp lại các ORđiều kiện. Đối với một số truy vấn, bạn có thể có hiệu suất tốt hơn bằng cách thay đổi thứ tự của các WHENbiểu thức bên trong một CASEcâu lệnh. Đôi khi bạn cũng có thể có hiệu suất tốt hơn khi thay đổi thứ tự các điều kiện trong một ORtuyên bố, nhưng đó không phải là hành vi được đảm bảo.

Có lẽ tốt nhất để đi qua nó với một ví dụ đơn giản. Tôi đang thử nghiệm với SQL Server 2016 để có thể bạn sẽ không nhận được kết quả chính xác như vậy trên máy của mình, nhưng theo như tôi biết thì áp dụng các nguyên tắc tương tự. Đầu tiên tôi sẽ đặt một triệu số nguyên từ 1 đến 1000000 vào hai bảng, một bảng có chỉ số được nhóm và một dưới dạng một đống:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Hãy xem xét các truy vấn sau:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Chúng tôi biết rằng việc đánh giá truy vấn phụ X_CIsẽ rẻ hơn nhiều so với truy vấn phụ X_HEAP, đặc biệt khi không có hàng phù hợp. Nếu không có một hàng phù hợp thì chúng ta chỉ cần thực hiện một vài lần đọc logic so với bảng có chỉ mục được nhóm. Tuy nhiên, chúng ta sẽ cần quét tất cả các hàng của heap để biết rằng không có một hàng phù hợp. Trình tối ưu hóa cũng biết điều này. Nói rộng hơn, sử dụng một chỉ mục được nhóm để tra cứu một hàng là rất rẻ so với việc quét một bảng.

Đối với dữ liệu ví dụ này, tôi sẽ viết truy vấn như thế này:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Điều đó có hiệu quả buộc SQL Server chạy truy vấn con đối với bảng với chỉ mục được nhóm trước. Đây là kết quả từ SET STATISTICS IO, TIME ON:

Bảng 'X_CI'. Quét số 0, đọc logic 3, đọc vật lý 0

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

Nhìn vào kế hoạch truy vấn, nếu tìm kiếm ở nhãn 1 trả về bất kỳ dữ liệu nào ngoài việc quét tại nhãn 2 là không bắt buộc và sẽ không xảy ra:

truy vấn tốt

Các truy vấn sau đây là ít hiệu quả hơn:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Nhìn vào kế hoạch truy vấn, chúng ta thấy rằng việc quét ở nhãn 2 luôn xảy ra. Nếu một hàng được tìm thấy thì tìm kiếm tại nhãn 1 bị bỏ qua. Đó không phải là thứ tự mà chúng tôi muốn:

kế hoạch truy vấn xấu

Kết quả thực hiện trở lại:

Bảng 'X_HEAP'. Quét số 1, đọc logic 7247

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

Quay trở lại truy vấn ban đầu, đối với truy vấn này, tôi thấy tìm kiếm và quét được đánh giá theo thứ tự tốt cho hiệu suất:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Và trong truy vấn này, chúng được đánh giá theo thứ tự ngược lại:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Tuy nhiên, không giống như các cặp truy vấn trước đó, không có gì buộc trình tối ưu hóa truy vấn SQL Server phải đánh giá cái này trước cái kia. Bạn không nên dựa vào hành vi đó cho bất cứ điều gì quan trọng.

Để kết luận, nếu bạn cần một truy vấn con được đánh giá trước cái kia thì hãy sử dụng một CASEcâu lệnh hoặc một số phương thức khác để buộc đặt hàng. Mặt khác, vui lòng đặt hàng các truy vấn con trong một ORđiều kiện theo cách bạn muốn, nhưng biết rằng không có gì đảm bảo rằng trình tối ưu hóa sẽ thực hiện chúng theo thứ tự như được viết.

Phụ lục:

Một câu hỏi tiếp theo tự nhiên là bạn có thể làm gì nếu bạn muốn SQL Server quyết định truy vấn nào rẻ hơn và thực hiện truy vấn đó trước? Tất cả các phương thức cho đến nay dường như được SQL Server triển khai theo thứ tự truy vấn được viết, ngay cả khi đó không phải là hành vi được đảm bảo cho một số trong số chúng.

Đây là một tùy chọn có vẻ hoạt động cho các bảng demo đơn giản:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Bạn có thể tìm thấy một bản demo fiddle db ở đây . Thay đổi thứ tự của các bảng dẫn xuất không thay đổi kế hoạch truy vấn. Trong cả hai truy vấn, X_HEAPbảng không được chạm vào. Nói cách khác, trình tối ưu hóa truy vấn xuất hiện để thực hiện truy vấn rẻ hơn trước. Tôi không thể khuyên bạn nên sử dụng một cái gì đó như thế này trong sản xuất để nó có giá trị tò mò. Có thể có một cách đơn giản hơn nhiều để thực hiện điều tương tự.


4
Hoặc CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDcó thể là một lựa chọn thay thế, mặc dù điều đó vẫn phụ thuộc vào việc quyết định thủ công truy vấn nào nhanh hơn và đặt truy vấn đó trước. Tôi không chắc có cách nào để thể hiện nó để SQL Server sẽ tự động sắp xếp lại để cái giá rẻ được tự động đánh giá trướ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.