优化涉及IF EXISTS的SQL

时间:2016-01-19 13:49:13

标签: sql-server query-optimization

我正在尝试跟踪是否需要更新某些表。我有一个事件表,我想要监视更改,并有另一个表,称为DictionaryRefresh,它跟踪对该表所做的更改。如果事件表被编辑,它的编辑时间将被保存并且晚于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中的行数增加而减慢,特别是如果没有符合条件的话。

这是执行计划。

enter image description here

和突出的统计数据(索引寻求占成本的94% - 访问的行数实际上是事件表中行数的平方)...

enter image description here

我尝试过更换

IF EXISTS 

IF (SELECT COUNT ...) <> 0

以及

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

但似乎没有更快。

我很明白你可能有任何建议。

提前致谢。

取值

2 个答案:

答案 0 :(得分:2)

稍微重新格式化您的查询我来到这里:

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

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

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

  • [DBO]。[事件]

    • [EditTime]被定义为NUL-able。也许你认为NULL是'插入记录但从未更新'?在那种情况下,我宁愿使用'1 jan 1900'作为'魔术'值并使该字段不可用,它会使以后的生活更容易。
  • [DBO]。[DictionaryRefresh]

    • 我想知道为什么你希望INSERT能够为空?不应该一直填写,否则记录的目的是什么?
    • 您还应该在指向RecordNumber表的字段上放置FOREIGN KEY,这样服务器就知道所有值都来自那里
    • Events也被定义为NULL-able,我认为你希望它总是被填写。否则记录是如何进入表中的?
    • 非常确定你想要RefreshTime,但这对此并不重要。

无论如何,回到查询。您要弄清楚的是,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同时使用JOINRecordNumber(同样是一个无法使用的字段,可能没有充分的理由)。但是,我们已经知道EventKey在[事件]中唯一标识了一条记录(它是RecordNumber!),所以如果你只是PK加入实际应该做的事情,除非你能拥有RecordNumber中不同的EventKey值?这对我来说没有意义...事实上,DictonaryRefresh中似乎并不真正需要该字段,因为它首先可以在DictionaryRefresh中找到。如果这个假设是正确的,你可以将它从表中移除,从而Events再次加快速度。

有点长时间阅读,希望我没有做太多的事情=)

答案 1 :(得分:1)

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