优化涉及 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
。由于 <
运算符仅在左侧为 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
现有记录更新为新的 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
同时使用RecordNumber
和EventKey
(同样是一个可以为NULL 的字段,可能没有充分的理由)。但是,我们已经知道 RecordNumber
唯一标识 [Events] 中的一条记录(它是 PK
!),所以如果您只加入 RecordNumber
,那实际上应该这样做,除非您可以在其中有不同的 EventKey
值DictonaryRefresh
?这对我来说没有意义……事实上,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的主要内容,如果未能解决你的问题,请参考以下文章