优化涉及 IF EXISTS 的 SQL

Posted

技术标签:

【中文标题】优化涉及 IF EXISTS 的 SQL【英文标题】:Optimising SQL involving IF EXISTS 【发布时间】:2016-01-19 13:49:13 【问题描述】:

我正在尝试跟踪是否需要更新某些表。我有一个要监视更改的事件表,还有另一个名为 DictionaryRefresh 的表,它跟踪对该表所做的更改。如果 Events 表被编辑,它的编辑时间将被保存并且比 DictionaryRefresh 表的最后刷新时间晚,因此表明需要刷新。此外,如果将新行添加到 Events 表中,则 DictionaryRefresh 表中还需要关联新条目 - 因此是 LEFT JOIN。

这里是表结构

CREATE TABLE [dbo].[DictionaryRefresh]
(
    [LookupKey] [varchar](31) NOT NULL,
    [LookupValue] [varchar](255) NOT NULL,
    [RecordNumber] [int] NULL,
    [RefreshTime] [datetime] NULL,
    [EventKey] [varchar](31) NULL,
    [MappedLookupKey] [varchar](31) NULL
 ) ON [PRIMARY]

索引是(遵循 DBEngine Tuning Advisor)

CREATE NONCLUSTERED INDEX [idx_DictionaryRefresh2146B4EB] 
ON [dbo].[Ifx_DictionaryRefresh] ([LookupKey] ASC)

CREATE NONCLUSTERED INDEX [idx_DictionaryRefresh51EC6492] 
ON [dbo].[Ifx_DictionaryRefresh] ([MappedLookupKey] ASC, [RefreshTime] ASC, [RecordNumber] ASC, [EventKey] ASC)

CREATE NONCLUSTERED INDEX [idx_DictionaryRefreshFCDAD7FA] 
ON [dbo].[Ifx_DictionaryRefresh] ([LookupValue] ASC)

事件表如下:

