Truy vấn con sử dụng Tồn tại 1 hoặc Tồn tại *


88

Tôi đã từng viết kiểm tra EXISTS của mình như thế này:

IF EXISTS (SELECT * FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Where Columns=@Filters
END

Một trong những DBA ở kiếp trước đã nói với tôi rằng khi tôi thực hiện một EXISTSmệnh đề, hãy sử dụng SELECT 1thay vìSELECT *

IF EXISTS (SELECT 1 FROM TABLE WHERE Columns=@Filters)
BEGIN
   UPDATE TABLE SET ColumnsX=ValuesX WHERE Columns=@Filters
END

Điều này có thực sự tạo ra sự khác biệt?


1
Bạn quên TỒN TẠI (CHỌN NULL TỪ ...). Điều này đã được hỏi gần đây btw
OMG Ngựa Non

16
ps nhận một DBA mới. Mê tín dị đoan không có chỗ đứng trong lĩnh vực CNTT, đặc biệt là trong việc quản lý cơ sở dữ liệu (từ một DBA cựu !!!)
Matt Rogish

Câu trả lời:


135

Không, SQL Server thông minh và biết nó đang được sử dụng cho một TỒN TẠI và trả về KHÔNG DỮ LIỆU cho hệ thống.

Quoth Microsoft: http://technet.microsoft.com/en-us/library/ms189259.aspx?ppud=4

Danh sách lựa chọn của một truy vấn con được EXISTS giới thiệu hầu như luôn bao gồm dấu hoa thị (*). Không có lý do gì để liệt kê tên cột vì bạn chỉ đang kiểm tra xem các hàng đáp ứng các điều kiện được chỉ định trong truy vấn con có tồn tại hay không.

Để tự kiểm tra, hãy thử chạy như sau:

SELECT whatever
  FROM yourtable
 WHERE EXISTS( SELECT 1/0
                 FROM someothertable 
                WHERE a_valid_clause )

Nếu nó thực sự đang làm gì đó với danh sách SELECT, nó sẽ ném ra lỗi div bằng 0. Nó không.

CHỈNH SỬA: Lưu ý, Chuẩn SQL thực sự nói về điều này.

Tiêu chuẩn ANSI SQL 1992, trang 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) Trường hợp:
a) Nếu dấu <select list>"*" đơn giản được chứa trong a <subquery> mà ngay lập tức được chứa trong dấu <exists predicate>, thì dấu <select list> tương đương với a <value expression> là tùy ý <literal>.


1
các EXISTStrick với 1/0 thậm chí có thể được mở rộng đến nay SELECT 1 WHERE EXISTS(SELECT 1/0)... dường như là một bước trừu tượng hơn sau đó là thứ hai SELECTkhông có FROMkhoản
whytheq

1
@whytheq - Hoặc SELECT COUNT(*) WHERE EXISTS(SELECT 1/0). Một SELECTkhông có FROMtrong SQL Server được coi là mặc dù nó đã được truy cập vào một bảng duy nhất hàng (ví dụ tương tự như lựa chọn từ các dualbảng trong RDBMS khác)
Martin Smith

@MartinSmith cổ vũ - vì vậy vấn đề là SELECTtạo ra một bảng 1 hàng trước khi nó thực hiện bất kỳ điều gì khác, vì vậy mặc dù 1/0rác vẫn là bảng 1 hàng EXISTS?
whytheq

Có phải trường hợp này luôn luôn xảy ra hay đó là một cách tối ưu hóa đã được giới thiệu trong một phiên bản SQL Server cụ thể?
Martin Brown,

1
@MartinSmith TIL "quoth". Cảm ơn vì đã sửa nó trở lại.
Gurwinder Singh

111

Lý do cho quan niệm sai lầm này có lẽ là vì niềm tin rằng nó sẽ kết thúc việc đọc tất cả các cột. Có thể dễ dàng nhận thấy rằng không phải như vậy.

CREATE TABLE T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)

CREATE NONCLUSTERED INDEX NarrowIndex ON T(Y)

