【问题标题】:Subquery using Exists 1 or Exists *使用 Exists 1 或 Exists * 的子查询
【发布时间】:2010-12-08 12:29:22
【问题描述】:

我曾经这样写我的 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

这真的有影响吗?

【问题讨论】:

  • 你忘记了 EXISTS(SELECT NULL FROM ...)。顺便说一句,这是最近被问到的
  • 附注获得一个新的 DBA。迷信在 IT 中没有立足之地,尤其是在数据库管理中(来自前 DBA !!!)

标签: sql sql-server tsql


【解决方案1】:

不,SQL Server 很聪明,知道它正被用于 EXISTS,并且向系统返回 NO DATA。

微软: 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 by zero 错误。没有。

编辑:注意,SQL 标准实际上谈到了这一点。

ANSI SQL 1992 标准,第 191 页 http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

3) 案例:
a) 如果<select list> "*" 简单地包含在<subquery> 中,那么 立即包含在<exists predicate> 中,那么<select list> 是 相当于<value expression> 那是一个任意的<literal>

【讨论】:

  • 带有 1/0 的 EXISTS 技巧甚至可以扩展到这个 SELECT 1 WHERE EXISTS(SELECT 1/0) ... 似乎比第二个 SELECT 没有 FROM 子句更抽象
  • @whytheq - 或SELECT COUNT(*) WHERE EXISTS(SELECT 1/0)。 SQL Server 中没有FROMSELECT 被视为访问单行表(例如,类似于从其他RDBMS 中的dual 表中进行选择)
  • @MartinSmith 欢呼 - 所以重点是 SELECT 在执行其他任何操作之前创建了一个 1 行表,所以即使 1/0 是垃圾 1 行表仍然 EXISTS
  • 一直都是这样,还是在特定版本的 SQL Server 中引入了优化?
  • @MartinSmith TIL "quoth"。感谢您修复它。
【解决方案2】:

造成这种误解的原因大概是因为相信它最终会阅读所有专栏。很容易看出情况并非如此。

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 解释说 here,他通常在这种情况下使用 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 1SELECT * 之间没有一致的赢家,两种方法之间的差异可以忽略不计。不过,SELECT Not Null colSELECT 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 列的情况下,调用堆栈看起来像下面这样,表明它确实花费了大部分时间来加载列元数据,即使使用 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 代码分析器支持,该分析器显示了在两种情况下消耗编译时间的非常不同的函数选择(Top 15 Functions 1024 columnsTop 15 Functions 1 column)。

SELECT 1SELECT * 版本都会检查列权限,如果用户没有被授予对表中所有列的访问权限,则会失败。

我从 the 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

因此,人们可能会推测,使用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   |
+-------------+-----------+------------+-----------+--------------+

The test script I used can be found here

【讨论】:

  • +1 这个答案值得更多的支持来获得真实数据。
  • 知道这些统计信息是在哪个版本的 SQL Server 上生成的吗?
  • @MartinBrown - IIRC 最初是 2008 年,尽管我最近在 2012 年重新进行了测试以进行最新编辑并发现相同。
【解决方案3】:

了解的最佳方法是对两个版本进行性能测试并检查两个版本的执行计划。选择一个有很多列的表格。

【讨论】:

  • +1。不知道为什么这被否决了。我一直认为教一个人钓鱼比只给他一条鱼更好。人们将如何学习任何东西?
【解决方案4】:

在 SQL Server 中没有任何区别,在 SQL Server 中从来都不是问题。优化器知道它们是相同的。如果您查看执行计划,您会发现它们是相同的。

【讨论】:

    【解决方案5】:

    就我个人而言,我很难相信他们没有针对相同的查询计划进行优化。但是,了解您的特定情况的唯一方法是对其进行测试。如果你这样做了,请回来报告!

    【讨论】:

      【解决方案6】:

      没有任何真正的区别,但可能会对性能造成非常小的影响。根据经验,您不应该要求比您需要的更多的数据。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-06-12
        • 2020-12-27
        • 2011-11-30
        • 1970-01-01
        • 2012-12-20
        • 1970-01-01
        相关资源
        最近更新 更多