How to generate date range sequence without temp table in SQL - sql-server

I want to convert some line of code from Oracle query to MSSQL.
WITH DATE_MONTHS AS
(
SELECT TO_CHAR(ADD_MONTHS(TO_DATE(TRUNC(TO_DATE(P_REQUIRED_DATE),'MON')), - LEVEL
+1),'DD-MON-YYYY') MONTHS FROM DUAL
CONNECT BY LEVEL <= P_MONTH_RANG
)
SELECT * from DATE_MONTHS
Parameters:
P_REQUIRED_DATE i.e sysdate
P_MONTH_RANG i.e 4
Result:
01-05-2017
01-04-2017
01-03-2017
01-02-2017

One simple way is to use tally table and generate like below:
declare #P_Required_Date date = '2015-05-01'
declare #P_Month_Rang int = 4
Select top (#P_Month_Rang) Dts = DateAdd(month, -(Row_Number() over(order by (Select NULL))-1), #P_Required_Date) from
master..spt_values s1, master..spt_values s2
Output as below:
+------------+
| Dts |
+------------+
| 2015-05-01 |
| 2015-04-01 |
| 2015-03-01 |
| 2015-02-01 |
+------------+
Your CTE approach:
declare #P_Required_Date date = '2015-05-01'
declare #P_Month_Rang int = 4
;with Date_Months as
(
Select #P_Required_Date as Dates, 1 as Levl
Union all
Select DateAdd(MONTH,-1, Dates), Levl+1 as Levl from Date_Months
where Levl < #P_Month_Rang
)
Select convert(varchar(10), dates, 103) from Date_Months
For converting to your dd-mm-yyyy format one way is to do convert with option 103 or use Format.

Another option using Stacked Ctes
declare #fromdate date = '20150501';
declare #months int = 4;
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (#months)
[Date]=convert(date,dateadd(month,-(row_number() over(order by (select 1))-1),#fromdate))
from n as deka cross join n as hecto cross join n as kilo cross join n as tenK
order by [Date] desc
)
select [Date] = convert(char(10),[date],105)
from dates;
rextester demo: http://rextester.com/UUW2271
returns:
+------------+
| Date |
+------------+
| 01-05-2015 |
| 01-04-2015 |
| 01-03-2015 |
| 01-02-2015 |
+------------+
Benchmarks & Performance testing: Generate a set or sequence without loops - 2 - Aaron Bertrand

Related

Group Values between date time range

I have a table that looks like this:
And I want to group the data order by the daydifference=1, the rows where the daydifference is bigger than 1, will be grouped between the days that have difference=1. So this will be like this:
So I tried this way:
SELECT
InicialData
,FinalDate
FROM #ValueAnalysis
WHERE WorkDays=1
GROUP BY InicialData, FinalDate
And now I want to join the orders where the daydifference is bigger than 1.
Can someone help me please?
If you have SQL Server 2017 or newer, you can use STRING_AGG. For example like this:
SELECT va.InitialDate,
va.FinalDate,
STRING_AGG(cc.Order_,',') WITHIN GROUP (ORDER BY cc.Order_ ASC) as Order_1
FROM ValueAnalysis va
OUTER APPLY (
SELECT Order_
FROM ValueAnalysis
WHERE va.InitialDate = InitialDate or va.FinalDate = FinalDate
) as cc
WHERE DayDiff = 1
GROUP BY va.InitialDate, va.FinalDate
Output:
| InitialDate | FinalDate | Order_1 |
|-------------|------------|---------|
| 2020-01-02 | 2020-01-02 | 21,23 |
| 2020-01-03 | 2020-01-03 | 22 |
| 2020-01-04 | 2020-01-04 | 23,24 |
EDIT#1
If you need generate new table and include dates that are not in original dataset, then you can use recursive CTE:
;WITH cte AS (
SELECT MIN(InitialDate) InitialDate,
MAX(FinalDate) FinalDate,
FROM ValueAnalysis
UNION ALL
SELECT DATEADD(day,1,InitialDate)
FinalDate
FROM cte
WHERE InitialDate < FinalDate
)
SELECT c.InitialDate InitialDate,
c.InitialDate FinalDate,
STRING_AGG(va.Order_,',') WITHIN GROUP (ORDER BY va.Order_ ASC) as Order_1
FROM cte c
LEFT JOIN ValueAnalysis va
ON va.InitialDate = c.InitialDate or va.FinalDate = c.InitialDate
GROUP BY c.InitialDate, c.FinalDate
OPTION (MAXRECURSION 100)

How to repeat values in case of null values on left join

I have a table with a calendar, and a table with rates. In the table with the rates, there are no values existing for days in the weekend. I'm trying to join the two, in order to have a table where there is a rate for all days, and I need the rates in the weekend to be the latest available rate. Instad of it showing NULL values, as it would when you make a left join and the record doesn't exist, it should just take the latest available, repeating the previous value.
I have the below code, which works, but it takes 2 min to do on 7,397 rows, which is way too long.
Does anyone know a faster way to get the same results?
SELECT
c.CalendarID,
MAX(r.RateID)
FROM Dim_Calendar c
LEFT JOIN Dim_Rates r ON r.RateDate <= c.CalendarID
What I get without <= and just an = is the following
CalendarID | RateID
20131001 | 2
20131002 | 3
20131003 | 4
20131004 | 5
20131005 | NULL
20131006 | NULL
20131007 | 6
And this is the desired table:
CalendarID | RateID
20131001 | 2
20131002 | 3
20131003 | 4
20131004 | 5
20131005 | 5
20131006 | 5
20131007 | 6
You can use LAG() window function:
SELECT c.CalendarID,
COALESCE(
r.RateID,
LAG(r.RateID, 1) OVER (ORDER BY c.CalendarID),
LAG(r.RateID, 2) OVER (ORDER BY c.CalendarID)
) RateID
FROM Dim_Calendar c LEFT JOIN Dim_Rates r
ON r.RateDate = c.CalendarID
ORDER BY c.CalendarID
See the demo.
Results:
> CalendarID | RateID
> ---------: | :-----
> 20131001 | 2
> 20131002 | 3
> 20131003 | 4
> 20131004 | 5
> 20131005 | 5
> 20131006 | 5
> 20131007 | 6
You could use a correlated subquery to fill the gaps:
SELECT
c.CalendarID,
(SELECT TOP 1 r.RateID FROM Dim_Rates r
WHERE r.RateDate <= c.CalendarID AND r.RateID IS NOT NULL
ORDER BY r.RateDate DESC) AS RateID
FROM Dim_Calendar c
ORDER BY c.CalendarID;
This query can be improved by using the following index:
CREATE INDEX idx ON Dim_Rates (RateDate, RateID);
As pointed out, you need to check for proper and covering indexing. It appears you are running a against a DW DB and if that is the case then you can replace the CTE with indexed temp tables if the esitmated row count approximation is way off in the query plan.
;WITH NormalizedData AS
(
SELECT
RateID,CalendarID,
VirtualGroupID = SUM(LastRecordBeforeGap) OVER (ORDER BY CalendarID ROWS UNBOUNDED PRECEDING)
FROM
(
SELECT RateID,CalendarID,
LastRecordBeforeGap = CASE WHEN LEAD(RateID) OVER(ORDER BY CalendarID) IS NULL AND RateID IS NOT NULL THEN 1 ELSE 0 END
FROM
Dim_Calendar c
LEFT JOIN Dim_Rates r ON r.RateDate = c.CalendarID
)AS x
)
SELECT
RateID = ISNULL(RateID, SUM(RateID) OVER(PARTITION BY VirtualGroupID)),
CalendarID
FROM
NormalizedData

T-SQL n rows for amount of days between 2 days

I have a question which I cannot answer myself. I'm using T-SQL and a basic query:
SELECT OpenArt, DayFrom, Dayto
FROM Locations
WHERE OpenArt = 'closed' AND S_ID = '123'
I want to get every date, where my location is closed. This works so far, as the output is something like:
| OpenArt | DayFrom | DayTo |
+---------+------------+------------+
| Closed | 06.12.2019 | 09.12.2019 |
| Closed | 23.12.2019 | 31.12.2019 |
Basically, it shows a range, when a location is closed. However, for an API, I need to send 1 row for each closed day. So for the range 23.12.2019 - 31.12.2019, I'd need 9 single rows like:
| OpenArt | DayClosed |
+---------+------------+
| Closed | 23.12.2019 |
| Closed | 24.12.2019 |
| Closed | 25.12.2019 |
and so on. The naming of the headers aren't that important, I can adjust that. I simply don't know how to "dupe" the results, depending on the range between the 2 days. I know there is datediff(), but that is all I could come up with. Thanks in advance.
There are no restrictions, there can be a new temp_table, an UDF or anything that works.
One option is to use an ad-hoc tally table in concert with a CROSS APPLY,
Example
Set Dateformat DMY
Declare #YourTable Table ([OpenArt] varchar(50),[DayFrom] date,[DayTo] date) Insert Into #YourTable Values
('Closed','06.12.2019','09.12.2019')
,('Closed','23.12.2019','31.12.2019')
Select OpenArt
,DayClosed = D
From #YourTable
Cross Apply (
Select Top (DateDiff(DAY,[DayFrom],[DayTo])+1)
D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),[DayFrom])
From master..spt_values n1,master..spt_values n2
) B
Returns
Or Yet another option with known date range
Declare #Date1 date = '2019-01-01'
Declare #Date2 date = '2020-12-31'
Select OpenArt
,DayClosed = D
From #YourTable
Join (
Select Top (DateDiff(DAY,#Date1,#Date2)+1)
D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),#Date1)
From master..spt_values n1,master..spt_values n2
) B on D between [DayFrom] and [DayTo]

T-SQL create multiply records from one records

I have a cost record and I would like to create N records from it.
The children records have some different parameters.
For example:
The parents record:
date | amount | duration
20170201 | 5000 | 5 months
The children records:
date | amount | duration
20170301 | 1000 | 1 months
20170401 | 1000 | 1 months
20170501 | 1000 | 1 months
20170601 | 1000 | 1 months
20170701 | 1000 | 1 months
How can I do this without iteration? Without cursor or while?
Following SQL CTE query could be used based on Abdul's solution
/*
Create Table PARENT (PARENT_DATE DATE, PARENT_AMOUNT DECIMAL(18,2),PARENT_MONTH INT)
INSERT INTO PARENT SELECT '20170201',5000 ,5
INSERT INTO PARENT SELECT '20180601',120 ,3
*/
;WITH CTE_CHILD
AS (
SELECT
Parent_Date,
Parent_Amount,
Parent_Month,
DateAdd(Month, 1, Parent_Date) as Child_Date,
Parent_Amount/Parent_Month AS Child_Amount,
1 AS Child_Duration
FROM Parent
UNION ALL
SELECT
Parent_Date,
Parent_Amount,
Parent_Month,
DateAdd(Month, 1, Child_Date) as Child_Date,
Child_Amount,
Child_Duration
FROM CTE_CHILD
WHERE
DateAdd(Month, 1, Child_Date) <= DateAdd(Month, Parent_Month, Parent_Date)
)
SELECT
Child_Date,
Child_Amount,
Child_Duration
FROM CTE_CHILD
assuming you have a table like below:
create table tblRecords ( date int, amount money, duration int);
insert into tblRecords values
(20170201,5000,5),
(20180101,9000,3);
you can use a query like below:
select
date= date + r*100
,amount= amount/duration
,duration =1
from tblRecords
cross apply
(
select top (select duration)
r= row_number() over(order by (select null))
from
sys.objects s1
cross join
sys.objects s2
) h
see working demo
One method is CTE.
DECLARE #PARENT AS TABLE
(PARENT_DATE DATE, PARENT_AMOUNT DECIMAL(18,2),PARENT_MONTH INT)
INSERT INTO #PARENT
SELECT '20170201',5000 ,5
;WITH CTE_CHILD
AS (
SELECT DATEADD(MONTH,1,PARENT_DATE) AS CHILD_DATE
,PARENT_AMOUNT/PARENT_MONTH AS CHILD_AMOUNT
,1 AS CHILD_DURATION
FROM #PARENT
WHERE DATEADD(MONTH,1,PARENT_DATE) <= DATEADD(MONTH,PARENT_MONTH,PARENT_DATE)
UNION ALL
SELECT DATEADD(MONTH,1,CHILD_DATE)
,PARENT_AMOUNT/PARENT_MONTH
,1
FROM CTE_CHILD
INNER JOIN #PARENT ON DATEADD(MONTH,1,CHILD_DATE) <= DATEADD(MONTH,PARENT_MONTH,PARENT_DATE)
)
SELECT * FROM CTE_CHILD
option (maxrecursion 0)
Output:-
CHILD_DATE CHILD_AMOUNT CHILD_DURATION
2017-03-01 1000.0000000000000 1
2017-04-01 1000.0000000000000 1
2017-05-01 1000.0000000000000 1
2017-06-01 1000.0000000000000 1
2017-07-01 1000.0000000000000 1

Stop sum when value reached

Assume Table1:
|PaymentID|CashAmount|
----------------------
| P1 | 3000|
| P2 | 5000|
| P3 | 8000|
| P4 | 700|
| P5 | 5500|
| P6 | 1900|
If I want to sum of CashAmount to be 'at least' 9000. PaymentID order should be the same.
Expected Result:
|PaymentID|CashAmount|
----------------------
| P1 | 3000|
| P2 | 5000|
| P3 | 8000|
If I want to sum of CashAmount to be 'at least' 4000. PaymentID order should be the same.
Expected Result:
|PaymentID|CashAmount|
----------------------
| P1 | 3000|
| P2 | 5000|
I had a look at limiting the rows to where the sum a column equals a certain value in MySQL. But the accepted answer is not working with MSSQL and is not exactly what I'm looking for. Most of the answers there I've tested and they return only rows that the total amount is less than, not at least specific value.
SQL Server 2005 and Later
SELECT *
FROM TableName t
CROSS APPLY (SELECT SUM(Amount)
FROM TableName
WHERE [Date] <= t.[DATE]) c(AmtSum)
WHERE AmtSum <= 13
SQL Server 2012 and Later
SELECT *
FROM (
SELECT *
,SUM(Amount) OVER (ORDER BY [Date], Amount) AmtSum
FROM TableName
)t
WHERE AmtSum <= 13
According to your new input I changed my approach slightly. Hope this is what you need...
EDIT: Here's the version with SUM(x) OVER(...):
DECLARE #payment TABLE(PaymentID VARCHAR(10),CashAmount INT);
INSERT INTO #payment VALUES
('P1',3000)
,('P2',5000)
,('P3',8000)
,('P4',700)
,('P5',5500)
,('P6',1900);
DECLARE #myMinToReach INT=9000;
WITH SortedPayment AS
(
SELECT ROW_NUMBER() OVER(ORDER BY PaymentID) AS inx
,SUM(CashAmount) OVER(ORDER BY PaymentID) AS Summa
FROM #payment
)
SELECT * FROM SortedPayment
WHERE inx<=(SELECT TOP 1 x.inx
FROM SortedPayment AS x
WHERE Summa>#myMinToReach
ORDER BY Summa ASC);
And that's the old version for SQL-Server < 2012
DECLARE #payment TABLE(PaymentID VARCHAR(10),CashAmount INT);
INSERT INTO #payment VALUES
('P1',3000)
,('P2',5000)
,('P3',8000)
,('P4',700)
,('P5',5500)
,('P6',1900);
DECLARE #myMinToReach INT=4000;
WITH SortedPayment AS
(
SELECT ROW_NUMBER() OVER(ORDER BY PaymentID) AS inx,*
FROM #payment
)
,Accumulated AS
(
SELECT tbl.*
FROM
(
SELECT SortedPayment.*
,Accumulated.Summa
FROM SortedPayment
CROSS APPLY
(
SELECT SUM(ps2.CashAmount) AS Summa
FROM SortedPayment AS ps2
WHERE ps2.inx<=SortedPayment.inx
) AS Accumulated
) AS tbl
)
SELECT * FROM Accumulated
WHERE inx<=(SELECT TOP 1 x.inx
FROM Accumulated AS x
WHERE Summa>#myMinToReach
ORDER BY Summa ASC);
declare #s int;
update table set rc=row_count() over (order by date)
declare #i int;
set #i=1;
while #s<=12 or #i<100000
set #s=#s+(select amount from table where rc=#i+1);
set #i=#i+1;
end
// #s has at least 12

Resources