在 where 子句中使用 DATEADD 的 TSQL 性能问题

Posted

技术标签:

【中文标题】在 where 子句中使用 DATEADD 的 TSQL 性能问题【英文标题】:TSQL Performance issues using DATEADD in where clause 【发布时间】:2011-09-19 18:57:05 【问题描述】:

我有一个使用 DATEADD 方法的查询,这需要很长时间。 我会尽量简化我们的工作。 我们正在监控温度,每 5 分钟我们将最高温度和最低温度存储在 表A

日期          |时间      |最高温度||最低温度 2011-09-18 | 12:05:00 | 38.15        | 38.099 2011-09-18 | 12:10:00 | 38.20        | 38.10 2011-09-18 | 12:15:00 | 38.22        | 38.17 2011-09-18 | 12:20:00 | 38.21        | 38.20 ... 2011-09-19 | 11:50:00 | 38.17        | 38.10 2011-09-19 | 12:55:00 | 38.32        | 38.27 2011-09-19 | 12:00:00 | 38.30        | 38.20

日期/时间列的类型是日期/时间(而不是日期时间)

在另一个表(表 B)中,我们存储了一整天的一些数据,其中一天是从中午(12PM)到中午(不是午夜到午夜)。

所以表 B 列包括: 日期(只有日期没有时间) 班次经理 MaxTemp(这是从该日期中午到第二天中午的整个 24 小时的最高温度) 最低温度

我得到包含所有数据的表 B,只需要使用表 A 更新 MaxTemp 和 MinTemp 例如:对于 2011 年 9 月 18 日,我需要 2011 年 9 月 18 日中午 12 点到 2011 年 9 月 19 日中午 12 点之间的最大温度读数。 在我们上面的 TableA 示例中,返回结果将为 38.32,因为它是所需时间段的 MAX(MaxTemp)。

我正在使用的 SQL:

update TableB
set MaxTemp = (
select MAX(HighTemp) from TableA
where
(Date=TableB.Date and Time > '12:00:00') 
or 
(Date=DATEADD(dd,1,TableB.Date) and Time <= '12:00:00')
)

而且这需要很多时间(如果我删除 DATEADD 方法会很快)。

这是一个简化的示例,显示了我拥有的数据和预期的结果:

DECLARE @TableA TABLE ([Date] DATE, [Time] TIME(0), HighTemp DECIMAL(6,2));
DECLARE @TableB TABLE ([Date] DATE, MaxTemp DECIMAL(6,2));

INSERT @TableA VALUES
('2011-09-18','12:05:00',38.15),
('2011-09-18','12:10:00',38.20),
('2011-09-18','12:15:00',38.22),
('2011-09-19','11:50:00',38.17),
('2011-09-19','11:55:00',38.32),
('2011-09-19','12:00:00',38.31),
('2011-09-19','12:05:00',38.33),
('2011-09-19','12:10:00',38.40),
('2011-09-19','12:15:00',38.12),
('2011-09-20','11:50:00',38.27),
('2011-09-20','11:55:00',38.42),
('2011-09-20','12:00:00',38.16);

INSERT @TableB VALUES
('2011-09-18', 0),
('2011-09-19', 0);

-- This is how I get the data, now I just need to update the max temp for each day

with TableB(d, maxt) as
(
select * from @TableB
)
update TableB
set maxt = ( 
select MAX(HighTemp) from @TableA 
where
(Date=TableB.d and Time > '12:00:00')
or
(Date=DATEADD(dd,1,TableB.d) and Time <= '12:00:00')
)

select * from @TableB

希望我能够解释自己,有什么想法可以做不同的吗?谢谢!

【问题讨论】:

大括号将格式化您的代码。提示:4 个前导空格... TableB.Date 是否使用默认的午夜时间? 【参考方案1】:

列上的函数通常会影响性能。 OR也可以。

但是,我假设您想要 AND 而不是 OR,因为它是一个范围。

所以,应用一些逻辑并只进行一次计算

update TableB
 set MaxTemp =
 (
    select MAX(HighTemp) from TableA
    where
    (Date + Time - 0.5 = TableB.Date)
 )

(Date + Time - 0.5) 将中午到中午更改为午夜到午夜(0.5 = 12 小时)。更重要的是,您可以将其设为computed column and index it

更准确地说,Date + Time - 0.5DATEADD(hour, -12, Date+Time) 假设 DateTime 是真实的日期/时间,而不是 varchar...

编辑:这个答案是错误的,但我会将其保留为“不该做什么”

查看更多:

Bad Habits to Kick : Using shorthand with date/time operations

【讨论】:

