问题:对于给定的 SQL Server 表,是否可以“约束”或使用“唯一索引”,以便它只允许用户输入基于开始和结束日期只有一个“活动/实时”行的行?
示例数据库架构:
CREATE TABLE [dbo].[Service]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ServiceName] varchar(50) NOT NULL,
CONSTRAINT [PK_Service]
PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[ServiceRate]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ServiceId] [int] NOT NULL,
[StartDate] [datetime] NOT NULL,
[EndDate] [datetime] NULL,
[CountryId] [int] NOT NULL,
CONSTRAINT [PK_ServiceRate]
PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[ServiceRate] WITH CHECK
ADD CONSTRAINT [FK_ServiceRate_Service]
FOREIGN KEY([ServiceId]) REFERENCES [dbo].[Service] ([Id])
GO
ALTER TABLE [dbo].[ServiceRate] CHECK CONSTRAINT [FK_ServiceRate_Service]
GO
ALTER TABLE [dbo].[ServiceRate] WITH CHECK
ADD CONSTRAINT [CK_ServiceRate_StartDate_EndDate]
CHECK (([StartDate] <= COALESCE([EndDate], '9999-12-31')))
GO
ALTER TABLE [dbo].[ServiceRate] CHECK CONSTRAINT [CK_ServiceRate_StartDate_EndDate]
GO
CREATE UNIQUE NONCLUSTERED INDEX [UX_ServiceId_EndDate_CountryId]
ON [dbo].[ServiceRate] ([ServiceId] ASC, [EndDate] ASC, [CountryId] ASC)
例如,如果我用这样的数据填充
[ServiceRate]
表:
身份证 | 服务ID | 开始日期 | 结束日期 | 国家/地区ID |
---|---|---|---|---|
2 | 1 | 2023-01-01 00:00:00.000 | 空 | 0 |
3 | 1 | 2023-01-01 00:00:00.000 | 空 | 53 |
5 | 1 | 2023-01-01 00:00:00.000 | 空 | 70 |
6 | 2 | 2023-01-01 00:00:00.000 | 空 | 0 |
7 | 2 | 2023-01-01 00:00:00.000 | 空 | 53 |
9 | 1 | 2023-02-01 00:00:00.000 | 2023-02-28 00:00:00.000 | 0<--- THIS SHOULD ERROR (conflict row 1) |
11 | 1 | 2023-02-02 00:00:00.000 | 2023-02-27 00:00:00.000 | 0<--- THIS SHOULD ERROR (conflict row 9) |
注意:
CountryId
0 表示“所有国家/地区”
然后,只要
([ServiceId], [CountryId])
的列是唯一的,但也具有来自 [StartDate]
和 [EndDate]
的单独“实时”日期范围,那么一切都很好。
但是我希望 SQL Server 在尝试添加第 9 行时抛出错误,因为这与第 1 行相同的“活动/实时”日期范围冲突(以及
ServiceId
和 CountryId
的重复)
此外,如果第 1 行不存在 - 那么这也应该在第 11 行引发错误,因为这与第 9 行相同的“活动/实时”日期范围冲突(以及
ServiceId
和 CountryId
的重复)
)。
因此解释这一点的另一种方法是说 - 我可以限制表中的数据,以便该查询在任何日期运行时都不会返回大于 1 的
[rec]
值吗?
DECLARE @dt datetime = '2023-02-14'
SELECT ServiceId, CountryId, COUNT(*) rec
FROM [dbo].[ServiceRate]
WHERE @dt >= StartDate
AND @dt < COALESCE(EndDate, '9999-12-31')
GROUP BY ServiceId, CountryId
到目前为止,我已经有了
CONSTRAINT
和 UNIQUE NONCLUSTERED INDEX
,但我不太明白这一点。
我看过其他文章描述使用 LAG 函数的查询 - 但这仅在前面的
[EndDate]
与该 [StartDate]
组的下一个 (ServiceId, CountryId)
完全匹配时才有效。
任何帮助或提示,我们将不胜感激。
虽然我不太喜欢此类约束,因为这意味着您无法控制输入,但您可以创建一个函数检查约束:
-- Test data
SELECT id, ServiceId, cast(startdate AS datetime) AS startdate, cast(enddate AS datetime) AS enddate, CountryId
INTO tblRanges
FROM
(
VALUES (2, 1, N'2023-01-01 00:00:00.000', NULL, N'0')
, (3, 1, N'2023-01-01 00:00:00.000', NULL, N'53')
, (5, 1, N'2023-01-01 00:00:00.000', NULL, N'70')
, (6, 2, N'2023-01-01 00:00:00.000', NULL, N'0')
, (7, 2, N'2023-01-01 00:00:00.000', NULL, N'53')
) t (ID,ServiceId,StartDate,EndDate,CountryId)
go
-- Check function
CREATE FUNCTION dbo.fn_tblranges (@id int)
RETURNS int AS
BEGIN
RETURN (
CASE WHEN EXISTS(
SELECT 1
FROM dbo.tblRanges t
INNER JOIN dbo.tblRanges t2
ON t2.CountryId = t.CountryId
AND t2.ServiceId = t.ServiceId
AND ISNULL(t2.EndDate, CONVERT(datetime, '99990101', 112)) >= t.StartDate
AND ISNULL(t2.StartDate, CONVERT(datetime, '99990101', 112)) <=ISNULL(t.EndDate, CONVERT(datetime, '99990101', 112))
WHERE t.id = @id
AND t2.id <> t.id
) THEN 1 ELSE 0 END)
END
go
-- Check constraint
ALTER TABLE tblranges
ADD CONSTRAINT chkDupes
CHECK (dbo.fn_tblranges(id) = 0);
go
一些测试代码:
-- Fails
INSERT INTO tblRanges
VALUES (9, 1, N'2023-02-01 00:00:00.000', N'2023-02-28 00:00:00.000', N'0');
go
-- Fails
INSERT INTO tblRanges
VALUES (11, 1, N'2023-02-02 00:00:00.000', N'2023-02-27 00:00:00.000', N'0');
go
-- Succeeds
INSERT INTO tblranges
VALUES (8, 1, N'2022-01-01 00:00:00.000', '20221230', N'0')
, (12, 1, N'20221231', '20221231', N'0')
go
另一种方法是创建一个执行相同检查的触发器。
请注意,函数检查对于性能而言可能相对较差,尤其是使用标量函数来检查表数据的函数检查