CTE loop in loop [duplicate] - sql

This question already has an answer here:
CTE looping query
(1 answer)
Closed 8 years ago.
situation: I got 2 tables. One where there are 2 fields StartDate & EndDate. And one table where there is only one field date. So when you got 3 days between Start- and Enddate. He must insert 3 rows in the new table.
I have next code and it insert perfect my line in the table Availability.
with View_Solidnet_Training as
(
select cast('2013-04-09' as datetime) DateValue
union all
select DateValue + 1
from View_Solidnet_Training
where DateValue + 1 <= cast('2013-04-11' as datetime)
)
insert into OBJ_Availability
select 34, DateValue, 'AM', 2, 'Test' from View_Solidnet_Training;
But now, after he inserts the lines in the new table, he stops. But after the loop, he must change the the Start and EndDate again in the new values of the next row in the view: View_Solidnet_Training.
So is there a possible solution, or should I make a new loop where I check if ID of the view is not zero?

As i understand the your question i think you should start from getting the dates into list and then do insert.
Sample:
create table Dates
(
startdate datetime,
endDate datetime
)
insert dates
SELECT '2013-04-06','2013-04-08'
SELECT * from Dates
Declare #date int
Declare #tbl table
(
date_ datetime
)
SELECT #date = datediff(day,startDate-1,EndDate) from Dates
SELECT #date
while(#date != 0 )
Begin
insert into #tbl
SELECT dateadd(day,#date,StartDate-1) from dates
set #date = #date -1
END
/*
--TO-DO
--Update StartDate and EndDate values in table Dates
-- insert YourTable
-- select date_ from #tbl
*/
SELECT * from #tbl
order by date_

Related

How can I assign values to next n rows in SQL based on the value of a previous row?

Please help,
I need to be able to determine if the next seven rows fall within 7 days of a row's date. How to do this in Sql or excel?
I am at a loss.
Hope this will work for you.
create table #Temp
(
ID int identity(1,1),
InsertedDate datetime,
Data_XYZ varchar(50)
)
truncate table #Temp
insert into #Temp
values
('1/1/2020','A'),
('1/2/2020','B'),
('1/3/2020','C'),
('1/4/2020','D'),
('1/5/2020','E'),
('1/8/2020','F'),
('1/10/2020','G'),
('1/11/2020','H'),
('1/17/2020','I'),
('1/20/2020','J'),
('1/25/2020','K'),
('1/26/2020','L'),
('1/26/2020','M'),
('1/31/2020','N')
declare #InsertedDate datetime; set #InsertedDate='1/5/2020';
select top 7 *
from #Temp
where ID > (select ID from #Temp where InsertedDate = #InsertedDate)
and InsertedDate between #InsertedDate and DATEADD(dd, 7, #InsertedDate)
In SQL, you can use lead(). You haven't provided a database tag -- or even defined what you mean by "7 days".
But the idea is:
select t.*,
(case when lead(date, 7) over (order by date) <= date + interval '7 day'
then 'within'
else 'without'
end)
from t;

Query to aggregate totals between dates

I have data of the following format:
Date Value
08/28 100
09/01 1
09/01 5
09/10 2
I would like my output to be:
Date Value
08/28 100
08/29 100
08/30 100
08/31 100
09/01 106
09/02 106
.
.
.
09/10 108
I'm just getting started with SQL, so any help would be appreciated. What I have right now is below, but that's not really close to what I seek:
SELECT Date, COUNT(DISTINCT(Service)) AS Value
FROM [Directory]
WHERE Date <= #myDate
GROUP BY Date ORDER BY Date
First, you can use a sub query to get the aggregate values
SELECT Date, (SELECT SUM(Value) FROM Directory d WHERE d.Date <= Directory.Date)
FROM [Directory]
WHERE Date <= #myDate
ORDER BY Date
Which would give you something that looks like this:
Date Value
08/28 100
09/01 101
09/01 106
09/10 108
Then you can add a Date table as sgeddes suggested. This article explains if fairly well: http://michaelmorley.name/how-to/create-date-dimension-table-in-sql-server
Then you can modify your query like so
SELECT DateTable.Date, (SELECT SUM(Value) FROM Directory d WHERE d.Date <= Directory.Date)
FROM [Directory] LEFT OUTER JOIN DateTable on Directory.Date = DateTable.Date
WHERE DateTable.Date <= #myDate
ORDER BY DateTable.Date
To get the data format you're looking for.
Based on sgeddes suggestion:
SELECT a.Date, COUNT(DISTINCT(d.Service)) AS Value
FROM [Directory] d
LEFT OUTER JOIN [Date Table] a on d.Date = a.Date
WHERE Date <= #myDate
GROUP BY Date
ORDER BY Date
Use following script in sqlserver :
BEGIN
--If exist then drop temp tables
DROP TABLE #YOURTABLE;
DROP TABLE #TEST1;
DECLARE #MINDATE DATETIME;
DECLARE #MAXDATE DATETIME;
CREATE TABLE #YOURTABLE(
CDATE DATE,
VALUE INT
);
INSERT INTO #YOURTABLE VALUES ('08/28/2014',100),('09/01/2014',1),('09/01/2014',5),('09/10/2014',100);
--select start date and end date from your table
SELECT #MINDATE=MIN(CDATE),#MAXDATE=MAX(CDATE) FROM #YOURTABLE;
CREATE TABLE #TEST1(
CDATE DATE,
VALUE INT
);
;WITH CALENDAR
AS (
SELECT #MINDATE CDATE
UNION ALL
SELECT CDATE + 1
FROM CALENDAR
WHERE CDATE + 1 <= #MAXDATE
)
-- insert all dates with 0 value in temp table
INSERT INTO #TEST1 SELECT CDATE,0 FROM CALENDAR;
--delete dates which are already there in your table
DELETE FROM #TEST1 WHERE CDATE IN (SELECT CDATE FROM #YOURTABLE)
-- insert all dates with values from your table to temporary table which holds dates which are not in your table
INSERT INTO #TEST1 SELECT * FROM #YOURTABLE;
SELECT T1.CDATE,(SELECT SUM(VALUE) FROM #TEST1 T2 WHERE T2.CDATE<=T1.CDATE) FROM #TEST1 T1
END

Looping in SELECT statement in ms sqlserver [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
SQL Server 2008 Generate a Series of date times
I have to Loop through a startDate and endDate
The SELECT statement should produce result as..
Expected Output :
------------
Date
------------
09/01/2012 -> startDate
09/02/2012
09/03/2012
.
.
.
.
09/30/2012 -> endDate
i tried
declare #startDate datetime , #endDate endDate
set #startDate='09/01/2012'
set #endDate='09/30/2012'
while DATEDIFF(#startDate,#endDate)!=-1
begin
select #startDate as Date
set #startDate = DATEADD(day,2,#startDate)
end
But its not working out..
it generates 30 outputs..
i want the dates in a single output as in the expected output..
where am i going wrong here guys ?
That will give you a resultset for each loop iteration as you select per iteration.
If you want a single resultset insert into a temp table/variable per iteration then select from it or
;with T(day) as
(
select #startDate as day
union all
select day + 1
from T
where day < #endDate
)
select day as [Date] from T
If you want to use a WHILE loop:
declare #startDate datetime , #endDate datetime
set #startDate='09/01/2012'
set #endDate='09/30/2012'
create table #temp (startDate datetime)
while #startDate <= #endDate
begin
insert into #temp
select #startDate as Date
set #startDate = DATEADD(day,1,#startDate)
end
select *
from #temp
drop table #temp
see SQL Fiddle with Demo
You could create a temp table for the values and select from that in the end, after the iteration.
declare #temp table (TheDate date)
declare #startDate datetime , #endDate datetime
set #startDate='09/01/2012'
set #endDate='09/30/2012'
while DATEDIFF(day, #startDate, #endDate)!=-1
begin
insert into #temp (thedate) values (#startDate)
set #startDate = DATEADD(day,2,#startDate)
end
select * from #temp
edit: The cte Alex suggest is imo a much cleaner way to do it, and more of a sql way to do it, without using loops or cursors.

UPDATE field based on another row filed value

I have a table stored on a SQL Server 2008, that associate a value to a date range.
DateFrom DateTo Value
2012-01-01 2012-02-01 10
2012-02-02 2012-02-15 15
The application that deal with this table, can insert a new range between the existings.
For example, If i insert
DateFrom DateTo Value
2012-02-07 2012-02-10 12
The result must be
DateFrom DateTo Value
2012-01-01 2012-02-01 10
2012-02-02 2012-02-06 15
2012-02-07 2012-02-10 12
2012-02-11 2012-02-15 15
I can do that programmatically from the application, but I wonder if there is some fast SQL statement that make me able to set the data values by referencing other row's field and performing data operation on it.
A MUST requirement is that the date range must represent a time sequence, two range cannot span each other.
I've written an example based on the example I gave you in a comment, it may do what you want. Since, in general terms, there might be multiple rows to insert/delete, it's best to define them all separately, then use a MERGE to perform the overall change.
I've also assumed that it's okay to delete/insert to achieve the splitting - you can't update and produce 2 rows from 1, so you'd always have to do an insert, and the symmetry is cleaner if I do both:
declare #T table (DateFrom datetime2, DateTo datetime2,Value int)
insert into #T(DateFrom , DateTo , Value) VALUES
('20120101', '20120201', 10),
('20120202', '20120206', 15),
('20120207', '20120210', 12),
('20120211', '20120215', 15)
select * from #t order by DateFrom
declare #NewFrom datetime2 = '20120205'
declare #NewTo datetime2 = '20120208'
declare #NewValue int = 8
--We need to identify a) rows to delete, b) new sliced rows to create, and c) the new row itself
;With AlteredRows as (
select #NewFrom as DateFrom,#NewTo as DateTo,#NewValue as Value,1 as toInsert
union all
select DateFrom,DATEADD(day,-1,#NewFrom),Value,1 from #t where #NewFrom between DATEADD(day,1,DateFrom) and DateTo
union all
select DATEADD(day,1,#NewTo),DateTo,Value,1 from #t where #NewTo between DateFrom and DATEADD(day,-1,DateTo)
union all
select DateFrom,DateTo,0,0 from #t where DateTo > #NewFrom and DateFrom < #NewTo
)
merge into #t t using AlteredRows ar on t.DateFrom = ar.DateFrom and t.DateTo = ar.DateTo
when matched and toInsert=0 then delete
when not matched then insert (DateFrom,DateTo,Value) values (ar.DateFrom,ar.DateTo,ar.Value);
select * from #t order by DateFrom
It may be possible to re-write the CTE so that it's a single scan of #t - but I only think it's worth doing that if performance is critical.
I've had similar problems in the past, and found that if the range needs to be continuous the best approach is to do away with the End Date of the range, and calculate this as the Next start date. Then if needs be create a view as follows:
SELECT FromDate,
( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM YourTable b
WHERE b.FromDate > a.FromDate
) [ToDate],
Value
FROM YourTable a
This ensures that 2 ranges can never cross, however does not necessarily ensure no work is required upon insert to get the desired result, but it should be more maintainable and have less scope for error than storing both the start and end date.
ADDENDUM
Once I had written out all of the below I realised it does not improve maintainability that much to do away with the DateTo Field, it still requires a fair amount of code for the validation, but here's how I would do it anyway.
DECLARE #T table (DateFrom DATE, Value INT)
INSERT INTO #T VALUES ('20120101', 10), ('20120202', 15), ('20120207', 12), ('20120211', 15)
DECLARE #NewFrom DATE = '20120209',
#NewTo DATE = '20120210',
#NewValue INT = 8
-- SHOW INITIAL VALUES FOR DEMONSTATIVE PURPOSES --
SELECT DateFrom,
ISNULL(( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
), CAST(GETDATE() AS DATE)) [DateTo],
Value
FROM #t a
ORDER BY DateFrom
;WITH CTE AS
( SELECT DateFrom,
( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
) [DateTo],
Value
FROM #t a
),
MergeCTE AS
( SELECT #NewFrom [DateFrom], #NewValue [Value], 'INSERT' [RowAction]
WHERE #NewFrom < #NewTo -- ENSURE A VALID RANGE IS ENTERED
UNION ALL
-- INSERT A ROW WHERE THE NEW DATE TO SLICES AN EXISTING PERIOD
SELECT DATEADD(DAY, 1, #NewTo), Value, 'INSERT'
FROM CTE
WHERE #NewTo BETWEEN DateFrom AND DateTo
UNION ALL
-- DELETE ALL ENTRIES STARTING WITHIN THE DEFINED PERIOD
SELECT DateFrom, Value, 'DELETE'
FROM CTE
WHERE DateFrom BETWEEN #NewFrom AND #NewTo
)
MERGE INTO #t t USING MergeCTE c ON t.DateFrom = c.DateFrom AND t.Value = c.Value
WHEN MATCHED AND RowAction = 'DELETE' THEN DELETE
WHEN NOT MATCHED THEN INSERT VALUES (c.DateFrom, c.Value);
SELECT DateFrom,
ISNULL(( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
), CAST(GETDATE() AS DATE)) [DateTo],
Value
FROM #t a
ORDER BY DateFrom
You can use a cursor to get each row from the table at a time and aftwerwards do the necessary calculations.
If NewDateFrom >= RowDateFrom and NewDateFrom <= RowDateTo ...
Check this article to see how to make a cursor.

Grouping by contiguous dates, ignoring weekends in SQL

I'm attempting to group contiguous date ranges to show the minimum and maximum date for each range. So far I've used a solution similar to this one: http://www.sqlservercentral.com/articles/T-SQL/71550/ however I'm on SQL 2000 so I had to make some changes. This is my procedure so far:
create table #tmp
(
date smalldatetime,
rownum int identity
)
insert into #tmp
select distinct date from testDates order by date
select
min(date) as dateRangeStart,
max(date) as dateRangeEnd,
count(*) as dates,
dateadd(dd,-1*rownum, date) as GroupID
from #tmp
group by dateadd(dd,-1*rownum, date)
drop table #tmp
It works exactly how I want except for one issue: weekends. My data sets have no records for weekend dates, which means any group found is at most 5 days. For instance, in the results below, I would like the last 3 groups to show up as a single record, with a dateRangeStart of 10/6 and a dateRangeEnd of 10/20:
Is there some way I can set this up to ignore a break in the date range if that break is just a weekend?
Thanks for the help.
EDITED
I didn't like my previous idea very much. Here's a better one, I think:
Based on the first and the last dates from the set of those to be grouped, prepare the list of all the intermediate weekend dates.
Insert the working dates together with weekend dates, ordered, so they would all be assigned rownum values according to their normal order.
Use your method of finding contiguous ranges with the following modifications:
1) when calculating dateRangeStart, if it's a weekend date, pick the nearest following weekday;
2) accordingly for dateRangeEnd, if it's a weekend date, pick the nearest preceding weekday;
3) when counting dates for the group, pick only weekdays.
Select from the resulting set only those rows where dates > 0, thus eliminating the groups formed only of the weekends.
And here's an implementation of the method, where it is assumed, that a week starts on Sunday (DATEPART returns 1) and weekend days are Sunday and Saturday:
DECLARE #tmp TABLE (date smalldatetime, rownum int IDENTITY);
DECLARE #weekends TABLE (date smalldatetime);
DECLARE #minDate smalldatetime, #maxDate smalldatetime, #date smalldatetime;
/* #1 */
SELECT #minDate = MIN(date), #maxDate = MAX(date)
FROM testDates;
SET #date = #minDate - DATEPART(dw, #minDate) + 7;
WHILE #date < #maxDate BEGIN
INSERT INTO #weekends
SELECT #date UNION ALL
SELECT #date + 1;
SET #date = #date + 7;
END;
/* #2 */
INSERT INTO #tmp
SELECT date FROM testDates
UNION
SELECT date FROM #weekends
ORDER BY date;
/* #3 & #4 */
SELECT *
FROM (
SELECT
MIN(date + CASE DATEPART(dw, date) WHEN 1 THEN 1 WHEN 7 THEN 2 ELSE 0 END)
AS dateRangeStart,
MAX(date - CASE DATEPART(dw, date) WHEN 1 THEN 2 WHEN 7 THEN 1 ELSE 0 END)
AS dateRangeEnd,
COUNT(CASE WHEN DATEPART(dw, date) NOT IN (1, 7) THEN date END) AS dates,
DATEADD(d, -rownum, date) AS GroupID
FROM #tmp
GROUP BY DATEADD(d, -rownum, date)
) s
WHERE dates > 0;