【问题标题】:How can I improve the speed of this SQL query?如何提高此 SQL 查询的速度?
【发布时间】:2015-02-03 21:29:57
【问题描述】:

我们有两台设备以大约 30 秒的间隔收集数据。这些设备位于两个相距较远的地点。每个站点的每次收集的绝对时间可以变化 +/- 30 秒。有时,网站会因各种原因下线。每个设备的数据代表不同类型的测量,例如来自设备 1 的温度和来自设备 2 的湿度。一个进程将来自 device1 和 device2 的数据记录到 SQL Server 2012 Express 数据库中的单独表中,该数据库在与每个设备分开的服务器上运行。

希望将来自两个设备的数据关联到记录中,这些记录将包含具有特定日期/时间的 site1 值的列,如果有任何可用的数据,则结合 site2 的数据。然后,用户程序将请求指定日期/时间范围的记录集。为此,我构建了以下SP:

ALTER PROCEDURE [db_datareader].[DataJoinDateRange] 
@DateFrom DateTime = '2014-05-15 15:10:24.000', 
@DateTo DateTime = '2014-06-15 15:10:24.000' 
AS
BEGIN
SET NOCOUNT ON;
WITH site1(id, date_time, dataval)
AS
(
    SELECT * 
    FROM site1_data 
    WHERE site1_data.date_time BETWEEN @DateFrom AND @DateTo
),
site2(id, date_time, datavaql)
AS
(
    SELECT *
    FROM site2_data
    WHERE site2_data.date_time BETWEEN @DateFrom AND @DateTo
)
SELECT * from site1 site1_res
INNER JOIN (select id, date_time, data_val) site2_res
on ABS(DATEDIFF("SECOND", site1_res.date_time, site_2_res.date_time)) < 30
END

目的是首先选择所需日期/时间范围内的记录,然后将站点 1 中的记录连接到站点 2 中在 +/- 30 秒内的记录。范围。结果记录集将包含来自两个设备的数据,或者在不存在相应记录时为空。

这似乎可行:具有所需形式的记录被输出并对应于每个表中的正确记录。但是执行速度很慢。在几周的日期范围内进行查询大约需要 1 分 30 秒。 Site1 在此日期范围内包含大约 5000 条记录,而 Site2 仅包含 1 条记录。仅针对每个表的日期范围的 SELECT 查询将在不到一秒的时间内执行。

我以前从未深入研究过 SQL,但我们这个小组目前没有其他人来完成这项任务。谁能告诉我正确的方法来做到这一点,或者至少如何加速这个 SP?

【问题讨论】:

  • 您能否在加载数据时编辑表中的 date_time 字段,使其始终为 :00 或 :30 或为此添加一个新字段(日期时间或整数),而不是计算 datediff?
  • 当我想加快查询速度时,我总是求助于索引,看看是否可以为手头的任务创建索引。您还可以创建跨表的索引。
  • 您的子查询在查询中没有表。然后你有 nonSARGable 谓词,因为你有你的列在一个函数中。最后但并非最不重要的是,您有一个三角形连接。 sqlservercentral.com/articles/T-SQL/61539
  • JamesZ,数据上的时间戳本质上是可变的,我必须从 site2 中找到与给定 site1 记录中的时间戳“足够接近”的记录(如果有的话)。所以我总是需要做一些计算来确定来自 site2 的哪些(如果有的话)记录是合适的。
  • Reenactor Rob - 我确实为两个表中的 date_time 字段创建了索引。创建一个结合两个表的索引听起来很有趣,我将不得不找出如何做到这一点并尝试一下。

标签: sql sql-server sql-server-2008 sql-server-2012 sql-server-2008r2-express


【解决方案1】:

您可以尝试通过更好地利用date_time 列上的索引来改进您的解决方案。

ABS(S1 - S2) < 30

等价于

ABS(S2 - S1) < 30

<=>

-30 < S2 - S1 < 30

<=>

S2 - S1 < 30
AND
S2 - S1 > -30

<=>

S2 < S1 + 30
AND
S2 > S1 - 30

您并不真正需要第一个 CTE,尽管它不应该受到伤害。但是,CROSS APPLY 中的 WHERE 子句最好这样写。此外,如果您想查看来自site1 的数据,您应该使用OUTER APPLY 而不是CROSS APPLY,它没有来自site2 的任何相应数据。现在site2.date_time 不在函数调用内部,优化器可以在该列上使用索引。