注意DATEADD 的简写。如果任何一列是新的日期/时间数据类型(DATE/TIME/DATETIME2/DATETIMIEOFFSET),它将失败并显示Msg 8117, Level 16, State 1, Line 1 - Operand data type date is invalid for add operator. 是的,没错。只是试图改变人们的习惯,因为如果/当他们迁移到新数据类型时代码会中断,或者有人出现并且已经在使用新类型,他们可能不理解错误。如果你拼出DATEADD,它可以在任何地方使用。 我假设表 B 没有时间分量,但表 A 有。这只会匹配 TableB 上中午时间的阅读。我认为。 @BalamBalam:TableB 有一个日期,OP 想要将日期+时间“四舍五入”到最近的午夜 @GNB 只是一个健康的问题,我可能是错的,但我对此进行了测试。是的,0.5 到了中午,但它与 TableA 中时间为上午 10.21 或下午 2:53 的行不匹配。它只匹配时间正好是中午的行。我假设 TableB 正在记录实际时间。在他的慢查询中,条件是一个日期范围。【参考方案2】:

如果您使用单个 SMALLDATETIME 列而不是将这些数据分成 DATE/TIME 列,这可能会容易得多。此外,我假设您使用的是 SQL Server 2008,而不是之前的版本,您将 DATE/TIME 数据存储为字符串。请指定 SQL Server 的版本和实际使用的数据类型。

DECLARE @d TABLE ([Date] DATE, [Time] TIME(0), MaxTemp DECIMAL(6,3), MinTemp DECIMAL(6,3));

INSERT @d VALUES
('2011-09-18','12:05:00',38.15,38.099),
('2011-09-18','12:10:00',38.20,38.10),
('2011-09-18','12:15:00',38.22,38.17),
('2011-09-18','12:20:00',38.21,38.20),
('2011-09-19','11:50:00',38.17,38.10),
('2011-09-19','12:55:00',38.32,38.27),
('2011-09-19','12:00:00',38.30,38.20);

SELECT '-- before update';
SELECT * FROM @d;

;WITH d(d,t,dtr,maxt) AS
(
    SELECT [Date], [Time], DATEADD(HOUR, -12, CONVERT(SMALLDATETIME, CONVERT(CHAR(8), 
        [Date], 112) + ' ' + CONVERT(CHAR(8), [Time], 108))), MaxTemp FROM @d 
),
d2(dtr, maxt) AS 
(
    SELECT CONVERT([Date], dtr), MAX(maxt) FROM d
    GROUP BY CONVERT([Date], dtr)
)
UPDATE d SET maxt = d2.maxt FROM d
    INNER JOIN d2 ON d.dtr >= d2.dtr AND d.dtr < DATEADD(DAY, 1, d2.dtr);

SELECT '-- after update';
SELECT * FROM @d;

结果:

-- before update

2011-09-18  12:05:00    38.150  38.099
2011-09-18  12:10:00    38.200  38.100
2011-09-18  12:15:00    38.220  38.170
2011-09-18  12:20:00    38.210  38.200
2011-09-19  11:50:00    38.170  38.100
2011-09-19  12:55:00    38.320  38.270
2011-09-19  12:00:00    38.300  38.200

-- after update

2011-09-18  12:05:00    38.220  38.099
2011-09-18  12:10:00    38.220  38.100
2011-09-18  12:15:00    38.220  38.170
2011-09-18  12:20:00    38.220  38.200
2011-09-19  11:50:00    38.220  38.100
2011-09-19  12:55:00    38.320  38.270
2011-09-19  12:00:00    38.320  38.200

大概你也想更新 MinTemp,那就是:

;WITH d(d,t,dtr,maxt,mint) AS
(
    SELECT [Date], [Time], DATEADD(HOUR, -12,
         CONVERT(SMALLDATETIME, CONVERT(CHAR(8), [Date], 112) 
         + ' ' + CONVERT(CHAR(8), [Time], 108))), MaxTemp, MaxTemp
    FROM @d 
),
d2(dtr, maxt, mint) AS 
(
    SELECT CONVERT([Date], dtr), MAX(maxt), MIN(mint) FROM d
    GROUP BY CONVERT([Date], dtr)
)
UPDATE d
    SET maxt = d2.maxt, mint = d2.maxt
    FROM d
    INNER JOIN d2
        ON d.dtr >= d2.dtr
        AND d.dtr < DATEADD(DAY, 1, d2.dtr);

现在,这并不比您现有的查询更好,因为它仍将使用扫描来确定聚合和所有需要更新的行。我并不是说你应该更新表格,因为这些信息总是可以在查询时得到,但如果这是你真正想做的事情,我会结合这些建议答案并考虑修改架构。例如,如果架构是:

USE [tempdb];
GO

CREATE TABLE dbo.d
(
    [Date] SMALLDATETIME, 
    MaxTemp DECIMAL(6,3), 
    MinTemp DECIMAL(6,3),
    RoundedDate AS (CONVERT(DATE, DATEADD(HOUR, -12, [Date]))) PERSISTED
);

CREATE INDEX rd ON dbo.d(RoundedDate);

INSERT dbo.d([Date],MaxTemp,MinTemp) VALUES
('2011-09-18 12:05:00',38.15,38.099),
('2011-09-18 12:10:00',38.20,38.10),
('2011-09-18 12:15:00',38.22,38.17),
('2011-09-18 12:20:00',38.21,38.20),
('2011-09-19 11:50:00',38.17,38.10),
('2011-09-19 12:55:00',38.32,38.27),
('2011-09-19 12:00:00',38.30,38.20);

