Use a WITH in an SQL function? - sql

I'm trying to make a function but I do not know how to get the WITH in it. Here is my code.
CREATE FUNCTION CubicVolume (#StartDate date, #EndDate date) RETURNS #TableDays TABLE
(Days int)
AS
BEGIN
INSERT #TableDays
WITH Dates AS (
SELECT #StartDate AS DayInQuestion
UNION ALL
SELECT DATEADD(Day, 1, DayInQuestion) AS DayInQuestion
FROM Dates AS Dates
WHERE (DayInQuestion < #EndDate)
)
SELECT DISTINCT count(Dates.DayInQuestion)
FROM Dates AS Dates LEFT OUTER JOIN
HEATHrs ON Dates.DayInQuestion = HEATHrs.StartDate
WHERE (CAST(DATEPART(weekday, Dates.DayInQuestion) AS int) BETWEEN 2 AND 6)
RETURN
END

You have to put the Common Table Expression (CTE) before the INSERT:
WITH Dates AS
(
SELECT #StartDate AS DayInQuestion
UNION ALL
SELECT DATEADD(Day, 1, DayInQuestion) AS DayInQuestion
FROM Dates AS Dates
WHERE (DayInQuestion < #EndDate)
)
INSERT #TableDays
SELECT DISTINCT count(Dates.DayInQuestion)
FROM Dates AS Dates LEFT OUTER JOIN
HEATHrs ON Dates.DayInQuestion = HEATHrs.StartDate
WHERE (CAST(DATEPART(weekday, Dates.DayInQuestion) AS int) BETWEEN 2 AND 6)

Related

SQL - Insert all dates within range from table into another

I'm looking to populate a table with dates, based upon values contained within another.
Source : tblA
dtFrom dtTo
2019-01-01 2019-01-03
2019-02-01 2019-02-02
2019-03-01 2019-03-01
Destination : tblB
sDate
2019-01-01
2019-01-02
2019-01-03
2019-02-01
2019-02-02
2019-03-01
SQL Server 2014. As always, thanks in advance :-)
You can use a recursive CTE:
with dates as (
select dtfrom as dt, dtto
from tblA
union all
select dateadd(day, 1, dt), dtto
from dates
where dt < dtto
)
insert tblB (sDate)
select distinct dt
from dates;
The select distinct is only necessary to handle overlapping periods. If you know there are no overlaps, then don't use it.
You can use union to combine values from both columns into one rowset:
insert tblB
(sDate)
select distinct dt
from (
select dtFrom as dt
from tblA
union all
select dtTo
from tblA
) s
Use the always handy Calendar Table, which is a table that holds 1 row for each day, for all days between specific years. You can add additional columns like IsBusinessDay or WorkingStartHour / WorkingEndHour to make your date queries much easier.
-- Create Calendar Table
DECLARE #StartDate DATE = '2000-01-01'
DECLARE #EndDate DATE = '2050-01-01'
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
CREATE TABLE CalendarTable (
Date DATE PRIMARY KEY,
IsWorkingDay BIT
-- Other columns you might need
)
;WITH RecursiveCTE AS
(
SELECT
Date = #StartDate
UNION ALL
SELECT
Date = DATEADD(DAY, 1, R.Date)
FROM
RecursiveCTE AS R
WHERE
DATEADD(DAY, 1, R.Date) <= #EndDate
)
INSERT INTO CalendarTable (
Date,
IsWorkingDay)
SELECT
Date = R.Date,
IsWorkingDay = CASE WHEN DATEPART(WEEKDAY, R.Date) BETWEEN 1 AND 5 THEN 1 ELSE 0 END
FROM
RecursiveCTE AS R
OPTION
(MAXRECURSION 0)
Now with your calendar table, just join with a BETWEEN and INSERT to your destination table. You can use DISTINCT to make sure dates don't repeat:
INSERT INTO tblB (
sDate)
SELECT DISTINCT
sDate = C.Date
FROM
tlbA AS A
INNER JOIN CalendarTable AS C ON C.Date BETWEEN A.dtFrom AND A.dtTo
Let's say for example that you only want to insert records that are working days (monday to friday). You just need to filter the calendar table and done. You can add whichever logic you want on your table and just filter it when using, without repeating complex datetime logics.
INSERT INTO tblB (
sDate)
SELECT DISTINCT
sDate = C.Date
FROM
tlbA AS A
INNER JOIN CalendarTable AS C ON C.Date BETWEEN A.dtFrom AND A.dtTo
WHERE
C.IsWorkingDay = 1
With a Calendar you can inner join on the ranges to produce an Insert statement.
DECLARE #StartDate DATETIME = (SELECT MIN(dtFrom) FROM tblA)
DECLARE #EndDate DATETIME = (SELECT MAX(dtTo) FROM tblB)
;WITH Calendar as
(
SELECT CalendarDate = #StartDate, CalendarYear = DATEPART(YEAR, #StartDate), CalendarMonth = DATEPART(MONTH, #StartDate)
UNION ALL
SELECT CalendarDate = DATEADD(MONTH, 1, CalendarDate), CalendarYear = DATEPART(YEAR, CalendarDate), CalendarMonth = DATEPART(MONTH, CalendarDate)
FROM Calendar WHERE DATEADD (MONTH, 1, CalendarDate) <= #EndDate
)
INSERT INTO tblB
SELECT DISTINCT
C.CalendarDate
FROM
Calendar C
INNER JOIN tblA A ON C.CalendarDate BETWEEN A.dtFrom AND A.dtTo
You can achieve this result by using the below queries.
Steps 1 - Create a Custom Function which will take date range as a parameter and will return date series.
CREATE FUNCTION [dbo].[GenerateDateRange]
(#StartDate AS DATE,
#EndDate AS DATE,
#Interval AS INT
)
RETURNS #Dates TABLE(DateValue DATE)
AS
BEGIN
DECLARE #CUR_DATE DATE
SET #CUR_DATE = #StartDate
WHILE #CUR_DATE <= #EndDate BEGIN
INSERT INTO #Dates VALUES(#CUR_DATE)
SET #CUR_DATE = DATEADD(DAY, #Interval, #CUR_DATE)
END
RETURN;
END;
Step 2 - Join this custom function with your table tblA and insert the record in tblb as needed
insert tblb
select b.* from tblA a cross apply dbo.GenerateDateRange(a.dtFrom, a.dtTo, 1) b

Based on day fetch all dates - sql

I have start date, end date and name of days. How can fetch all dates between those two dates of that specific days in sql?
example data:
start_date:4/11/2018
end_date: 5/11/2018
days: monday, thursday
expected output: all dates between start and end date which comes on monday and thursday and store them in table
updated
my present code(not working)
; WITH CTE(dt)
AS
(
SELECT #P_FROM_DATE
UNION ALL
SELECT DATEADD(dw, 1, dt) FROM CTE
WHERE dt < #P_TO_DATE
)
INSERT INTO Table_name
(
ID
,DATE_TIME
,STATUS
,CREATED_DATE
,CREATED_BY
)
SELECT #P_ID
,(SELECT dt FROM CTE WHERE DATENAME(dw, dt) In ('tuesday','friday',null))
,'NOT SENT'
,CAST(GETDATE() AS DATE)
,#USER_ID
Another approach for generating dates between ranges can be like following query. This will be faster compared to CTE or WHILE loop.
DECLARE #StartDate DATETIME = '2018-04-11'
DECLARE #EndDate DATETIME = '2018-05-15'
SELECT #StartDate + RN AS DATE FROM
(
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
) T
WHERE RN <= DATEDIFF(DAY,#StartDate,#EndDate)
AND DATENAME(dw,#StartDate + RN) IN('Monday','Thursday')
Note:
If the row count present in master..[spt_values] is not sufficient for the provided range, you can make a cross join with the same to get a bigger range like following.
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
CROSS JOIN master..[spt_values] T2
By this you will be able to generate date between a range with gap of 6436369 days.
You can use a recursive common table expression (CTE) to generate a list of days. With datepart(dw, ...) you can filter for specific days of the week.
An example that creates a list of Mondays and Thursdays between March 1st and today:
create table ListOfDates (dt date);
with cte as
(
select cast('2018-03-01' as date) as dt -- First day of interval
union all
select dateadd(day, 1, dt)
from cte
where dt < getdate() -- Last day of interval
)
insert into ListOfDates
(dt)
select dt
from cte
where datepart(dw, dt) in (2, 5) -- 2=Monday and 5=Thursday
option (maxrecursion 0)
See it working at SQL Fiddle.
This will work for you:
DECLARE #table TABLE(
ID INT IDENTITY(1,1),
Date DATETIME,
Day VARCHAR(50)
)
DECLARE #Days TABLE(
ID INT IDENTITY(1,1),
Day VARCHAR(50)
)
INSERT INTO #Days VALUES ('Monday')
INSERT INTO #Days VALUES ('Thursday')
DECLARE #StartDate DATETIME='2018-01-01';
DECLARE #EndDate DATETIME=GETDATE();
DECLARE #Day VARCHAR(50)='Friday';
DECLARE #TempDate DATETIME=#StartDate;
WHILE CAST(#TempDate AS DATE)<=CAST(#EndDate AS DATE)
BEGIN
IF EXISTS (SELECT 1 FROM #Days WHERE DAY IN (DATENAME(dw,#TempDate)))
BEGIN
INSERT INTO #table
VALUES (
#TempDate, -- Date - datetime
DATENAME(dw,#TempDate) -- Day - varchar(50)
)
END
SET #TempDate=DATEADD(DAY,1,#TempDate)
END
SELECT * FROM #table
INSERT INTO TargetTab(dateCOL)
SELECT dateCOL
FROM tab
WHERE dateCOL >= startdate AND dateCOL <= enddate
AND (DATENAME(dw,dateCOL) ='Thursday' OR DATENAME(dw,dateCOL) = 'Monday')
Try this query to get your result.
Use a recursive CTE to generate your dates, then filter by week day.
SET DATEFIRST 1 -- 1: Monday, 7 Sunday
DECLARE #StartDate DATE = '2018-04-11'
DECLARE #EndDate DATE = '2018-05-15'
DECLARE #WeekDays TABLE (WeekDayNumber INT)
INSERT INTO #WeekDays (
WeekDayNumber)
VALUES
(1), -- Monday
(4) -- Thursday
;WITH GeneratingDates AS
(
SELECT
GeneratedDate = #StartDate,
WeekDay = DATEPART(WEEKDAY, #StartDate)
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate),
WeekDay = DATEPART(WEEKDAY, DATEADD(DAY, 1, G.GeneratedDate))
FROM
GeneratingDates AS G -- Notice that we are referencing a CTE that we are also declaring
WHERE
G.GeneratedDate < #EndDate
)
SELECT
G.GeneratedDate
FROM
GeneratingDates AS G
INNER JOIN #WeekDays AS W ON G.WeekDay = W.WeekDayNumber
OPTION
(MAXRECURSION 30000)
Try this:
declare #start date = '04-11-2018'
declare #end date = '05-11-2018'
declare #P_ID int = 1
declare #USER_ID int = 11
;with cte as(
select #start [date]
union all
select dateadd(DAY, 1, [date]) from cte
where [date] < #end
)
--if MY_TABLE doesn't exist
select #P_ID,
[date],
'NOT SENT',
cast(getdate() as date),
#USER_ID
into MY_TABLE
from cte
--here you can specify days: 1 - Sunday, 2 - Monday, etc.
where DATEPART(dw,[date]) in (2, 5)
option (maxrecursion 0)
--if MY_TABLE does exist
--insert into MY_TABLE
--select #P_ID,
-- [date],
-- 'NOT SENT',
-- cast(getdate() as date),
-- #USER_ID
--from cte
--where DATEPART(dw,[date]) in (2, 5)
--option (maxrecursion 0)

Generate date range in between 2 dates

for the last couple of hours I have been breaking my head over this.
I want to create a result set which contains a series of dates like this:
2011-07-05
2011-07-04
2011-07-03
2011-07-02
2011-07-01
2011-06-30
2011-06-29
2011-06-28
...
Ideally between 2 dates given. But If I can say the last 30 days or the last 100 days from now that would be fine also.
Normally I would this with a CTE like this
;WITH Dates AS
(
SELECT CONVERT(DATE, GETDATE()) [Date]
UNION ALL
SELECT DATEADD(DAY,-1, [Date])
FROM Dates
WHERE [Date] > DATEADD(DAY, -30, CONVERT(DATE, GETDATE()))
)
SELECT [Date]
But I am not allowed to use any statements that can't be executed in a subquery. The program I am using executes queries like this:
Select *
From (
TheQuery
) as t1
This means I can't use declares, no stored procedures, no CTEs..
Is there any way I can obtain the dataset I need with these limitations?
I am using azure SQL
You can use a recursive cte if you put it in a table valued function
CREATE FUNCTION FnDateRange
(
#startDate date,
#endDate date
)
RETURNS #DateRange Table
(myDate date)
AS
begin
with Dates_rte as
(
select #startDate myDate
union all
select cast(dateadd(day,1,myDate) as date)
from Dates_rte
where cast(dateadd(day,1,myDate) as date) <= #endDate
)
insert into #DateRange
select * from Dates_rte option (maxrecursion 0)
return
end
GO
select * from fnDateRange('2017-07-01','2017-07-06')
If you dont't want create a calendar table or a number table, nor use existing table to generate numbers/ date (see for example https://sqlperformance.com/2013/01/t-sql-queries/generate-a-set-1)
you could use something like this:
SELECT DATEADD(DAY, -B.N1+1, CONVERT(DATE, GETDATE())) AS D1
FROM
(SELECT 1 AS N1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10) A
CROSS JOIN (SELECT 1 AS N1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10) B
DECLARE #fromdate DATE
DECLARE #todate DATE
DECLARE #tcaldate Table (CalenderDate Date);
set #fromdate='2017-04-17'
set #todate='2017-05-13'
INSERT INTO #tcaldate SELECT TOP (DATEDIFF(DAY, #fromdate, #todate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #fromdate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
Select * from #tcaldate
Hope this helps...
Well, I think the easiest way is to create calendar table and in subquery just select dates between dates.
You can do this by this query:
CREATE TABLE dbo.Calendar ([Date] date)
DECLARE #startDate date, #endDate date
SET #startDate = '2000-01-01'
SET #endDate = '2020-12-31'
WHILE #startDate <= #endDate
BEGIN
INSERT INTO dbo.Calendar
SELECT #startDate
SET #startDate = DATEADD(DD,1,#startDate)
END
Selecting dates:
Select *
From dbo.Calendar WHERE [Date] BETWEEN #date1 AND #date2

Table-Valued-Function with cte

I was trying to loop through a cte with a given parameters From and To Date. I read this create while loop with cte and tried it on my query but the result shows only the start date and the next date.
DECLARE #fromdate datetime
DECLARE #todate datetime
SET #fromdate='2013-07-23'
SET #todate='2013-07-24'
;with dates(num) as (
select #fromdate as fromdate
union all
select #fromdate + 1
from dates
where dates.num < #todate
),
T(employee_id,actualogin,actuallogout) as (
select d.*
from dates cross apply
dbo.ufn_GET_ATTENDANCE(dates.num) d
)
SELECT * FROM T
OPTION (MAXRECURSION 0)
I would be inclined to first get the list of dates and then apply the function:
with dates as (
select #fromdate as date
union all
select date + 1
from dates
where dates.date < #todate
),
T(employee_id,actualogin,actuallogout) as (
select t.*
from dates cross apply
dbo.ufn_GET_ATTENDANCE(dates.date)
)

generate sql temp table of sequential dates to left outer join to

i have a table of data that i want to select out via stored proc such that users can connect a MS excel front end to it and use the raw data as a source to graph.
The problem with the raw data of the table is there exist gaps in the dates because if there is no data for a given day (there is no records with that date) then when users try to graph it it creates problems.
I want too update my stored proc to left outer join to a temp table of dates so that the right side will come in as nulls that i can cast to zero's for them to have a simple plotting experience.
how do i best generate a one field table of dates between a start and end date?
In SQL Server 2005 and up, you can use something like this (a Common Table Expression CTE) to do this:
DECLARE #DateFrom DATETIME
SET #DateFrom = '2011-01-01'
DECLARE #DateTo DATETIME
SET #DateTo = '2011-01-10'
;WITH DateRanges AS
(
SELECT #DateFrom AS 'DateValue'
UNION ALL
SELECT DATEADD(DAY, 1, DateValue)
FROM DateRanges
WHERE DateValue < #DateTo
)
SELECT * FROM DateRanges
You could LEFT OUTER JOIN this CTE against your table and return the result.
Another way to do it is with a memory table. It won't choke due to recursion limitations like some of the above solutions.
DECLARE #dates AS TABLE ([Date] date);
DECLARE #date date = {d '2010-10-01'};
DECLARE #endDate date = {d '2010-11-01'};
while (#date < #endDate)
BEGIN
INSERT INTO #dates VALUES (#date);
SET #date = dateadd(DAY, 1, #date)
END
SELECT * FROM #dates;
SQL Fiddle
One way would be with a CTE:
with cte_dates as (
select cast('20110119' as datetime) as [date]
union all
select dateadd(dd, 1, [date])
from cte_dates
where dateadd(dd, 1, [date]) <= '20111231'
)
select [date], YourColumn
from cte_dates
left join YourTable
on ...
option (maxrecursion 0);