CREATE TABLE [dbo].[Events](
    [RecordNumber] [int] NOT NULL,
    ...
    [EventKey] [varchar](31) NOT NULL,
    ...
    [EditTime] [datetime] NULL,
    ...
PRIMARY KEY CLUSTERED([RecordNumber] ASC)

CREATE NONCLUSTERED INDEX [idxEvents299ADAC8] 
ON [dbo].[Events]([EditTime] ASC)

CREATE NONCLUSTERED INDEX [idxEvents5B151A5E] 
ON [dbo].[Events]([EventKey] ASC)

现在我正在运行的 SQL 如下 - 需要将近一分钟才能返回。如果我只执行子查询,它会立即返回。

IF EXISTS (
    SELECT 1
    FROM (
        SELECT 
            e.EventKey AS DictionaryKey
            ,ISNULL(e.EditTime, '1 Jan 1900 01:00') AS EditTime
            ,e.RecordNumber AS DictionaryRecordNumber
        FROM Events e) d
    LEFT JOIN DictionaryRefresh r ON r.RecordNumber = DictionaryRecordNumber
        AND r.EventKey = DictionaryKey
        AND r.MappedLookupKey = 'M18E2I501'
    WHERE r.RefreshTime < d.EditTime
        OR r.RecordNumber IS NULL)
BEGIN
    PRINT 'TRUE'
END

DictionaryRefresh 表中有大约 130K 行,Events 表中有大约 8K 行

DictionaryRefresh 表为空或很小时,它会非常快,但会随着 DictionaryRefresh 中行数的增加而减慢,尤其是在没有符合条件的情况下。

这是执行计划。

以及显着的统计数据(索引查找占成本的 94% - 访问的行数实际上是事件表中行数的平方)...

我已经尝试更换了

IF EXISTS 

IF (SELECT COUNT ...) <> 0

还有

IF (SELECT TOP 1 1 ...) = 1

但似乎没有一个更快。

如果您有任何建议,我将不胜感激。

提前致谢。

S

【问题讨论】:

你试过这样 IF EXISTS (SELECT TOP 1 1 FROM events e LEFT JOIN dictionaryrefresh r ON r.RecordNumber = e.DictionaryRecordNumber AND r.EventKey = e.DictionaryKey AND r.MappedLookupKey = 'M18E2I501 ' WHERE r.RefreshTime 请提供Events表的表结构,如Dictionaryrefresh 谢谢。不幸的是,它花费了大致相同的时间。 IF EXISTS 是有效的,因为它只查看记录是否存在。所以如果你有 if exists (select top 1 1 from abc) 或 (select * from abc) 没关系。 没有必要在 EXISTS() 构造中使用 TOP SELECT 1。服务器知道在找到第一条记录后停止。无论如何,您的设置最让我印象深刻的是您似乎没有任何 unique 索引。除非您的数据非常奇特,否则我建议您至少在使记录可唯一识别的字段上放置一个主键。 【参考方案1】:

重新格式化你的查询我来了:

IF EXISTS ( SELECT 1 
              FROM (SELECT e.EventKey AS DictionaryKey
                          ,ISNULL(e.EditTime, '1 Jan 1900 01:00') AS EditTime
                          ,e.RecordNumber AS DictionaryRecordNumber
                      FROM Events e) d
              LEFT OUTER JOIN DictionaryRefresh r 
                           ON r.RecordNumber = d.DictionaryRecordNumber
                          AND r.EventKey = d.DictionaryKey
                          AND r.MappedLookupKey = 'M18E2I501'
             WHERE r.RefreshTime < d.EditTime
                OR r.RecordNumber IS NULL)
BEGIN
    PRINT 'TRUE'
END

我没有看到Events 上的子查询的充分理由,因此等效查询变为:

IF EXISTS ( SELECT *
              FROM Events e
              LEFT OUTER JOIN DictionaryRefresh r 
                           ON r.RecordNumber = e.RecordNumber
                          AND r.EventKey = e.EventKey
                          AND r.MappedLookupKey = 'M18E2I501'
             WHERE r.RefreshTime < ISNULL(e.EditTime, '1 Jan 1900 01:00')
                OR r.RecordNumber IS NULL
         )
BEGIN
    PRINT 'TRUE'
END

首先要注意的是,您在WHERE 子句中使用了r.RefreshTime。由于 &lt; 运算符仅在左侧为 DEFINED 且小于右侧时才返回 true,这意味着每次 r.RefreshTime 为 NULL 时,都会跳过该记录。然而,下一行你显然提到你想要r.RecordNumber 为 NULL 的所有记录,这只会在值实际上为 NULL 或LEFT OUTER JOIN 找不到匹配项时发生。所以这里有点冲突。要么你想做一个INNER JOIN,要么你真的想要一个OUTER JOIN,但需要将r.RefreshTime &lt; d.EditTime移动到JOIN ON子句。

现在,看看您的表定义,我认为还有一些改进的余地。按照您在Events 表上方给出的解释,是所有数据的“来源”。它会随着时间的推移而附加,然后偶尔运行一个扫描“新”和“更新”记录的进程,执行一些魔术,然后将 DictionorayRefresh(UPDATE 现有记录更新为新的 RefreshTime 和 @987654337 @新人为

[dbo].[事件]

[EditTime] 被定义为 NUL-able。也许您认为 NULL 是“记录已插入但从未更新”?在这种情况下,我宁愿使用 '1 jan 1900' 作为“魔术”值,并使该字段不可为空,这样以后的生活会更轻松。

[dbo].[字典刷新]

我想知道为什么您希望RecordNumber 可以为NULL?不应该一直填,不然记录有什么用? 您还应该在指向Events 表的字段上放置FOREIGN KEY,这样服务器就知道所有值都来自那里 RefreshTime 也被定义为 NULL-able,我想你会希望它始终被填写。否则记录是如何进入表格的? 很确定你想要MappedLookupKey,但这并不重要。

无论如何,回到查询。您要弄清楚的是,Events 中的记录是否在DictionaryRefresh 中与给定的MappedLookupKey 匹配记录,并且EditTime 比相应的RefreshTime 更新。或者,那根本就没有这样的记录(对于这个MappedLookupKey

我个人会这样写:

IF EXISTS ( SELECT *
              FROM Events e
             WHERE NOT EXISTS ( SELECT *
                                  FROM DictionaryRefresh r 
                                 WHERE r.RecordNumber = e.RecordNumber
                                   AND r.EventKey = e.EventKey
                                   AND r.MappedLookupKey = 'M18E2I501'
                                   AND r.RefreshTime >= e.EditTime )

         )
BEGIN
    PRINT 'TRUE'
END

为了快速完成这项工作,您需要以下索引:

CREATE INDEX idx1 ON DictionaryRefresh  (MappedLookupKey, RecordNumber, EventKey, RefreshTime)

Events 表上,我认为PK 可以...

有趣的事实:您的JOIN 同时使用RecordNumberEventKey(同样是一个可以为NULL 的字段,可能没有充分的理由)。但是,我们已经知道 RecordNumber 唯一标识 [Events] 中的一条记录(它是 PK !),所以如果您只加入 RecordNumber ,那实际上应该这样做,除非您可以在其中有不同的 EventKeyDictonaryRefresh ?这对我来说没有意义……事实上,DictionaryRefresh 中似乎并不真正需要该字段,因为它首先可以在Events 中找到。如果该假设正确,您可以将其从表中删除,从而JOIN 再次加快速度。

有点长篇大论,希望我没有搞砸太多 =)

【讨论】:

哇。谢谢。我需要一点时间来消化。非常有帮助,尽管再次感谢。【参考方案2】:
CREATE NONCLUSTERED INDEX ix1
    ON dbo.DictionaryRefresh (RecordNumber, EventKey, MappedLookupKey, RefreshTime)

CREATE NONCLUSTERED INDEX ix2
    ON dbo.[Events] (RecordNumber, EventKey, EditTime)

IF EXISTS (
    SELECT TOP(1) 1
    FROM dbo.[Events] e /*WITH(INDEX(ix2))*/
    LEFT JOIN dbo.DictionaryRefresh r /*WITH(INDEX(ix1))*/ ON r.RecordNumber = e.RecordNumber
        AND r.EventKey = e.EventKey
        AND r.MappedLookupKey = 'M18E2I501'
    WHERE (r.RefreshTime < e.EditTime AND e.EditTime IS NOT NULL)
        OR r.RecordNumber IS NULL
)
BEGIN
    PRINT 'TRUE'
END

【讨论】:

非常感谢。那么,您会在每次要运行查询时重新创建索引,还是只检查是否存在,如果不存在则创建?你能否解释一下你为什么这样做。再次感谢 不需要重新创建索引...我只是更改索引列的顺序。这有帮助吗? 初步测试似乎表明它有帮助......需要再测试一下......但是再次感谢vm......很快就会回复你 说实话,我建议不要使用 INDEX 提示。如果索引有用,那么查询优化器会找到并使用它们;否则它可能会更喜欢另一个索引和/或方法。就像现在一样,您基本上使优化器无法提出更好的计划。 (现在可能是最佳状态,将来可能会保持这种状态......或者可能不会......为什么要冒险?) 我建议先为您的数据添加唯一键,然后再进一步。您的查询并不是那么复杂,我无法想象它会使查询优化器如此困惑。我将开始一个新的答案类型一些建议。

以上是关于优化涉及 IF EXISTS 的 SQL的主要内容,如果未能解决你的问题,请参考以下文章

面试被问之-----sql优化中in与exists的区别

sql优化,如何将in换为exists

IF EXISTS before INSERT, UPDATE, DELETE 优化

说说对SQL 语句优化有哪些方法?

SQL优化案例之exists中套or not exists

SQL优化案例之exists中套or not exists