SQL Query based on a while loop - sql

I am trying to construct an sql query using a while loop that increments a datetime by one minute each iteration and then generates a select statement based on the time:
declare #dt datetime
set #dt = '2011-7-21'
while #dt < '2011-7-22'
begin
select Count(*) From Actions Where Timestamp = #dt
set #dt = DATEADD(mi, 1, #dt)
end
The query works as intended except that every iteration of the while loop seems to produce a new query entirely, rather than simply a new row. Is there a way to construct this so that its one single query and each row is generated by the incrementation of the loop?
I believe this occurs because the select statement is inside the loop, but I'm not sure how to construct it a different way that works.
EDIT - Here is what I came up with using a temporary table, but it is slow. Maybe there is a faster way? If not thats fine, atleast this works:
create table #temp
(
[DT] datetime not null,
[Total] int not null
)
declare #dt datetime
declare #result int
set #dt = '2011-7-21'
while #dt < '2011-7-22'
begin
set #result = Count(*) From Actions Where Timestamp = #dt
insert #temp ([DT],[Total]) values (#dt, #result)
set #dt = DATEADD(mi, 1, #dt)
end
select * from #temp;
drop table #temp;

One way by using a table of numbers
declare #dt datetime
set #dt = '2011-07-21'
select DATEADD(mi, number, #dt)
from master..spt_values
where type = 'P'
and DATEADD(mi, number, #dt) < '2011-07-22'
If you have your own number table, use that
See here for more info http://wiki.lessthandot.com/index.php/Date_Ranges_Without_Loops
you full query would be like
DECLARE #dt DATETIME
SET #dt = '2011-07-21'
SELECT x.SomeTime,y.TheCount FROM
(SELECT DATEADD(mi, number, #dt) as SomeTime FROM master..spt_values
WHERE TYPE = 'P'
AND DATEADD(mi, number, #dt) < '2011-07-22') x
LEFT JOIN (
SELECT TIMESTAMP, COUNT(*) AS TheCount
FROM Actions
GROUP BY TIMESTAMP
) AS y
ON x.SomeTime = dateadd(mi, datediff(mi, 0, y.Timestamp)+0, 0)

If you have a numbers table (from 0 to a million or whatever), this is relatively simple:
SELECT *
FROM Numbers AS n
LEFT JOIN (
SELECT Timestamp, COUNT(*) AS Ct
FROM Actions
GROUP BY Timestamp
) AS ActionSummary
ON ActionSummary.Timestamp = DATEADD(mi, n.Number, '2011-07-21')
WHERE DATEADD(mi, n.Number, '2011-07-21') < '2011-07-22'
ORDER BY DATEADD(mi, n.Number, '2011-07-21')
No need for loops.
There's ways to optimize this, but that should be fairly understandable as it is.
Also note that the timestamps cannot have any seconds or fractions of a second for this to work (your original has this problem as well).

Related

Add a record for every Week-Day of the year to a blank table

I have a blank table that has two columns [ID] and [MyDate].
I would like to populate that table with all of the days of the current year MINUS weekends.
Is there a way to do this with a SQL query?
In this case I am using MSSQL T-SQL
I do not have any example code, as I am at a loss on where to get started for this scenario.
Using a numbers (Tally) table helps you to avoid using loops.
If you don't already have a numbers table, you can use this script to create it:
SELECT TOP 10000 IDENTITY(int,0,1) AS Number
INTO Tally
FROM sys.objects s1
CROSS JOIN sys.objects s2
ALTER TABLE Tally ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
For more information about the creation of a numbers table, read this SO post.
Now that you have a numbers table, you can use a cte to generate the dates you want. I've used DATEFROMPARTS and GETDATE() to get Jauary 1st of the current year, if you are using a version of sql server below 2012 you need to use other methods for that:
DECLARE #StartDate Date,
#EndDate Date
SELECT #StartDate = DATEFROMPARTS(YEAR(GetDate()), 1, 1)
SELECT #EndDate = DATEADD(Year, 1, #StartDate)
Now, create a CTE to get the dates required using the numbers table, and insert the records from the cte to the table:
;WITH CTE AS
(
SELECT DATEADD(Day, Number, #StartDate) As TheDate
FROM Tally
WHERE DATEADD(Day, Number, #StartDate) < #EndDate
)
INSERT INTO WeekDays
SELECT TheDate
FROM CTE
WHERE DATEPART(WeekDay, TheDate) BETWEEN 2 AND 6
See a live demo on rextester.
This will do it. Here the 1 and the 7 represents Sunday and Saturday
CREATE TABLE T (
ID INT NOT NULL IDENTITY(1,1),
MyDate DATE NOT NULL)
DECLARE #Start DATE
DECLARE #End DATE
SET #Start = '20170101'
SET #End = '20171231'
WHILE #Start <= #End
BEGIN
IF (DATEPART(DW, #Start) NOT IN (1,7))
BEGIN
INSERT INTO T (MyDate) VALUES (#Start)
END
SET #Start = DATEADD(DAY, 1, #Start)
END
SELECT * FROM T
Here's my quick attempt at your problem. Just use your table instead
select
CAST('2017-03-15' as datetime) as datestuff
into #test
Delete from #test
DECLARE #Y datetime = CAST('2017-12-31' AS DATE)
while #y != '2017-01-01'
begin
if DATENAME(DW, #y) not IN ('SUNDAY', 'SATURDAY')
BEGIN
INSERT INTO #test
SELECT #y
END
SET #Y = DATEADD(DD, -1, #Y)
end
select * from #test

Generate random records for datetime columns by stored procedure in SQL

I want to generate 5 random records from a field which is a datetime column and contains several records of (OrderDate) for a given date range using stored procedure for the table named Orders
CREATE PROCEDURE test
#StartDate DATETIME = NULL,
#EndDate DATETIME = NULL,
AS
BEGIN
SELECT OrderDate = DATEADD(......)
FROM Orders
END
May I get some help!
A while loop works ok for this purpose, especially if you're concerned with limiting your randomness to a bounded date range.
The downside is that potentially many insert queries get executed vs. a single insert for a recursive CTE as in the other answer.
create procedure dbo.spGenDates2
#MinDate datetime,
#MaxDate datetime,
#RecordCount int = 5
as
SET NOCOUNT ON;
DECLARE #Range int, #DayOffset int, #Cnt int
SET #Range = DATEDIFF(dd, #MinDate, #MaxDate)
SET #Cnt = 1
WHILE #Cnt <= #RecordCount
BEGIN
SET #DayOffset = RAND() * (#Range + 1)
INSERT INTO _test (Dt) VALUES(DATEADD(dd, #DayOffset, #MinDate))
SET #Cnt = #Cnt + 1
END
Based on your syntax I'm assuming you're using SQL Server...
Note that you cannot reliably use the sql random number generator function RAND() within the context of a single query because it does not get reseeded per row so you end up receiving the same, single random number for each row result. Instead, an approach using NEWID() converted into a numeric does the trick when generating random values within the execution of a single query.
Here's a procedure that will give you n number of sample dates in the near past.
create procedure dbo.spGenDates
#MaxDate datetime,
#RecordCount int = 5
as
WITH dates as (
SELECT DATEADD(MILLISECOND, ABS(CHECKSUM(NEWID())) * -1, #MaxDate) D,
1 as Cnt
UNION ALL
SELECT DATEADD(MILLISECOND, ABS(CHECKSUM(NEWID())) * -1, #MaxDate) D,
x.Cnt + 1 as Cnt
FROM dates x
WHERE x.Cnt < #RecordCount
)
INSERT INTO _test (Dt)
SELECT D
FROM dates
The wording of the question has been clarified (see comments on another answer) to be a desire to SELECT 5 random sample dates within a bounded range from a table.
A query like this will yield the desired result.
SELECT TOP (5) OrderDate
FROM Orders
WHERE OrderDate >= #StartDate
AND OrderDate < #EndDate
ORDER BY NEWID()

SQL Current status for given day (FOR loop)

For simplicity lets assume that I have a view with three fields
date_in (date)
Container (varchar)
date_out (date)
Now, the container is IN if the date_in is lesser or equal to given date and date_out is null or greater than given date. Now I am trying to count the containers for given time period. In pseudocode between two values STARTDATE and ENDDATE it would be something like
FOR X =STARTDATE, X<= ENDDADE, X++ {
if date_in <=X and date_out>x
count (container)
}
or closer to SQL:
declare #startdate date,
#d date;
set #startdate = '1/01/2014'
set #d = #startdate
"FOR on the #d variable would go here" {
select #d as SNAP_DATE, count (container) where date_in <#d
and (date_out is null or date_out> #d)
}
It might be simple - I guess I could make a new table and manually do multiple SELECT INTO (and later query from this new table) but its not very elegant solution.
Edit: just to precise - in the end I'd like to have something like:
DATE Count
1/02/2014 10
2/02/2014 15
...
7/03/2014 19
You could do this procedurally as follows:
Use a while loop to loop from start to end date.
Use a table variable to store each date-count pair.
Select from the table variable to get the summarised result.
declare #start date = '1/01/2014'
declare #end date = '7/03/2014'
declare #tbl table(Date date, Count int)
while(#start < #end)
begin
insert into #tbl
select #start, count(*)
from your_view
where (in_date < #start)
and ((out_date is null) or (out_date > #start))
set #start = dateadd(day, 1, #start)
end
select * from #tbl
You might be able to do something like the following. It uses a numbers table, which can be a real or derived table. It contains rows of integers. You need a table that begins with 0 and has enough values to cover your date range. Check here for more information on a numbers table.
DECLARE #StartDate DATE = '1/2/2014'
DECLARE #EndDate DATE = '1/4/2014'
SELECT DATEADD(d, n.num, #StartDate) AS DATE, COUNT(*) AS COUNT
FROM Numbers n
JOIN MyView mv ON mv.date_in < DATEADD(d, n.num, #StartDate)
AND (mv.date_out IS NULL OR mv.date_out > DATEADD(d, n.num, #StartDate))
WHERE DATEADD(d, n.num, #StartDate) BETWEEN #StartDate AND #EndDate
GROUP BY DATEADD(d, n.num, #StartDate)
ORDER BY DATEADD(d, n.num, #StartDate)
The numbers in the numbers table are converted to the list of dates between the date range. Each date is joined to your view based on the criteria you need.

Selecting all dates from a table within a date range and including 1 row per empty date

I am trying to refactor some code in an ASP.Net website and having a problem with a stored procedure I am writing.
What I want to do is get a date range, then select all data within that range from a table BUT if a date is not present I need to still select a row.
My idea for this as you can see in the code below is to create a temporary table, and populate it with all the dates within my date range, then join this onto the table I am selecting from however this does not work. Am I doing something wrong here? The tempDate column is always null in this join however I have checked the table and it deffinately has data in it.
-- Parameters
DECLARE #DutyDate datetime='2012-01-01 00:00:00'
DECLARE #InstructorID nvarchar(2) = N'29'
DECLARE #datesTBL TABLE (tempDate DATETIME)
-- Variables
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SELECT
#StartDate =StartDate,
#EndDate = EndDate
FROM
DutyPeriodTbl
WHERE
(StartDate <= #DutyDate)
AND
(EndDate >= #DutyDate)
DECLARE #d DATETIME = #StartDate
WHILE #d<=#EndDate
BEGIN
INSERT INTO #datesTBL VALUES (CONVERT(DATETIME, #d, 102))
SET #d=DATEADD(day,1,#d)
END
SELECT
dt.tempDate ,
InstructorID, EventStart,
EventEnd, cancelled,
cancelledInstructor,
EventType, DevName,
Room, SimLocation,
ClassLocation, Event,
Duration, TrainingDesc,
Crew, Notes,
LastAmended, InstLastAmended,
ChangeAcknowledged, Type,
OtherType, OtherTypeDesc,
CourseType
FROM
OpsInstructorEventsView iv
LEFT OUTER JOIN
#datesTBL dt
ON
CONVERT(DATETIME, iv.EventStart, 102) = CONVERT(DATETIME, dt.tempDate, 102)
WHERE
InstructorID = #InstructorID
AND
EventStart BETWEEN CONVERT(DATETIME, #StartDate, 102) AND CONVERT(DATETIME, #EndDate, 102)
ORDER BY
EventStart
There are several ways of dealing with missing rows, but all are about having another set of data to combine with your current results.
That could be derived from your results, created by a CTE or other process (such as your example), or (my preference) by using a permanent template to join against.
The template in your case could just be a table of dates, like your #datesTBL. The difference being that it's created in advance with, for example, 100 years worth of dates.
Your query may then be similar to your example, but I would try the following...
SELECT
dt.tempDate ,
InstructorID, EventStart,
EventEnd, cancelled,
cancelledInstructor,
EventType, DevName,
Room, SimLocation,
ClassLocation, Event,
Duration, TrainingDesc,
Crew, Notes,
LastAmended, InstLastAmended,
ChangeAcknowledged, Type,
OtherType, OtherTypeDesc,
CourseType
FROM
#datesTBL dt
LEFT OUTER JOIN
OpsInstructorEventsView iv
ON iv.EventStart >= dt.tempDate
AND iv.EventStart < dt.tempDate + 1
AND iv.InstructorID = #InstructorID
WHERE
dt.tempDate >= #StartDate
AND dt.tempDate <= #EndDate
ORDER BY
dt.tempDate,
iv.EventStart
This puts the calendar template on the LEFT, and so makes many queries easier as you know the calendar's date field is always populated, is always a date only (no time part) value, is in order, is simple to GROUP BY, etc.
Well, idea is the same, but i would write function, that returns table with all dates in period. Look at this:
Create Function [dbo].[Interval]
(
#DateFrom Date,
#DateTo Date
)
Returns #tab Table
(
MyDate DateTime
)
As
Begin
Declare #Days int
Declare #i int
Set #Days = DateDiff(Day, #DateFrom, #DateTo)
Set #i = 0;
While (#Days > #i)
Begin
Insert Into #tab(MyDate)
Values (DateAdd(Day, #i, #DateTo))
Set #i = #i + 1
End
return
End
And reuse the function whenever you need it..
Select *
From [dbo].[Interval]('2011-01-01', GETDATE())

SQL first of every month

Supposing that I wanted to write table valued function in SQL that returns a table with the first day of every month between the argument dates, what is the simplest way to do this?
For example fnFirstOfMonths('10/31/10', '2/17/11') would return a one-column table with 11/1/10, 12/1/10, 1/1/11, and 2/1/11 as the elements.
My first instinct is just to use a while loop and repeatedly insert first days of months until I get to before the start date. It seems like there should be a more elegant way to do this though.
Thanks for any help you can provide.
Something like this would work without being inside a function:
DECLARE #LowerDate DATE
SET #LowerDate = GETDATE()
DECLARE #UpperLimit DATE
SET #UpperLimit = '20111231'
;WITH Firsts AS
(
SELECT
DATEADD(DAY, -1 * DAY(#LowerDate) + 1, #LowerDate) AS 'FirstOfMonth'
UNION ALL
SELECT
DATEADD(MONTH, 1, f.FirstOfMonth) AS 'FirstOfMonth'
FROM
Firsts f
WHERE
DATEADD(MONTH, 1, f.FirstOfMonth) <= #UpperLimit
)
SELECT *
FROM Firsts
It uses a thing called CTE (Common Table Expression) - available in SQL Server 2005 and up and other database systems.
In this case, I start the recursive CTE by determining the first of the month for the #LowerDate date specified, and then I iterate adding one month to the previous first of month, until the upper limit is reached.
Or if you want to package it up in a stored function, you can do so, too:
CREATE FUNCTION dbo.GetFirstOfMonth(#LowerLimit DATE, #UpperLimit DATE)
RETURNS TABLE
AS
RETURN
WITH Firsts AS
(
SELECT
DATEADD(DAY, -1 * DAY(#LowerLimit) + 1, #LowerLimit) AS 'FirstOfMonth'
UNION ALL
SELECT
DATEADD(MONTH, 1, f.FirstOfMonth) AS 'FirstOfMonth'
FROM
Firsts f
WHERE
DATEADD(MONTH, 1, f.FirstOfMonth) <= #UpperLimit
)
SELECT * FROM Firsts
and then call it like this:
SELECT * FROM dbo.GetFirstOfMonth('20100522', '20100831')
to get an output like this:
FirstOfMonth
2010-05-01
2010-06-01
2010-07-01
2010-08-01
PS: by using the DATE datatype - which is present in SQL Server 2008 and newer - I fixed the two "bugs" that Richard commented about. If you're on SQL Server 2005, you'll have to use DATETIME instead - and deal with the fact you're getting a time portion, too.
create function dbo.fnFirstOfMonths(#d1 datetime, #d2 datetime)
returns table as return
select dateadd(m,datediff(m,0,#d1)+v.number,0) as FirstDay
from master..spt_values v
where v.type='P' and v.number between 0 and datediff(m, #d1, #d2)
and dateadd(m,datediff(m,0,#d1)+v.number,0) between #d1 and #d2
GO
Notes
master..spt_values is a source for general purpose sequence numbers in SQL Server
dateadd(m, datediff(m is a technique for working out the first day of month for any date
+v.number is used to increase it by one month each time
0 and datediff(m, #d1, #d2) this condition gives us all the numbers we need to generate a first-of-month date for each month between #d1 and #d2, inclusive of both months
and dateadd(m,datediff(m,0,#d1)+v.number,0) between #d1 and #d2 the final filter to verify that the first-of-month date generated is between #d1 and #d2
Performance comparison against marc_s's code
Summary
8220 ms (CTE)
4173 ms (master..spt_values)
Test
declare #t table (dt datetime)
declare #d datetime
declare #i int
set nocount on
set #d = GETDATE()
set #i = 0
while #i < 10000
begin
insert #t select * from dbo.getfirstofmonth('20090102', '20100506')
delete #t
set #i = #i + 1
end
print datediff(ms, #d, getdate())
set #d = GETDATE()
set #i = 0
while #i < 10000
begin
insert #t select * from dbo.fnfirstofmonths('20090102', '20100506')
delete #t
set #i = #i + 1
end
print datediff(ms, #d, getdate())
Performante
It will loop just between the months involved (4 times in the example):
set dateformat mdy;
declare #date1 smalldatetime,#date2 smalldatetime,#i int
set #date1= '10-31-2010'
set #date2= '02-17-2011'
set #i=1
while(#i<=DATEDIFF(mm,#date1,#date2))
begin
select convert(smalldatetime,CONVERT(varchar(6),DATEADD(mm,#i,#date1),112)+'01',112)
set #i=#i+1
end
I realize this isn't a function, but I'm going to throw this into the mix anyway.
select cal_date from calendar
where day_of_month = 1
and cal_date between '2011-01-01' and '2012-01-01'
This calendar table runs on a PostgreSQL server at work. I'll port it to SQL Server tonight, and run some speed comparisons. (Why? Because this stuff is fun, that's why.)
Just in case anybody is still reading this ...
I cannot imaging that any of the aforementioned functions is faster than this:
declare #DatFirst date = '20101031', #DatLast date = '21110217';
declare #DatFirstOfFirstMonth date = dateadd(day,1-day(#DatFirst),#DatFirst);
select DatFirstOfMonth = dateadd(month,n,#DatFirstOfFirstMonth)
from (
select top (datediff(month,#DatFirstOfFirstMonth,#DatLast)+1)
n=row_number() over (order by (select 1))-1
from (values (1),(1),(1),(1),(1),(1),(1),(1)) a (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) b (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) c (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) d (n)
) x