Although the loop in SQL Server 2008 repeats through a date range and then INSERT

I have a table with several columns, one of which is a Timestamp column. But there is currently no daily entry in this table. This means that there are entries for January 1 and January 2, but there are no entries from January 3 or January 4 in the "Timestamp" field. However, recordings continue on January 5th and January 6th and so on. In principle, there are no weekends or other random days.

I am trying to write a script that scans this table from StartDate to EndDate (any date range that I choose) and repeat this date range, and if the record does not exist with any of the dates in this date range, insert a new record with this specific date in the Timestamp field, but empty / NULL data for the remaining fields.

Here is the pseudo code that I still have, and I think this is the right approach:

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 SELECT * FROM myTable WHERE myTable.Timestamp = "@CurrentDate" IF @@ROWCOUNT < 1 print @CurrentDate /*insert a new row query here*/ SET @CurrentDate = convert(varchar(30), dateadd(day,1, @CurrentDate), 101); /*increment current date*/ END 

Here's SQLFiddle - http://sqlfiddle.com/#!6/06c73/1

I am writing my first script in SQL Server Management Studio 2008, and I have what I think might be for an intermediate user. I am a PHP / MySQL developer and I am very familiar with these technologies, but I am completely new to SQL and VBScript. I understand programming concepts and logic, but it seems completely different than what I'm used to.

I really appreciate all the help and understanding in advance!

+7
sql sql-server-2008
source share
1 answer

SQL is a set-based language, and loops should be a last resort. So a set-based approach would be to first create all the dates you need and insert them at a time, rather than looping and inserting one at a time. Aaron Bertrand wrote a large series about creating a set or sequence without loops:

Part 3 is of particular importance as it relates to dates.

Assuming you don't have a calendar table, you can use the sophisticated CTE method to create a list of dates between start and end 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) SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1) Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate) FROM N3; 

I missed some details on how this works, as it is described in a related article, in fact, it starts with a hard coded table of 10 rows, and then joins this table with itself to get 100 rows (10 x 10), then joins this table of 100 rows to itself to get 10,000 rows (I stopped at this point, but if you need extra rows, you can add extra joins).

At each step, the output is a single column named N with a value of 1 (so that everything is simple). At the same time, determining how to generate 10,000 rows, I actually tell SQL Server to generate only the number needed with TOP and the difference between the start and end dates is TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1) This avoids unnecessary work. I had to add 1 to the difference to ensure that both dates are included.

Using the ranking function ROW_NUMBER() I add an incremental number to each of the generated rows, then add this incremental number to the start date to get a list of dates. Since ROW_NUMBER() starts at 1, I need to subtract 1 from this to include the start date.

Then it will be just an exception to dates that already exist with NOT EXISTS . I have cited the results of the above query in my own CTE called 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 script example


If you were to create a calendar table (as described in related articles), you may not need to insert these additional rows, you can simply create your own result set on the fly, for example:

 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; 

ADDITION

To answer your real question, your loop will be written as follows:

 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); /*increment current date*/ END 

SQL script example

I do not advocate this approach, simply because something is done only once, does not mean that I should not demonstrate the correct way to do it.


FURTHER EXPLANATION

Since the complex CTE method can have a complex set-based approach, I will simplify it using the table of undocumented systems master..spt_values . If you run:

 SELECT Number FROM master..spt_values WHERE Type = 'P'; 

You will see that you get all numbers from 0 to -2047.

Now if you run:

 DECLARE @StartDate DATE = '2015-01-01', @EndDate DATE = GETDATE(); SELECT Date = DATEADD(DAY, number, @StartDate) FROM master..spt_values WHERE type = 'P'; 

You will receive all dates from the start date to 2047 days in the future. If you add an optional where clause, you can limit it to dates before the end date:

 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; 

Now that you have all the required dates in one set-based query, you can exclude rows that already exist in your table using 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)); 

Finally, you can insert these dates in your table using 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)); 

Hopefully this will somehow show that the set-based approach is not only much more efficient, but also easier.

+14
source share

All Articles