IF EXISTS (SELECT * FROM T)
    PRINT 'Y'

Cung cấp kế hoạch

Kế hoạch

Điều này cho thấy SQL Server đã có thể sử dụng chỉ mục hẹp nhất có sẵn để kiểm tra kết quả mặc dù thực tế là chỉ mục không bao gồm tất cả các cột. Quyền truy cập chỉ mục nằm dưới toán tử kết hợp bán có nghĩa là nó có thể ngừng quét ngay sau khi hàng đầu tiên được trả lại.

Như vậy rõ ràng niềm tin trên là sai lầm.

Tuy nhiên, Conor Cunningham từ nhóm Trình tối ưu hóa truy vấn giải thích ở đây rằng anh ấy thường sử dụng SELECT 1trong trường hợp này vì nó có thể tạo ra sự khác biệt nhỏ về hiệu suất trong quá trình biên dịch truy vấn.

QP sẽ tiếp nhận và mở rộng tất cả * sớm trong đường ống và liên kết chúng với các đối tượng (trong trường hợp này là danh sách các cột). Sau đó, nó sẽ loại bỏ các cột không cần thiết do bản chất của truy vấn.

Vì vậy, đối với một EXISTStruy vấn con đơn giản như thế này:

SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2)Các *sẽ được mở rộng đến một số danh sách cột có khả năng lớn và sau đó nó sẽ được xác định rằng ngữ nghĩa của các EXISTS không yêu cầu bất kỳ của những cột, vì vậy về cơ bản tất cả trong số họ có thể được gỡ bỏ.

"SELECT 1 " sẽ tránh phải kiểm tra mọi siêu dữ liệu không cần thiết cho bảng đó trong quá trình biên dịch truy vấn.

Tuy nhiên, trong thời gian chạy, hai dạng truy vấn sẽ giống hệt nhau và sẽ có thời gian chạy giống hệt nhau.

Tôi đã thử nghiệm bốn cách có thể để thể hiện truy vấn này trên một bảng trống có nhiều cột khác nhau. SELECT 1vsSELECT * vs SELECT Primary_Keyvs SELECT Other_Not_Null_Column.

Tôi đã chạy các truy vấn trong một vòng lặp bằng cách sử dụng OPTION (RECOMPILE)và đo số lần thực thi trung bình mỗi giây. Kết quả bên dưới

nhập mô tả hình ảnh ở đây

+-------------+----------+---------+---------+--------------+
| Num of Cols |    *     |    1    |   PK    | Not Null col |
+-------------+----------+---------+---------+--------------+
| 2           | 2043.5   | 2043.25 | 2073.5  | 2067.5       |
| 4           | 2038.75  | 2041.25 | 2067.5  | 2067.5       |
| 8           | 2015.75  | 2017    | 2059.75 | 2059         |
| 16          | 2005.75  | 2005.25 | 2025.25 | 2035.75      |
| 32          | 1963.25  | 1967.25 | 2001.25 | 1992.75      |
| 64          | 1903     | 1904    | 1936.25 | 1939.75      |
| 128         | 1778.75  | 1779.75 | 1799    | 1806.75      |
| 256         | 1530.75  | 1526.5  | 1542.75 | 1541.25      |
| 512         | 1195     | 1189.75 | 1203.75 | 1198.5       |
| 1024        | 694.75   | 697     | 699     | 699.25       |
+-------------+----------+---------+---------+--------------+
| Total       | 17169.25 | 17171   | 17408   | 17408        |
+-------------+----------+---------+---------+--------------+

Có thể thấy không có người chiến thắng nhất quán giữa SELECT 1SELECT * và sự khác biệt giữa hai cách tiếp cận là không đáng kể. các SELECT Not Null colSELECT PK làm xuất hiện nhanh hơn mặc dù hơi.

Tất cả bốn truy vấn đều giảm hiệu suất khi số lượng cột trong bảng tăng lên.

