高性能查询以获取上次更改值时的日期时间
High performance query to get datetime when value was last changed
我正在处理的数据
考虑以下 2 个数据库 tables:
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)
联系人 table 是联系人记录的主要 table。它存储联系人的姓名和状态(例如 "Alive"、"Dead" 或其他)。
Contact_Log table 存储对联系人 table 所做的所有更改。
下面是一些示例数据:
联系人:
+----+--------------------------------------+------+---------------+
| ID | Contact_UID | Name | ContactStatus |
+----+--------------------------------------+------+---------------+
| 1 | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Dead |
+----+--------------------------------------+------+---------------+
| 2 | F7844037-2FF5-47B9-874D-C0920E7DC092 | Jane | Alive |
+----+--------------------------------------+------+---------------+
Contact_Log:
+----+--------------------------------------+------+---------------+------------+
| 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 |
+----+--------------------------------------+------+---------------+------------+
注意:此时我还没有在 tables 上添加任何索引或类似的东西。
测试场景
以上只是一些示例数据。我正在测试的数据具有以下行数:
联系人:~10,000 行
Contact_Log:~3,000,000 行
我目前正在使用 SQL Server 2008 R2 进行测试。因此,首选支持的解决方案。
我想要达到的目标
基本上我正在尝试制定一个查询,该查询可以告诉我 LogDate
上次更改 ContactStatus
字段的时间,对于特定的 Contact_UID
,取自 Contact_Log
table.
例如,如果我感兴趣的记录是"John",那么结果应该是“2019-01-02”。因为这是 John 的 ContactStatus 上次更改的日期(即它从 "Alive" 更改为 "Dead")。
最后,我想把这个查询放到一个函数中。可以通过传入 Contact_UID 和我要检查的字段名称来调用的函数。然后可以将此函数作为更一般查询的一部分进行调用。例如:
SELECT Name, MyFunction('62918AC1-1C6C-4DEB-B7F8-5D5EF913F667', 'ContactStatus') AS StatusLastChanged FROM Contact
到目前为止我尝试了什么
好吧,我已经尝试了一些东西,虽然我能得到我想要的结果。我的尝试确实遇到了性能问题。
注意:虽然我真的只想要一个 datetimeoffset 结果。一些尝试包括更多 data/fields 只是为了尝试验证数据是否准确。
尝试 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 分钟,所以我停止了查询并放弃了。
问题
我如何才能在 "What I am trying to achieve" 中做我想做的事,同时又能保持合理的性能? (我很乐意在这个阶段将其缩短到 1 秒以内)。
记住,我只想要一个 datetimeoffset 作为结果,以便它可以在函数中使用。
到目前为止我还没有创建具体的索引。如果无法改进查询,我很乐意考虑将这些建议作为 suitable 的答案。或者对架构进行任何适当的更改。
底线
我正在寻找将产生 1 个结果的查询,其中包含 1 个 datetimeoffset 字段。 运行.
需要不到1秒的时间
您想要 select 紧跟在不等于当前 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
)
);
这是基于您最初的尝试:
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
的索引
我正在处理的数据
考虑以下 2 个数据库 tables:
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)
联系人 table 是联系人记录的主要 table。它存储联系人的姓名和状态(例如 "Alive"、"Dead" 或其他)。
Contact_Log table 存储对联系人 table 所做的所有更改。
下面是一些示例数据:
联系人:
+----+--------------------------------------+------+---------------+
| ID | Contact_UID | Name | ContactStatus |
+----+--------------------------------------+------+---------------+
| 1 | 62918AC1-1C6C-4DEB-B7F8-5D5EF913F667 | John | Dead |
+----+--------------------------------------+------+---------------+
| 2 | F7844037-2FF5-47B9-874D-C0920E7DC092 | Jane | Alive |
+----+--------------------------------------+------+---------------+
Contact_Log:
+----+--------------------------------------+------+---------------+------------+
| 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 |
+----+--------------------------------------+------+---------------+------------+
注意:此时我还没有在 tables 上添加任何索引或类似的东西。
测试场景
以上只是一些示例数据。我正在测试的数据具有以下行数:
联系人:~10,000 行
Contact_Log:~3,000,000 行
我目前正在使用 SQL Server 2008 R2 进行测试。因此,首选支持的解决方案。
我想要达到的目标
基本上我正在尝试制定一个查询,该查询可以告诉我 LogDate
上次更改 ContactStatus
字段的时间,对于特定的 Contact_UID
,取自 Contact_Log
table.
例如,如果我感兴趣的记录是"John",那么结果应该是“2019-01-02”。因为这是 John 的 ContactStatus 上次更改的日期(即它从 "Alive" 更改为 "Dead")。
最后,我想把这个查询放到一个函数中。可以通过传入 Contact_UID 和我要检查的字段名称来调用的函数。然后可以将此函数作为更一般查询的一部分进行调用。例如:
SELECT Name, MyFunction('62918AC1-1C6C-4DEB-B7F8-5D5EF913F667', 'ContactStatus') AS StatusLastChanged FROM Contact
到目前为止我尝试了什么
好吧,我已经尝试了一些东西,虽然我能得到我想要的结果。我的尝试确实遇到了性能问题。
注意:虽然我真的只想要一个 datetimeoffset 结果。一些尝试包括更多 data/fields 只是为了尝试验证数据是否准确。
尝试 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 分钟,所以我停止了查询并放弃了。
问题
我如何才能在 "What I am trying to achieve" 中做我想做的事,同时又能保持合理的性能? (我很乐意在这个阶段将其缩短到 1 秒以内)。
记住,我只想要一个 datetimeoffset 作为结果,以便它可以在函数中使用。
到目前为止我还没有创建具体的索引。如果无法改进查询,我很乐意考虑将这些建议作为 suitable 的答案。或者对架构进行任何适当的更改。
底线
我正在寻找将产生 1 个结果的查询,其中包含 1 个 datetimeoffset 字段。 运行.
需要不到1秒的时间您想要 select 紧跟在不等于当前 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
)
);
这是基于您最初的尝试:
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