สาเหตุของความเข้าใจผิดนี้น่าจะเป็นเพราะความเชื่อที่ว่ามันจะจบลงด้วยการอ่านคอลัมน์ทั้งหมด จะเห็นได้ง่ายว่าไม่เป็นเช่นนั้น
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'
ให้แผน
สิ่งนี้แสดงให้เห็นว่า SQL Server สามารถใช้ดัชนีที่แคบที่สุดเพื่อตรวจสอบผลลัพธ์แม้ว่าดัชนีจะไม่รวมคอลัมน์ทั้งหมดก็ตาม การเข้าถึงดัชนีอยู่ภายใต้ตัวดำเนินการรวมกึ่งซึ่งหมายความว่าสามารถหยุดการสแกนได้ทันทีที่ส่งคืนแถวแรก
ดังนั้นจึงเป็นที่ชัดเจนว่าความเชื่อข้างต้นไม่ถูกต้อง
อย่างไรก็ตาม Conor Cunningham จากทีม Query Optimiser อธิบายที่นี่ว่าเขามักจะใช้SELECT 1
ในกรณีนี้เนื่องจากสามารถสร้างความแตกต่างด้านประสิทธิภาพเล็กน้อยในการรวบรวมแบบสอบถาม
QP จะใช้และขยายทั้งหมด*
ในช่วงต้นของไปป์ไลน์และผูกเข้ากับอ็อบเจ็กต์ (ในกรณีนี้คือรายการคอลัมน์) จากนั้นจะลบคอลัมน์ที่ไม่จำเป็นออกเนื่องจากลักษณะของข้อความค้นหา
ดังนั้นสำหรับEXISTS
แบบสอบถามย่อยง่ายๆเช่นนี้:
SELECT col1 FROM MyTable WHERE EXISTS
(SELECT * FROM Table2 WHERE
MyTable.col1=Table2.col2)
*
จะขยายไปยังรายการคอลัมน์บางที่อาจเกิดขึ้นใหญ่แล้วมันจะได้รับการพิจารณาว่าความหมายของที่
EXISTS
ไม่จำเป็นต้องใด ๆ ของคอลัมน์เหล่านั้นดังนั้นโดยทั่วไปทั้งหมดของพวกเขาสามารถถอดออกได้
" SELECT 1
" จะหลีกเลี่ยงการตรวจสอบข้อมูลเมตาที่ไม่จำเป็นสำหรับตารางนั้นในระหว่างการรวบรวมคำค้นหา
อย่างไรก็ตามในรันไทม์แบบสอบถามทั้งสองรูปแบบจะเหมือนกันและจะมีเวลาทำงานที่เหมือนกัน
ฉันได้ทดสอบสี่วิธีที่เป็นไปได้ในการแสดงข้อความค้นหานี้บนตารางว่างที่มีจำนวนคอลัมน์ต่างๆ SELECT 1
เทียบกับSELECT *
เทียบSELECT Primary_Key
กับ SELECT Other_Not_Null_Column
.
ฉันรันการสืบค้นแบบวนซ้ำโดยใช้OPTION (RECOMPILE)
และวัดจำนวนการดำเนินการโดยเฉลี่ยต่อวินาที ผลลัพธ์ด้านล่าง
+
| 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 |
+
ดังจะเห็นได้ว่าไม่มีผู้ชนะที่สอดคล้องกันระหว่างSELECT 1
และSELECT *
และความแตกต่างระหว่างสองแนวทางนี้เป็นเรื่องเล็กน้อย SELECT Not Null col
และSELECT PK
ไม่ปรากฏเร็วขึ้นแม้เพียงเล็กน้อย
คำค้นหาทั้งสี่รายการลดประสิทธิภาพลงเมื่อจำนวนคอลัมน์ในตารางเพิ่มขึ้น
เนื่องจากตารางว่างเปล่าความสัมพันธ์นี้จึงดูเหมือนชัดเจนตามจำนวนข้อมูลเมตาของคอลัมน์เท่านั้น เพื่อให้COUNT(1)
ง่ายต่อการดูว่าสิ่งนี้ถูกเขียนใหม่COUNT(*)
ในบางจุดของกระบวนการจากด้านล่าง
SET SHOWPLAN_TEXT ON;
GO
SELECT COUNT(1)
FROM master..spt_values
ซึ่งให้แผนดังต่อไปนี้
|
|
|
การแนบดีบักเกอร์เข้ากับกระบวนการ SQL Server และการทำลายแบบสุ่มขณะดำเนินการด้านล่าง
DECLARE @V int
WHILE (1=1)
SELECT @V=1 WHERE EXISTS (SELECT 1 FROM
ฉันพบว่าในกรณีที่ตารางมี 1,024 คอลัมน์โดยส่วนใหญ่ call stack จะมีลักษณะดังต่อไปนี้แสดงว่ามีการใช้เวลาส่วนใหญ่ในการโหลดข้อมูลเมตาของคอลัมน์เวลาแม้ว่าSELECT 1
จะมีการใช้งานก็ตาม (สำหรับกรณีที่ ตารางมี 1 คอลัมน์การทำลายแบบสุ่มไม่ได้เข้าสู่สแต็คการโทรนี้ใน 10 ครั้ง)
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
ความพยายามในการสร้างโปรไฟล์ด้วยตนเองนี้ได้รับการสำรองข้อมูลโดยตัวสร้างรหัสโปรไฟล์ VS 2012 ซึ่งแสดงการเลือกฟังก์ชันที่แตกต่างกันมากซึ่งใช้เวลาในการรวบรวมสำหรับสองกรณี ( ฟังก์ชัน 15 อันดับแรก 1024 คอลัมน์เทียบกับ15 ฟังก์ชันสูงสุด 1 คอลัมน์ )
ทั้งเวอร์ชันSELECT 1
และSELECT *
เวอร์ชันจะปิดการตรวจสอบสิทธิ์ของคอลัมน์และล้มเหลวหากผู้ใช้ไม่ได้รับอนุญาตให้เข้าถึงคอลัมน์ทั้งหมดในตาราง
ตัวอย่างที่ฉันเปลื้องจากการสนทนาบนกอง
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);
GO
REVERT;
DROP USER blat
DROP TABLE T
ดังนั้นเราอาจคาดเดาได้ว่าความแตกต่างเล็กน้อยที่เห็นได้ชัดเมื่อใช้SELECT some_not_null_col
คือการตรวจสอบสิทธิ์ในคอลัมน์นั้น ๆ เท่านั้น (แม้ว่าจะยังคงโหลดข้อมูลเมตาสำหรับทุกคน) อย่างไรก็ตามสิ่งนี้ดูเหมือนจะไม่เข้ากับข้อเท็จจริงเนื่องจากเปอร์เซ็นต์ความแตกต่างระหว่างสองวิธีนี้หากสิ่งใดเล็กลงเมื่อจำนวนคอลัมน์ในตารางพื้นฐานเพิ่มขึ้น
ไม่ว่าในกรณีใดก็ตามฉันจะไม่รีบออกไปและเปลี่ยนคำค้นหาทั้งหมดของฉันเป็นแบบฟอร์มนี้เนื่องจากความแตกต่างมีน้อยมากและจะปรากฏเฉพาะในระหว่างการรวบรวมแบบสอบถาม การลบOPTION (RECOMPILE)
เพื่อให้การดำเนินการในภายหลังสามารถใช้แผนแคชให้ดังต่อไปนี้
+
| 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 |
+
สคริปต์ทดสอบที่ฉันใช้สามารถพบได้ที่นี่