Vì bảng trống, mối quan hệ này dường như chỉ có thể giải thích được bằng số lượng siêu dữ liệu cột. Để COUNT(1)dễ dàng nhận thấy rằng điều này được viết lại COUNT(*)vào một thời điểm nào đó trong quá trình từ bên dưới.

SET SHOWPLAN_TEXT ON;

GO

SELECT COUNT(1)
FROM master..spt_values

Cái nào đưa ra kế hoạch sau

  |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1004],0)))
       |--Stream Aggregate(DEFINE:([Expr1004]=Count(*)))
            |--Index Scan(OBJECT:([master].[dbo].[spt_values].[ix2_spt_values_nu_nc]))

Đính kèm trình gỡ lỗi vào quy trình SQL Server và ngắt ngẫu nhiên trong khi thực hiện thao tác bên dưới

DECLARE @V int 

WHILE (1=1)
    SELECT @V=1 WHERE EXISTS (SELECT 1 FROM ##T) OPTION(RECOMPILE)

Tôi nhận thấy rằng trong các trường hợp bảng có 1.024 cột hầu hết thời gian, ngăn xếp cuộc gọi trông giống như bên dưới cho thấy rằng nó thực sự dành một phần lớn thời gian tải siêu dữ liệu cột ngay cả khi SELECT 1được sử dụng (Đối với trường hợp bảng có 1 cột bị ngắt ngẫu nhiên đã không đạt được bit này của ngăn xếp cuộc gọi trong 10 lần thử)

sqlservr.exe!CMEDAccess::GetProxyBaseIntnl()  - 0x1e2c79 bytes  
sqlservr.exe!CMEDProxyRelation::GetColumn()  + 0x57 bytes   
sqlservr.exe!CAlgTableMetadata::LoadColumns()  + 0x256 bytes    
sqlservr.exe!CAlgTableMetadata::Bind()  + 0x15c bytes   
sqlservr.exe!CRelOp_Get::BindTree()  + 0x98 bytes   
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_FromList::BindTree()  + 0x5c bytes  
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CRelOp_QuerySpec::BindTree()  + 0xbe bytes 
sqlservr.exe!COptExpr::BindTree()  + 0x58 bytes 
sqlservr.exe!CScaOp_Exists::BindScalarTree()  + 0x72 bytes  
... Lines omitted ...
msvcr80.dll!_threadstartex(void * ptd=0x0031d888)  Line 326 + 0x5 bytes C
kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes 

Nỗ lực biên dịch thủ công này được sao lưu bởi trình biên dịch mã VS 2012, cho thấy lựa chọn rất khác nhau của các chức năng tiêu tốn thời gian biên dịch cho hai trường hợp ( 15 Hàm hàng đầu 1024 cột so với 15 Hàm hàng đầu 1 cột ).

Cả SELECT 1SELECT * phiên bản phiên bản đều kết thúc việc kiểm tra quyền của cột và không thành công nếu người dùng không được cấp quyền truy cập vào tất cả các cột trong bảng.

Một ví dụ tôi ghi lại từ một cuộc trò chuyện trên heap

CREATE USER blat WITHOUT LOGIN;
GO
CREATE TABLE dbo.T
(
X INT PRIMARY KEY,
Y INT,
Z CHAR(8000)
)
GO

GRANT SELECT ON dbo.T TO blat;
DENY SELECT ON dbo.T(Z) TO blat;
GO
EXECUTE AS USER = 'blat';
GO

SELECT 1
WHERE  EXISTS (SELECT 1
               FROM   T); 
/*  ↑↑↑↑ 
Fails unexpectedly with 

The SELECT permission was denied on the column 'Z' of the 
           object 'T', database 'tempdb', schema 'dbo'.*/

GO
REVERT;
DROP USER blat
DROP TABLE T

Vì vậy người ta có thể suy đoán rằng sự khác biệt rõ ràng nhỏ khi sử dụng SELECT some_not_null_col là nó chỉ kiểm tra các quyền trên cột cụ thể đó (mặc dù vẫn tải siêu dữ liệu cho tất cả). Tuy nhiên, điều này dường như không phù hợp với thực tế vì sự khác biệt về tỷ lệ phần trăm giữa hai cách tiếp cận nếu bất kỳ điều gì nhỏ hơn khi số lượng cột trong bảng bên dưới tăng lên.

Trong mọi trường hợp, tôi sẽ không vội vàng và thay đổi tất cả các truy vấn của mình thành biểu mẫu này vì sự khác biệt là rất nhỏ và chỉ rõ ràng trong quá trình biên dịch truy vấn. Loại bỏ OPTION (RECOMPILE)để các lần thực thi tiếp theo có thể sử dụng một kế hoạch được lưu trong bộ nhớ cache như sau.

nhập mô tả hình ảnh ở đây

+-------------+-----------+------------+-----------+--------------+
| Num of Cols |     *     |     1      |    PK     | Not Null col |
+-------------+-----------+------------+-----------+--------------+
| 2           | 144933.25 | 145292     | 146029.25 | 143973.5     |
| 4           | 146084    | 146633.5   | 146018.75 | 146581.25    |
| 8           | 143145.25 | 144393.25  | 145723.5  | 144790.25    |
| 16          | 145191.75 | 145174     | 144755.5  | 146666.75    |
| 32          | 144624    | 145483.75  | 143531    | 145366.25    |
| 64          | 145459.25 | 146175.75  | 147174.25 | 146622.5     |
| 128         | 145625.75 | 143823.25  | 144132    | 144739.25    |
| 256         | 145380.75 | 147224     | 146203.25 | 147078.75    |
| 512         | 146045    | 145609.25  | 145149.25 | 144335.5     |
| 1024        | 148280    | 148076     | 145593.25 | 146534.75    |
+-------------+-----------+------------+-----------+--------------+
| Total       | 1454769   | 1457884.75 | 1454310   | 1456688.75   |
+-------------+-----------+------------+-----------+--------------+

Tập lệnh thử nghiệm tôi đã sử dụng có thể được tìm thấy ở đây


3
+1 Câu trả lời này xứng đáng nhận được nhiều phiếu bầu hơn cho nỗ lực liên quan để có được dữ liệu thực.
Jon

1
Bất kỳ ý tưởng nào về phiên bản SQL Server mà các số liệu thống kê này được tạo trên?
Martin Brown,

3
@MartinBrown - IIRC ban đầu là năm 2008 mặc dù tôi đã làm lại các bài kiểm tra gần đây vào năm 2012 cho bản chỉnh sửa gần đây nhất và thấy giống nhau.
Martin Smith,

8

Cách tốt nhất để biết là kiểm tra hiệu suất cả hai phiên bản và kiểm tra kế hoạch thực thi cho cả hai phiên bản. Chọn một bảng có nhiều cột.


2
+1. Không biết tại sao cái này lại bị bỏ phiếu. Tôi luôn nghĩ rằng tốt hơn là dạy một người đàn ông câu cá, hơn là chỉ cho anh ta một con cá. Làm thế nào mọi người sẽ học được bất cứ điều gì?
Ogre Psalm33

5

Không có sự khác biệt trong SQL Server và nó chưa bao giờ là một vấn đề trong SQL Server. Trình tối ưu hóa biết rằng chúng giống nhau. Nếu bạn nhìn vào các kế hoạch thực hiện, bạn sẽ thấy rằng chúng giống hệt nhau.


1

Cá nhân tôi thấy rất, rất khó tin rằng họ không tối ưu hóa cho cùng một kế hoạch truy vấn. Nhưng cách duy nhất để biết trong tình huống cụ thể của bạn là kiểm tra nó. Nếu bạn làm vậy, vui lòng báo lại!


-1

Không có bất kỳ sự khác biệt thực sự nào nhưng có thể có một hiệu suất rất nhỏ. Theo nguyên tắc chung, bạn không nên yêu cầu nhiều dữ liệu hơn mức bạn cần.

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.