This question already has answers here:
Calculate a Running Total in SQL Server
(15 answers)
Closed 1 year ago.
I have the below SQL query that works but it's very slow. it takes about 1 min to run the query. This would be made into a stored procedure. which is not the problem. but that store procedure would be called for every item of which there are about 600 items. The estimated run time probably would end up taking about 10 hours. Does anyone have any suggestions of a better way of doing it?
What I'm trying to do is get a running stock level for every day for the past year for each item.
If you need any more information. Please let me know.
DECLARE #StartDate AS DATETIME
DECLARE #EndDate AS DATETIME
DECLARE #CurrentDate AS DATETIME
DECLARE #ItemName As Varchar(450)
DECLARE #QOH DECIMAL(19,4)
SET #QOH = 0
SET #ItemName = 'TUR001-02'
SET #StartDate = '2020-04-01'
SET #EndDate = GETDATE()
SET #CurrentDate = #StartDate
CREATE TABLE #TempTable
(
Date datetime,
ItemName char(450),
QOH DECIMAL(19,4)
);
WHILE (#CurrentDate < #EndDate)
BEGIN
DECLARE #daySales DECIMAL(19,4)
SELECT #daySales = SUM(Quantity)
FROM qbInvoiceLineDetail
WHERE TxnDate = #CurrentDate AND FullName = #ItemName;
SET #QOH = #QOH - #daySales
INSERT INTO #TempTable (Date, ItemName, QOH)
SELECT #CurrentDate, #ItemName, #QOH;
SET #CurrentDate = DATEADD(DAY, 1, #CurrentDate);
END
SELECT * FROM #TempTable
DROP TABLE #TempTable
You can use a tally table to generate your dates between start and end, then insert all your data in one hit into your table.
Caveat - this is untested as I have nothing to check it against, assumes the dates are dates only, if they include time then will need to use convert - hopefully will be what you are looking for:
/*first, create a tally table - this should be a permanent feature */
select top 1000 N=Identity(int, 0, 1)
into dbo.Digits
from master.dbo.syscolumns a cross join master.dbo.syscolumns
declare #StartDate datetime='20200401', #EndDate datetime=GetDate()
select DateAdd(day,N,#startDate) currentDate, FullName ItemName, Sum(Quantity) over(order by d.N) QOH
from Digits d
left join qbInvoiceLineDetail q on q.TxnDate=DateAdd(day,N,#startDate)
where DateAdd(day,N,#startDate)<=#EndDate
group by TxnDate, ItemName
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
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()
I need to make a temporary table that holds of range of dates, as well as a couple of columns that hold placeholder values (0) for future use. The dates I need are the first day of each month between $startDate and $endDate where these variables can be several years apart.
My original sql statement looked like this:
select dbo.FirstOfMonth(InsertDate) as Month, 0 as Trials, 0 as Sales
into #dates
from customer
group by dbo.FirstOfMonth(InsertDate)
"FirstOfMonth" is a user-defined function I made that pretty much does what it says, returning the first day of the month for the provided date with the time at exactly midnight.
This produced almost exactly what I needed until I discovered there were occasionally gaps in my dates where I had a few months were there were no records insert dates. Since my result must still have the missing months I need a different approach.
I have added the following declarations to the stored procedure anticipating their need for the range of the dates I need ...
declare $startDate set $startDate = select min(InsertDate) from customer
declare $endDate set $endDate = select max(InsertDate) from customer
... but I have no idea what to do from here.
I know this question is similar to this question but, quite frankly, that answer is over my head (I don't often work with SQL and when I do it tends to be on older versions of SQL Server) and there are a few minor differences that are throwing me off.
I needed something similar, but all DAYS instead of all MONTHS.
Using the code from MatBailie as a starting point, here's the SQL for creating a permanent table with all dates from 2000-01-01 to 2099-12-31:
CREATE TABLE _Dates (
d DATE,
PRIMARY KEY (d)
)
DECLARE #dIncr DATE = '2000-01-01'
DECLARE #dEnd DATE = '2100-01-01'
WHILE ( #dIncr < #dEnd )
BEGIN
INSERT INTO _Dates (d) VALUES( #dIncr )
SELECT #dIncr = DATEADD(DAY, 1, #dIncr )
END
This will quickly populate a table with 170 years worth of dates.
CREATE TABLE CalendarMonths (
date DATETIME,
PRIMARY KEY (date)
)
DECLARE
#basedate DATETIME,
#offset INT
SELECT
#basedate = '01 Jan 2000',
#offset = 1
WHILE (#offset < 2048)
BEGIN
INSERT INTO CalendarMonths SELECT DATEADD(MONTH, #offset, date) FROM CalendarMonths
SELECT #offset = #offset + #offset
END
You can then use it by LEFT joining on to that table, for the range of dates you require.
I would probably use a Calendar table. Create a permanent table in your database and fill it with all of the dates. Even if you covered a 100 year range, the table would still only have ~36,525 rows in it.
CREATE TABLE dbo.Calendar (
calendar_date DATETIME NOT NULL,
is_weekend BIT NOT NULL,
is_holiday BIT NOT NULL,
CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED (calendar_date)
)
Once the table is created, just populate it once in a loop, so that it's always out there and available to you.
Your query then could be something like this:
SELECT
C.calendar_date,
0 AS trials,
0 AS sales
FROM
dbo.Calendar C
WHERE
C.calendar_date BETWEEN #start_date AND #end_date AND
DAY(C.calendar_date) = 1
You can join in the Customers table however you need to, outer joining on FirstOfMonth(InsertDate) = C.calendar_date if that's what you want.
You can also include a column for day_of_month if you want which would avoid the overhead of calling the DAY() function, but that's fairly trivial, so it probably doesn't matter one way or another.
This of course will not work in SQL-Server 2000 but in a modern database where you don't want to create a permanent table. You can use a table variable instead creating a table so you can left join the data try this. Change the DAY to HOUR etc to change the increment type.
declare #CalendarMonths table (date DATETIME, PRIMARY KEY (date)
)
DECLARE
#basedate DATETIME,
#offset INT
SELECT
#basedate = '01 Jan 2014',
#offset = 1
INSERT INTO #CalendarMonths SELECT #basedate
WHILE ( DATEADD(DAY, #offset, #basedate) < CURRENT_TIMESTAMP)
BEGIN
INSERT INTO #CalendarMonths SELECT DATEADD(HOUR, #offset, date) FROM #CalendarMonths where DATEADD(DAY, #offset, date) < CURRENT_TIMESTAMP
SELECT #offset = #offset + #offset
END
A starting point of a useful kludge to specify a range or specific list of dates:
SELECT *
FROM
(SELECT CONVERT(DateTime,'2017-1-1')+number AS [Date]
FROM master..spt_values WHERE type='P' AND number<370) AS DatesList
WHERE DatesList.Date IN ('2017-1-1','2017-4-14','2017-4-17','2017-12-25','2017-12-26')
You can get 0 to 2047 out of master..spt_values WHERE type='P', so that's five and a half year's worth of dates if you need it!
Tested below and it works, though it's a bit convoluted.
I assigned arbitrary values to the dates for the test.
DECLARE #SD smalldatetime,
#ED smalldatetime,
#FD smalldatetime,
#LD smalldatetime,
#Mct int,
#currct int = 0
SET #SD = '1/15/2011'
SET #ED = '2/02/2012'
SET #FD = (DATEADD(dd, -1*(Datepart(dd, #SD)-1), #sd))
SET #LD = (DATEADD(dd, -1*(Datepart(dd, #ED)-1), #ED))
SET #Mct = DATEDIFF(mm, #FD, #LD)
CREATE TABLE #MyTempTable (FoM smalldatetime, Trials int, Sales money)
WHILE #currct <= #Mct
BEGIN
INSERT INTO #MyTempTable (FoM, Trials, Sales)
VALUES
(DATEADD(MM, #currct, #FD), 0, 0)
SET #currct = #currct + 1
END
SELECT * FROM #MyTempTable
DROP TABLE #MyTempTable
For SQL Server 2000, this stackoverflow post looks promising for a way to temporarily generate dates calculated off of a start and end date. It's not exactly the same but quite similar. This post has a very in-depth answer on truncating dates, if needed.
In case anyone stumbles on this question and is working in PostgreSQL instead of SQL Server 2000, here is how you might do it there...
PostgreSQL has a nifty series generating function. For your example, you could use this series of all days instead of generating an entire calendar table, and then do groupings and matchups from there.
SELECT current_date + s.a AS dates FROM generate_series(0,14,7) AS s(a);
dates
------------
2004-02-05
2004-02-12
2004-02-19
(3 rows)
SELECT * FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00', '10 hours');
generate_series
---------------------
2008-03-01 00:00:00
2008-03-01 10:00:00
2008-03-01 20:00:00
2008-03-02 06:00:00
2008-03-02 16:00:00
2008-03-03 02:00:00
2008-03-03 12:00:00
2008-03-03 22:00:00
2008-03-04 08:00:00
(9 rows)
I would also look into date_trunc from PostgreSQL using 'month' for the truncator field to maybe refactor your original query to easily match with a date_trunc version of the calendar series.
select top (datediff(D,#start,#end)) dateadd(D,id-1,#start)
from BIG_TABLE_WITH_NO_JUMPS_IN_ID
declare #start datetime
set #start = '2016-09-01'
declare #end datetime
set #end = '2016-09-30'
create table #Date
(
table_id int identity(1,1) NOT NULL,
counterDate datetime NULL
);
insert into #Date select top (datediff(D,#start,#end)) NULL from SOME_TABLE
update #Date set counterDate = dateadd(D,table_id - 1, #start)
The code above should populate the table with all the dates between the start and end. You would then just join on this table to get all of the dates needed. If you only needed a certain day of each month, you could dateadd a month instead.
SELECT P.Id
, DATEADD ( DD, -P.Id, P.Date ) AS Date
FROM (SELECT TOP 1000 ROW_NUMBER () OVER (ORDER BY (SELECT NULL)) AS Id, CAST(GETDATE () AS DATE) AS Date FROM master.dbo.spt_values) AS P
This query returns a table calendar for the last 1000 days or so. It can be put in a temporary or other table.
Create a table variable containing a date for each month in a year:
declare #months table (reportMonth date, PRIMARY KEY (reportMonth));
declare #start date = '2018', #month int = 0; -- base 0 month
while (#month < 12)
begin
insert into #months select dateAdd(month, #month, #start);
select #month = #month + 1;
end
--verify
select * from #months;
This is by far the quickest method I have found (much quicker than inserting rows 1 by 1 in a WHILE loop):
DECLARE #startDate DATE = '1900-01-01'
DECLARE #endDate DATE = '2050-01-01'
SELECT DATEADD(DAY, sequenceNumber, #startDate) AS TheDate
INTO #TheDates
FROM (
SELECT ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n + 10000*tenthousands.n AS sequenceNumber
FROM
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) ones(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tens(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) hundreds(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) thousands(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tenthousands(n)
WHERE ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n + 10000*tenthousands.n <= DATEDIFF(day, #startDate, #endDate)
) theNumbers
SELECT *
FROM #TheDates
ORDER BY TheDate
The recursive answer:
DECLARE #startDate AS date = '20220315';
DECLARE #endDate AS date = '20230316'; -- inclusive
WITH cte_minutes(dt)
AS (
SELECT
DATEFROMPARTS(YEAR(#startDate), MONTH(#startDate), 1)
UNION ALL
SELECT
DATEADD(month, 1, dt)
FROM
cte_minutes
WHERE DATEADD(month, 1, dt) < #endDate
)
SELECT
dt
into #dates
FROM
cte_minutes
WHERE
dt >= #startDate
AND
dt <= #endDate
OPTION (MAXRECURSION 2000);
DROP TABLE dbo.#dates
Guys, I am trying to write a stored procedure in T-SQL (SQL Server) that will select records based on a date field, keeping a variance of minutes in mind. Something like this:
CREATE PROCEDURE spGetCustomers(#DateRange DATETIME, #Variance int) AS
-- The next line is where I need help
-- I'm trying to subtract X amount of minutes from the date
-- So if #Variance = 4 AND #DateRange = '6/10/2009 1:15pm'
-- Then #StartDate should equal '6/10/2009 1:11pm'
DECLARE #StartDate = #DateRange - #Variance
-- I also need an #EndDate, which will be X amount of minutes
-- in the future. So if #Variance = 4 AND #DateRange = '6/10/2009 1:15pm'
-- Then #EndDate should equal '6/10/2009 1:19pm'
DECLARE #EndDate = #DateRange + #Variance
SELECT * FROM Customers WHERE Created BETWEEN #StartDate AND #EndDate
Hopefully this makes sense and someone can help me out! Thanks in advance
Check this out:
http://msdn.microsoft.com/en-us/library/ms186819(SQL.90).aspx
The DATEADD function allows you to add virtually any part of a date to another date object, it should be everything you need.
So basically do:
SELECT DATEADD(second, #Variance, #DateRange)
The following script provides an example that should get you started.
create table tmp_Customers
(
ID int identity(1,1),
CreatedDate datetime default getDate() not null,
Description varchar(15)
);
go
insert into tmp_Customers(Description) values('SomeData');
insert into tmp_Customers(Description) values('SomeData2');
insert into tmp_Customers(Description) values('SomeData3');
go
create procedure usp_GetCustomers
#iVarianceMinutes int,
#iDateRange datetime
as
set nocount on
declare #startDate datetime
declare #endDate datetime
--Define the date ranges for the select query
set #startDate = dateAdd(minute,-#iVarianceMinutes,#iDateRange)
set #endDate = dateAdd(minute,#iVarianceMinutes,#iDateRange)
--Get the Customers that were created within this time range.
SELECT *
FROM tmp_Customers
WHERE CreatedDate >= #startDate and CreatedDate < #endDate
return(0);
go
--Execute the procedure
declare #testDate datetime;
set #testDate = getDate();
exec usp_GetCustomers 5,#testDate
--drop procedure usp_GetCustomers
--drop table tmp_Customers