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
Đ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 1
trong 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 EXISTS
truy 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 1
vsSELECT *
vs SELECT Primary_Key
vs 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
+-------------+----------+---------+---------+--------------+
| 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 1
vàSELECT *
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 col
vàSELECT 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 1
vàSELECT *
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.
+-------------+-----------+------------+-----------+--------------+
| 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