获取上次更改值时的日期时间的高性能查询

Posted

技术标签:

【中文标题】获取上次更改值时的日期时间的高性能查询【英文标题】:High performance query to get datetime when value was last changed 【发布时间】:2019-11-28 14:40:25 【问题描述】:

我正在处理的数据

考虑以下 2 个数据库表:

CREATE TABLE [dbo].[Contact](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [Contact_UID] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Contact_Contact_UID]  DEFAULT (newsequentialid()),
    [Name] [nvarchar](255) NOT NULL,
    [ContactStatus] [nvarchar](255) NOT NULL)

CREATE TABLE [dbo].[Contact_Log](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [LogDate] [datetimeoffset](7) NOT NULL CONSTRAINT [DF_Contact_Log_LogDate]  DEFAULT (sysdatetimeoffset()),
    [Contact_UID] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Contact_Log_Contact_UID]  DEFAULT (newsequentialid()),
    [Name] [nvarchar](255) NOT NULL,
    [ContactStatus] [nvarchar](255) NOT NULL)

联系人表是联系人记录的主表。它存储联系人的姓名和状态(例如“Alive”、“Dead”或其他)。

Contact_Log 表存储对 Contact 表所做的所有更改。

所以这里有一些示例数据:

联系方式:

+----+--------------------------------------+------+---------------+
| ID | Contact_UID                          | Name | ContactStatus |
+----+--------------------------------------+------+---------------+
| 1  | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Dead          |
+----+--------------------------------------+------+---------------+
| 2  | F7844037-2FF5-47B9-874D-C0920E7DC092 | Jane | Alive         |
+----+--------------------------------------+------+---------------+

联系日志:

+----+--------------------------------------+------+---------------+------------+
| ID | Contact_UID                          | Name | ContactStatus | LogDate    |
+----+--------------------------------------+------+---------------+------------+
| 1  | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Alive         | 2019-01-01 |
+----+--------------------------------------+------+---------------+------------+
| 2  | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Dead          | 2019-01-02 |
+----+--------------------------------------+------+---------------+------------+
| 3  | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Dead          | 2019-01-03 |
+----+--------------------------------------+------+---------------+------------+
| 4  | F7844037-2FF5-47B9-874D-C0920E7DC092 | Jane | Alive         | 2019-01-04 |
+----+--------------------------------------+------+---------------+------------+

注意:此时我还没有在那些表上添加任何索引或类似的东西。

测试场景

以上只是一些示例数据。我正在测试的数据具有以下行数:

联系人:~10,000 行

Contact_Log:~3,000,000 行

我目前正在使用 SQL Server 2008 R2 进行测试。因此,首选在该版本及以后版本中受支持的解决方案。

我正在努力实现的目标

基本上,我正在尝试制定一个查询,该查询可以告诉我 LogDate 上次更改 ContactStatus 字段的时间,对于特定的 Contact_UID,取自 Contact_Log 表。

例如,如果我感兴趣的记录是“John”,那么结果应该是“2019-01-02”。因为这是 John 的 ContactStatus 上次更改的日期(即从“Alive”更改为“Dead”)。

最后,我想把这个查询放到一个函数中。一个可以通过传入 Contact_UID 和我要检查的字段的名称来调用的函数。然后可以将此函数作为更一般查询的一部分调用。例如:

SELECT Name, MyFunction('62918AC1-1C6C-4DEB-B7F8-5D5EF913F667', 'ContactStatus') AS StatusLastChanged FROM Contact

到目前为止我已经尝试过什么

嗯,我已经尝试了一些方法,虽然我可以得到我想要的结果。我的尝试确实在与性能问题作斗争。

注意:虽然我真的只想要一个 datetimeoffset 结果。一些尝试包括更多数据/字段,只是为了尝试验证数据是否准确。

尝试 1:

SELECT TOP(1) a.LogDate
FROM Contact_Log AS a
WHERE a.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667' 
AND a.ContactStatus <>
(
SELECT TOP(1) b.ContactStatus
FROM Contact_Log AS b
WHERE b.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
AND a.LogDate > b.LogDate
ORDER BY b.LogDate DESC
)
ORDER BY LogDate DESC

问题 1:太慢了。在等待近一个小时没有结果后,我不得不停止查询。

尝试 2:

SELECT A.LogDate
FROM (SELECT ROW_NUMBER() OVER (ORDER BY LogDate DESC, ID DESC) AS rnum, ID, LogDate, Contact_UID, ContactStatus FROM Contact_Log) A
LEFT JOIN (SELECT ROW_NUMBER() OVER (ORDER BY LogDate DESC, ID DESC) AS rnum, ID, LogDate, Contact_UID, ContactStatus FROM Contact_Log) B
ON A.rnum = B.rnum-1
WHERE 
(B.rnum IS NULL
OR (A.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667' 
AND B.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
AND A.ContactStatus != B.ContactStatus))
ORDER BY A.rnum

问题 2:这行得通,并为我提供了正确的数据集。但是,它需要 6 秒,这太慢了。请记住,它需要在更一般的查询(大约 10,000 行)中作为函数工作。

尝试 3:现在这与尝试 2 基本相同,希望我尝试应用 TOP(1) 以便获得我真正想要的结果。

