การสืบค้นย่อยโดยใช้ Exists 1 หรือ Exists *


89

ฉันเคยเขียนเช็ค EXISTS ของฉันดังนี้:

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

DBA คนหนึ่งในชีวิตก่อนหน้านี้บอกฉันว่าเมื่อฉันทำEXISTSประโยคให้ใช้SELECT 1แทนSELECT *

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

สิ่งนี้สร้างความแตกต่างได้จริงหรือ?


1
คุณลืม EXISTS (เลือกค่าว่างจาก ... ) สิ่งนี้ถูกถามเมื่อเร็ว ๆ นี้ btw
OMG Ponies

17
ps รับ DBA ใหม่ ไสยศาสตร์ไม่มีที่ใดในไอทีโดยเฉพาะอย่างยิ่งในการจัดการฐานข้อมูล (จากอดีต DBA !!!)
Matt Rogish

คำตอบ:


136

ไม่ SQL Server ฉลาดและรู้ว่ากำลังใช้สำหรับ EXISTS และส่งคืน NO DATA ไปยังระบบ

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

รายการที่เลือกของแบบสอบถามย่อยที่ EXISTS แนะนำมักจะประกอบด้วยเครื่องหมายดอกจัน (*) ไม่มีเหตุผลที่จะแสดงชื่อคอลัมน์เนื่องจากคุณเพิ่งทดสอบว่ามีแถวที่ตรงตามเงื่อนไขที่ระบุไว้ในเคียวรีย่อยหรือไม่

ในการตรวจสอบตัวเองให้ลองดำเนินการดังต่อไปนี้:

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

ถ้ามันกำลังทำอะไรบางอย่างกับรายการ SELECT มันจะทำให้ข้อผิดพลาด div เป็นศูนย์ มันไม่

แก้ไข: หมายเหตุมาตรฐาน SQL พูดถึงเรื่องนี้จริงๆ

มาตรฐาน ANSI SQL 1992, หน้า 191 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) กรณี:
ก) ถ้า<select list>"*" ที่มีอยู่เพียงแค่ใน<subquery> ที่มีอยู่ได้ทันทีใน<exists predicate>นั้น<select list> เทียบเท่ากับที่เป็นพล<value expression> <literal>


1
EXISTSเคล็ดลับกับ 1/0 สามารถขยายได้แม้กระทั่งนี้SELECT 1 WHERE EXISTS(SELECT 1/0)... ดูเหมือนว่าขั้นตอนนามธรรมมากขึ้นแล้วเป็นครั้งที่สองSELECTไม่มีFROMข้อ
whytheq

1
@whytheq - อรSELECT COUNT(*) WHERE EXISTS(SELECT 1/0). การSELECTไม่มีFROMSQL Server จะถือว่าเหมือนกับว่ากำลังเข้าถึงตารางแถวเดียว (เช่นคล้ายกับการเลือกจากdualตารางใน RDBMS อื่น ๆ )
Martin Smith

@MartinSmith ไชโย - ดังนั้นประเด็นก็คือSELECTสร้างตาราง 1 แถวก่อนที่จะทำอย่างอื่นแม้ว่า1/0จะยังคงขยะตาราง 1 แถวอยู่EXISTSหรือไม่?
whytheq

เป็นเช่นนี้มาตลอดหรือเป็นการเพิ่มประสิทธิภาพที่นำมาใช้ใน SQL Server เวอร์ชันใดรุ่นหนึ่ง
Martin Brown

1
@MartinSmith TIL "quoth". ขอขอบคุณที่แก้ไขกลับ
Gurwinder Singh

114

สาเหตุของความเข้าใจผิดนี้น่าจะเป็นเพราะความเชื่อที่ว่ามันจะจบลงด้วยการอ่านคอลัมน์ทั้งหมด จะเห็นได้ง่ายว่าไม่เป็นเช่นนั้น

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

ซึ่งให้แผนดังต่อไปนี้

  |--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]))

การแนบดีบักเกอร์เข้ากับกระบวนการ SQL Server และการทำลายแบบสุ่มขณะดำเนินการด้านล่าง

DECLARE @V int 

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

ฉันพบว่าในกรณีที่ตารางมี 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); 
/*  ↑↑↑↑ 
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

ดังนั้นเราอาจคาดเดาได้ว่าความแตกต่างเล็กน้อยที่เห็นได้ชัดเมื่อใช้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   |
+-------------+-----------+------------+-----------+--------------+

สคริปต์ทดสอบที่ฉันใช้สามารถพบได้ที่นี่


3
+1 คำตอบนี้สมควรได้รับการโหวตมากขึ้นสำหรับความพยายามที่จะได้รับข้อมูลจริง
จอน

1
มีความคิดว่า SQL Server เวอร์ชันใดที่สถิติเหล่านี้ถูกสร้างขึ้น
Martin Brown

3
@MartinBrown - IIRC เดิมปี 2008 แม้ว่าฉันจะทำการทดสอบซ้ำเมื่อเร็ว ๆ นี้ในปี 2012 สำหรับการแก้ไขล่าสุดและพบว่าเหมือนกัน
Martin Smith

8

วิธีที่ดีที่สุดคือการทดสอบประสิทธิภาพทั้งสองเวอร์ชันและดูแผนการดำเนินการของทั้งสองเวอร์ชัน เลือกตารางที่มีคอลัมน์จำนวนมาก


2
+1. ไม่รู้ว่าเหตุใดจึงมีการโหวตลดลง ฉันคิดเสมอว่าการสอนผู้ชายให้ตกปลาดีกว่าแค่ให้ปลาเขา คนเราจะเรียนรู้อะไรได้อย่างไร?
Ogre Psalm33

5

ไม่มีความแตกต่างใน SQL Server และไม่เคยมีปัญหาใน SQL Server เครื่องมือเพิ่มประสิทธิภาพรู้ว่าพวกเขาเหมือนกัน หากคุณดูแผนการดำเนินการคุณจะเห็นว่าเหมือนกัน


1

โดยส่วนตัวแล้วฉันพบว่ามันยากมากที่จะเชื่อว่าพวกเขาไม่ได้ปรับให้เหมาะสมกับแผนการสืบค้นเดียวกัน แต่วิธีเดียวที่จะรู้ว่าในสถานการณ์เฉพาะของคุณคือการทดสอบ หากเป็นเช่นนั้นโปรดแจ้งกลับ!


-1

ไม่มีความแตกต่างที่แท้จริง แต่อาจมีประสิทธิภาพที่ยอดเยี่ยมเล็กน้อย ตามหลักทั่วไปคุณไม่ควรขอข้อมูลมากเกินความต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.