那你的更新就这么简单,计划也好很多:

;WITH g(RoundedDate,MaxTemp)
AS
(
    SELECT RoundedDate, MAX(MaxTemp)
        FROM dbo.d
        GROUP BY RoundedDate
)
UPDATE d
    SET MaxTemp = g.MaxTemp
    FROM dbo.d AS d
    INNER JOIN g
    ON d.RoundedDate = g.RoundedDate;

最后,您现有的查询可能需要这么长时间的原因之一是您每次都在更新。上周的数据有变化吗?可能不是。那么为什么不将WHERE 子句仅限于最近的数据呢?我认为没有必要比昨天更早地重新计算任何东西,除非你不断收到关于上周二中午天气温暖程度的修正估计。那么为什么当前查询中没有 WHERE 子句来限制它尝试执行此工作的日期范围?您真的想每次都更新整个功能吗?这可能是您应该每天只在下午某个时间执行一次的操作,以便昨天更新。因此,需要 2 秒还是 2.5 秒并不重要。

【讨论】:

我看不出你在做什么来得到那个错误。你能在某处显示代码吗?我上面列出的代码已经过测试,我没有看到我在任何地方使用 HOUR 来对抗 DATE(除非你更改了它)。 对不起,这是我的错,我没有注意到 smalldatetime 类型。我喜欢 RoundDate 的想法,它工作得很好,并且对其他查询也有帮助。我确实需要一个单独的日期和时间字段,但我设法创建了这样的计算字段: [RoundedDay] AS (CONVERT([date],dateadd(hour,-12,CONVERT([datetime],[Date],0) +CONVERT([datetime],[Time],0)),0)) PERSISTED 你怎么看? 您可能希望使用DATEADD(MINUTE, -765,,基于我根据您的更新尝试的一些进一步查询。此外,您的公式似乎不正确...您尝试过吗?似乎 RoundedDay 是中午行的错误日期。如果您使用集群的 startdatetime 列而不是单独的日期/时间列开始,它可能会更有效率。这些数据类型有它们的位置,但在您的情况下似乎没有好处。 我试过了,看起来不错。不要担心要使用的确切数学,我会弄清楚...我认为我确实需要单独的日期和时间列,因为我有很多查询,我只按时间过滤,并按日期分组。就像每天早上 7 点到 10 点之间的平均最高温度等。我认为如果我有一个专门的时间字段,这样做会更容易,不是吗? 从单个 smalldatetime 列(使用或不使用计算列)派生日期和时间可能比仅将不同列中的日期/时间信息组合起来做同样的事情更容易。该列上的索引(包括根据定义包含它的非聚集索引,如果它是聚集索引)也将更精简。【参考方案3】:

您可能需要根据日期使用 -12 作为中午到中午内部的开始日期或结束日期。

    update tableA 
    set tableAx.MaxTemp = MAX(TableB.HighTemp)
    from tableA as tableAx
    join TableB 
    on tableAx.Date = CAST(DATEADD(hh,12,TableB.[Date]+TableB.[Time]) as Date) 
    group by tableAx.Date 

由于 12 小时的偏移量,不确定将 TableB 日期加时间直接放在 DateTime 字段中会获得多少收益。无法摆脱 DATEADD 并且即使进入函数的参数已编入索引,函数的输出也不会编入索引。您可能能够创建一个计算列,该列 = date + time +/- 12h 并索引该列。

就像 Arron 的建议一样,只更新那些没有价值的人。

    update tableA 
    set tableAx.MaxTemp = MAX(TableB.HighTemp)
    from tableA as tableAx
    join TableB 
    on tableAx.Date = CAST(DATEADD(hh,12,TableB.[Date]+TableB.[Time]) as Date) 
    where tableAx.MaxTemp is null 
    group by tableAx.Date

或插入新日期

    insert into tableA (date, MaxTemp) 
    select CAST(DATEADD(hh,12,TableB.[Date]+TableB.[Time]), as Date) as [date] , MAX(TableB.HighTemp) as [MaxTemp]
    from tableA as tableAx
    right outer join TableB 
    on tableAx.Date = CAST(DATEADD(hh,12,TableB.[Date]+TableB.[Time]) as Date) 
    where TableB.Date is null 
    group by CAST(DATEADD(hh,12,TableB.[Date]+TableB.[Time]) as Date)

【讨论】:

以上是关于在 where 子句中使用 DATEADD 的 TSQL 性能问题的主要内容,如果未能解决你的问题,请参考以下文章

添加 Where 子句后列名无效[重复]

Excel SQL - Where 子句语法不正确

在t-sql中,子查询只能放在where子句中吗

关于 Where 子句的 T-Sql 多重标准

使用 Func<T, string> lambda 动态构造 where 子句 - linq 到实体

SQL Selects combine - 第二个选择在 Where 子句中