SELECT TOP(1) A.LogDate
FROM (SELECT ROW_NUMBER() OVER (ORDER BY LogDate DESC, ID DESC) AS rnum, ID, LogDate, Contact_UID, ContactStatus FROM Contact_Log) A
LEFT JOIN (SELECT ROW_NUMBER() OVER (ORDER BY LogDate DESC, ID DESC) AS rnum, ID, LogDate, Contact_UID, ContactStatus FROM Contact_Log) B
ON A.rnum = B.rnum-1
WHERE 
(B.rnum IS NULL
OR (A.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667' 
AND B.Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
AND A.ContactStatus != B.ContactStatus))
ORDER BY A.rnum

问题 3:令我惊讶的是,这比尝试 2 花费的时间要长得多,尽管我所做的只是在开始时添加 TOP(1)。这花了 5 多分钟,所以我停止了查询并放弃了。

问题

我怎样才能在“我正在努力实现的目标”中做我想做的事情,但又能获得合理的性能? (我很乐意在这个阶段将它控制在 1 秒以内)。

请记住,我只想要一个 datetimeoffset 作为结果,以便可以在函数中使用它。

到目前为止,我还没有创建特定的索引。如果无法改进查询,我很乐意将这些建议视为合适的答案。或对架构进行任何适当的更改。

底线

我正在寻找一个会产生 1 个结果的查询,其中包含 1 个 datetimeoffset 字段。运行时间不到 1 秒。

【问题讨论】:

什么是MyFunction?因此,您是否使用 SQL Server 2019? MyFunction 将是我最终创建的函数,它返回我试图获得的结果。我正在使用 SQL Server 2008 R2 进行测试。 是否必须只针对一个 id?或者您想在一个查询中获取多条记录的日期? @SalmanA:该函数采用 Contact_UID(唯一标识符)并返回单个 datetimeoffset 结果。 表上有哪些索引定义? 【参考方案1】:

您希望选择紧接在不等于当前 ContactStatus 的最高日期之后的最小日期。应该是这样的:

select
min(LogDate)
from Contact_Log
where
Contact_UID='62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
and ContactStatus = (
        select top 1
        ContactStatus
        from Contact_Log where
        Contact_UID='62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
        order by Log_Date desc
        )
and LogDate > (
    select max(LogDate)
    from Contact_Log
    where Contact_UID='62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
    and ContactStatus != (
        select top 1
        ContactStatus
        from Contact_Log where
        Contact_UID='62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
        order by Log_Date desc
        )
    );

【讨论】:

如果可能,我需要尝试在不使用联系人表的情况下实现这一目标。 我也不知道当前值是多少。应该从 Contact_Log 表中确定当前值是多少。话虽如此,我测试了您的查询,而且速度很快。我只是不知道要在查询中使用的当前状态 对不起,sql-server 不是我的默认 SQL 谢谢。它似乎有效,其他答案也是如此。我会做更多的测试,并以最快的为准。【参考方案2】:

这是基于您最初的尝试:

SELECT ca3.LogDate
FROM (
    -- find last status
    SELECT TOP 1 *
    FROM Contact_Log
    WHERE Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
    ORDER BY LogDate DESC, ID DESC
) AS ca1
CROSS APPLY (
    -- find date when status changed
    SELECT TOP 1 *
    FROM Contact_Log
    WHERE Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
    AND ContactStatus <> ca1.ContactStatus
    ORDER BY LogDate DESC, ID DESC
) AS ca2
CROSS APPLY (
    -- find next date
    SELECT TOP 1 *
    FROM Contact_Log
    WHERE Contact_UID = '62918AC1-1C6C-4DEB-B7F8-5D5EF913F667'
    AND (LogDate = ca2.LogDate AND ID > ca2.ID
         OR LogDate > ca2.LogDate)
    ORDER BY LogDate, ID
) AS ca3

这个查询应该受益于由Contact_UID, LogDate, ID, Status组成的索引

【讨论】:

谢谢。我似乎得到了错误的结果,但它非常接近。正如您预测的那样,我的测试数据确实有重复的日期。我尝试在其中添加 ID,但得到相同的结果。您能否编辑答案以包含适用于重复日期的解决方案。这样我就可以检查我的测试是否正确,因为它可能只是我自己 别担心。我编辑的测试数据确实避免了重复的日期。您的解决方案效果很好。仅供参考,我认为问题在于尝试“查找下一个日期”时重复日期,因为 LogDate &gt; ca2.LogDate 部分最终也可能从更改之前获取值(因为它们具有相同的时间)。在实践中它不会发生,所以一切都很好。 其他答案似乎更快。我现在会赞成这两个答案都是正确的,并会在我完成一些更彻底的测试后奖励最快的答案。让我知道你是否可以加快你的速度;) 查看修改后的答案,这是我的想法,我没有测试边缘情况。索引应该有助于提高速度,这个和其他查询。

以上是关于获取上次更改值时的日期时间的高性能查询的主要内容,如果未能解决你的问题,请参考以下文章

Oracle/SQL:将日期和时间连接成单个日期值时的数字格式模型无效

SQL查询返回列更改时的最新日期

Django:获取上次用户访问日期

PHP Mysql获取表上次更新时间和日期[重复]

phpmyadmin 在更改任何值时更改日期

Winreg Python,QueryInfoKey 为上次更改提供不正确的日期/时间?