子查询使用Exists 1或Exists *
子查询使用Exists 1或Exists *
我以前像这样编写我的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
这样真的有区别吗?
这种误解的原因可能是因为人们认为这样做可以读取所有列。但很容易看出这不是正确的。
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在这里解释说,在编译查询时,他通常使用SELECT 1
,因为它可以使性能差异略微降低。
QP将在管道中尽早取出并展开所有
*
并将它们绑定到对象上(在这种情况下,是列的列表)。
然后,由于查询的性质,将删除不需要的列。因此,像此类简单的
EXISTS
子查询:
SELECT col1 FROM MyTable WHERE EXISTS (SELECT * FROM Table2 WHERE MyTable.col1=Table2.col2)
*
将被展开成一些可能很大的
列列表,然后确定EXISTS
语义不需要这些
列,所以基本上可以删除所有这些列。"
SELECT 1
将避免在查询编译期间
检查该表的任何不必要的元数据。但是,在运行时,这两种形式的
查询将是相同的,并且运行时间相同。
我通过使用不同数量的列测试了四种可能表达此查询的方式。
SELECT 1
vs SELECT *
vs SELECT Primary_Key
vs 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)
我发现,当表具有1024个列的情况下,大多数情况下调用堆栈看起来像下面这样,表明它确实在加载列元数据时占用了大部分时间,即使使用 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代码分析器支持,它显示了两种情况下消耗编译时间的不同函数选择情况(1024列的前15个函数与1列的前15个函数)。
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 | +-------------+-----------+------------+-----------+--------------+
SQL Server 聪明地意识到它正在被用于 EXISTS,并向系统返回没有数据。
引用微软的话:
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 列表进行了操作,它将抛出一个除以零的错误。但它没有。
编辑:请注意,SQL 标准实际上谈论了这个问题。
ANSI SQL 1992 标准,第191页,http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt
3) Case:
a) 如果“*” 简单地包含在立即包含在
中的
中,则
等价于一个任意
的
。