ALTER PROCEDURE [dbo].[SPJoinDateRange]
    @DateFrom DateTime = '2014-05-01 15:10:24.000', 
    @DateTo   DateTime = '2014-07-31 15:10:00.000'
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        site1_data.id AS id1
        ,site1_data.date_time AS date_time1
        ,site1_data.data_val1
        ,CA_site2.id2
        ,CA_site2.date_time2
        ,CA_site2.data_val2
    FROM
        site1_data
        OUTER APPLY
        (
            SELECT
                site2_data.id as id2
                ,site2_data.date_time as date_time2
                ,site2_data.data_val2
            FROM
                site2_data
            WHERE
                site2.date_time BETWEEN @DateFrom AND @DateTo
                AND site2.date_time < DATEADD(second, +30, site1_data.date_time)
                AND site2.date_time > DATEADD(second, -30, site1_data.date_time)
        ) AS CA_site2
    WHERE
        site1_data.date_time BETWEEN @DateFrom AND @DateTo
    ;

END

如果您可以添加一个包含四舍五入到最接近 30 秒的时间戳的额外列,它将运行得更快。如果您不需要精确的时间戳,或者将现有值四舍五入。

如果我们添加一个名为 date_time_rounded 的列,其中包含四舍五入为 30 秒的原始时间戳,在其上创建索引,则查询将如下所示:

ALTER PROCEDURE [dbo].[SPJoinDateRange]
    @DateFrom DateTime = '2014-05-01 15:10:24.000', 
    @DateTo   DateTime = '2014-07-31 15:10:00.000'
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        site1_data.id AS id1
        ,site1_data.date_time AS date_time1
        ,site1_data.data_val1
        ,site2_data.id AS id2
        ,site2_data.date_time AS date_time2
        ,site2_data.data_val2
    FROM
        site1_data
        LEFT JOIN site2_data ON site2_data.date_time_rounded = site1_data.date_time_rounded
    WHERE
        site1_data.date_time BETWEEN @DateFrom AND @DateTo
    ;

END

要将date_time 舍入到最接近的 30 秒,您可以使用以下内容:

DATEADD(second, 30 * ROUND(DATEDIFF(second, '20010101', date_time)/30.0, 0), '20010101')

它计算从2001-01-01 到给定date_time 的秒数,将它们除以30,将结果四舍五入为整数,将结果乘以30,将此秒数添加到2001-01-01

运行几次,看看它是如何工作的:

SELECT 
GETDATE() as original, 
DATEADD(second, 30 * ROUND(DATEDIFF(second, '20010101', GETDATE())/30.0, 0), '20010101') AS rounded

【讨论】:

    【解决方案2】:

    我在其他地方找到了this article,这很有帮助。因此,我将 SP 更改为以下内容:

    ALTER PROCEDURE [dbo].[SPJoinDateRange]
    @DateFrom DateTime = '2014-05-01 15:10:24.000', 
    @DateTo   DateTime = '2014-07-31 15:10:00.000'
    AS
    BEGIN
    SET NOCOUNT ON;
    WITH site1(id, date_time, data_val1)
    AS
    (
        SELECT * 
        FROM site1_data 
        WHERE site1_data.date_time BETWEEN @DateFrom AND @DateTo
    )
    SELECT * FROM site1
    CROSS APPLY 
    (
        SELECT id as id1, date_time as date_time1, data_val2
        FROM site2_data AS site2
        WHERE site2.date_time BETWEEN @DateFrom AND @DateTo 
        AND 
        ABS(DATEDIFF("SECOND", site1.date_time, site2.date_time)) < 30
    ) 
    AS result
    END
    

    此查询的结果时间为 6 秒。 (与之前版本的 90 秒相比。)这可能仍然比可能的慢得多;理想情况下,我的下一个任务是了解为什么这种方法更快。 Sean Lange 简洁的回答(和链接)无疑提供了一些线索。当然,我将不得不推迟它并继续我们初始实施中的下一个任务。

    感谢所有如此迅速地回答我的问题的人。

    【讨论】:

    • 听起来性能是一项重大改进。恭喜!我怀疑它可以做得快于 6 秒,但在不知道表结构和索引的情况下,很难说什么是最好的。一个建议是,如果您要使用 site1 cte,您应该明确命名 select 中的列,而不是使用 *。如果您的表结构发生更改,您的查询将被破坏。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-29
    • 2019-10-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多