SQL是一种基于集合的语言,循环应该是最后的选择。因此,基于集合的方法是先生成所需的所有日期并一次性插入,而不是一次一个地循环插入。Aaron Bertrand撰写了一篇关于如何在不使用循环的情况下生成集合或序列的系列文章,以下是其中部分内容:
第3部分特别关注日期处理。
假设您没有一个可用的日历表,您可以使用堆叠的CTE方法生成在开始日期和结束日期之间的日期列表。
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3;
我在此处省略了具体的实现细节,因为这些内容已经在链接的文章中涉及。简而言之,它始于一个硬编码的10行表格,然后将该表格与自身连接以获取100行(10 x 10),然后将该100行表格再次连接自身以获取10,000行(我到此为止,但如果需要更多行,则可以添加更多连接)。
在每一步中,输出是一个名为N
的单列,其值为1(为了保持简单)。同时,除了使用TOP
和您的开始和结束日期之间的差异 - TOP(DATEDIFF(DAY,@StartDate,@EndDate) + 1)
,我还告诉SQL Server如何生成10,000行。这避免了不必要的工作。我必须将差异加1,以确保两个日期都包含在内。
使用排名函数ROW_NUMBER()
,我对生成的每一行添加了增量编号,然后将此增量编号添加到起始日期中,以获取日期列表。由于ROW_NUMBER()
从1开始,因此我需要从中减去1,以确保包括起始日期。
然后,只需使用NOT EXISTS
来排除已经存在的日期即可。我将上述查询的结果封装在自己的CTE中,称为dates
:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Dates AS
( SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3
)
INSERT INTO MyTable ([TimeStamp])
SELECT Date
FROM Dates AS d
WHERE NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE d.Date = t.[TimeStamp])
SQL Fiddle上的示例
如果您创建了一个日历表(如链接文章中所述),则可能不需要插入这些额外的行,您可以即时生成结果集,例如:
SELECT [Timestamp] = c.Date,
t.[FruitType],
t.[NumOffered],
t.[NumTaken],
t.[NumAbandoned],
t.[NumSpoiled]
FROM dbo.Calendar AS c
LEFT JOIN dbo.MyTable AS t
ON t.[Timestamp] = c.[Date]
WHERE c.Date >= @StartDate
AND c.Date < @EndDate;
补充说明
为回答您的实际问题,你的循环应该编写如下:
DECLARE @StartDate AS DATETIME
DECLARE @EndDate AS DATETIME
DECLARE @CurrentDate AS DATETIME
SET @StartDate = '2015-01-01'
SET @EndDate = GETDATE()
SET @CurrentDate = @StartDate
WHILE (@CurrentDate < @EndDate)
BEGIN
IF NOT EXISTS (SELECT 1 FROM myTable WHERE myTable.Timestamp = @CurrentDate)
BEGIN
INSERT INTO MyTable ([Timestamp])
VALUES (@CurrentDate);
END
SET @CurrentDate = DATEADD(DAY, 1, @CurrentDate);
END
我不赞成这种方法,仅仅因为某些事情只需要做一次,并不意味着我不应该展示正确的做法。
进一步解释
由于堆叠CTE方法可能已经过于复杂了,我将使用未记录在文档中的系统表master..spt_values
来简化它。如果你运行:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
您会发现您可以获得从0到2047的所有数字。
如果您运行以下命令:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
您可以获取从起始日期到未来2047天的所有日期。如果您添加了进一步的where子句,您可以将其限制为在结束日期之前的日期:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate;
现在你已经在一个基于集合的查询中拥有了所有需要的日期,你可以使用
NOT EXISTS
来消除已经存在于你的表中的行。
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
最后,您可以使用
INSERT
将这些日期插入到您的表中。
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
INSERT YourTable ([Timestamp])
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
希望这能在一定程度上表明,基于集合的方法不仅更高效,而且更简单易懂。