Related
Please help to fetch the monthnames between two date columns as a concatenated string in a new column.
the above image shows my table data..
if the end date is null, we need to take till end of the year
desired output is
Below find the scripts needed for the table below
CREATE TABLE OfferTest(
[ID] [int] NOT NULL,
[StartDate] [datetime] NULL,
[EndDate] [datetime] NULL
)
insert into OfferTest values(1000,'01/01/2021','05/31/2021')
insert into OfferTest values(2000,'01/01/2021','05/31/2021')
insert into OfferTest values(3000,'07/01/2021','09/30/2021')
insert into OfferTest values(4000,'11/01/2021',NULL)
First create this number table if you don't have one already
SELECT TOP 10000 N=IDENTITY(INT, 0, 1)
INTO dbo.Numbers
FROM master.dbo.syscolumns a CROSS JOIN master.dbo.syscolumns b;
ALTER TABLE dbo.Numbers ADD CONSTRAINT NBR_pk PRIMARY KEY(N);
;with cte(id, Months) as (
select id, STRING_AGG(format(dateadd(m,n.n,StartDate),'MMM'),',')
from offertest o cross join dbo.Numbers n
where dateadd(m,n.n,StartDate)<=isnull(EndDate, '20211231')
group by id)
select o.StartDate,o.EndDate,c.Months
from cte c inner join offertest o on o.id=c.id
TEST
http://sqlfiddle.com/#!18/626e3/1
You can replace '20211231' with getdate() or other limit date
EDIT
You can create the numbers table as a tempt table just by adding #
SELECT TOP 10000 N=IDENTITY(INT, 0, 1)
INTO #Numbers
FROM master.dbo.syscolumns a CROSS JOIN master.dbo.syscolumns b;
ALTER TABLE #Numbers ADD CONSTRAINT NBR_pk PRIMARY KEY(N);
Regarding using the query as subquery, there is no need, just use it as a CTE like this:
;with cte(id, Months) as (
select id, STRING_AGG(format(dateadd(m,n.n,StartDate),'MMM'),',')
from offertest o cross join #Numbers n
where dateadd(m,n.n,StartDate)<=isnull(EndDate, '20211231')
group by id)
,months(StartDate,EndDate,Months) as (
select o.StartDate,o.EndDate,c.Months
from cte c inner join offertest o on o.id=c.id)
Select 'your query here joining Months'
We have a couple million rows of data that we need to "explode" out by adding a row for every date between the started_at date and the ended_at date. The while loop is what is taking the longest in our query.
Any idea on how to optimize or replace it?
IF (OBJECT_ID('TempDb..#exploded_services') IS NOT NULL)
DROP TABLE #exploded_services;
CREATE TABLE #exploded_services
(
target_date date,
move_id varchar(30),
initiation_id varchar(30),
initiated_at date,
booked_at date,
transferee varchar(60),
account_id varchar(30),
mc_id varchar(30),
po varchar(60),
weight int,
service varchar(150),
started_at date,
ended_at date,
location_id nvarchar(64),
description varchar(max),
provider varchar(max),
mode varchar(60),
origin_location_id nvarchar(64),
destination_location_id nvarchar(64),
transferee_phone varchar(40),
transferee_email varchar(100),
status varchar(10),
ordinal int
);
WHILE (#pointer <= #end_date)
BEGIN
INSERT INTO #exploded_services
SELECT
#pointer,
svcs.*
FROM #Services svcs
WHERE #pointer BETWEEN svcs.started_at AND COALESCE(svcs.ended_at,#end_date)
SET #pointer = DATEADD(dd, 1, #pointer)
END;
Create a table with one date column.
Populate it will all possible dates that applies to your services.
Populate your target table with:
INSERT INTO #exploded_services
SELECT
dates_table.date,
svcs.*
FROM #Services svcs
INNER JOIN dates_table ON dates_table.date BETWEEN svcs.started_at AND COALESCE(svcs.ended_at,_arbitrary_end_date_)
This can be achieved using a Tally table. Here's an example on how to do it using one created on the fly with cascading ctes.
WITH
E(n) AS(
SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))E(n)
),
E2(n) AS(
SELECT a.n FROM E a, E b
),
E4(n) AS(
SELECT a.n FROM E2 a, E2 b
),
cteTally(n) AS(
SELECT TOP(DATEDIFF(DD, #pointer, #end_date) + 1)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 n
FROM E4
)
INSERT INTO #exploded_services
SELECT
DATEADD( dd, n #pointer),
svcs.*
FROM #Services svcs
JOIN cteTally t ON DATEADD( dd, n #pointer) BETWEEN svcs.started_at AND COALESCE(svcs.ended_at,#end_date);
You could try below code using CTE to generate all dates needed:
-- cte to get all dates needed
;with cte as (
select #pointer ptr
union all
select DATEADD(dd, 1, #pointer) from cte
where #pointer < #end_date
)
-- adjusted insert query
INSERT INTO #exploded_services
select c.*, s.*
from #Services s
join cte c on c.ptr between s.started_at and coalesce(svcs.ended_at,#end_date)
I'm trying to plug a formula into a query to pull back how much should have run on a particular contract.
The formula itself is quite simple, but I can't find anywhere how to take the minimum date between 3, based on each record separately.
I need to calculate which is the earliest of Term_date, Suspend_date and today's date, some of which may be NULL, on each contract.
And interesting way to approach this is to use cross apply:
select t.contractid, mindte
from table t cross apply
(select min(dte) as mindte
from (values(t.term_date), (t.suspend_date), (getdate())) d(dte)
) d;
CASE
WHEN Term_date < Suspend_date AND Term_date < GETDATE() THEN Term_date
WHEN Suspend_date < GETDATE() THEN Suspend_date
ELSE GETDATE()
END AS MinimumDate
I know a CASE statement will be suggested, but I thought I'd try something different:
;WITH cte (RecordID, CheckDate) AS
( SELECT RecordID, Term_date FROM sourcetable UNION ALL
SELECT RecordID, Suspend_date FROM sourcetable UNION ALL
SELECT RecordID, GETDATE() FROM sourcetable )
SELECT src.RecordID, src.Field1, src.Field2, MinDate = MIN(cte.CheckDate)
FROM sourcetable src
LEFT JOIN cte ON cte.RecordID = src.RecordID
GROUP BY src.RecordID, src.Field1, src.Field2
Here is a method using cross apply to generate a work table from which you can get the minimum date:
-- mock table with sample testing data
declare #MyTable table
(
id int identity(1,1) primary key clustered,
term_date datetime null,
suspend_date datetime null
)
insert into #MyTable (term_date, suspend_date)
select null, null
union all select '1/1/2015', null
union all select null, '1/2/2015'
union all select '1/3/2015', '1/3/2015'
union all select '1/4/2015', '1/5/2015'
union all select '1/6/2015', '1/5/2015'
select * from #MyTable
select datevalues.id, min([date])
from #MyTable
cross apply
(
values (id, term_date), (id, suspend_date), (id, getdate())
) datevalues(id, [date])
group by datevalues.id
I have a table like this:
id START_DATE end_date
1 01/01/2011 01/10/2011
2 01/11/2011 01/20/2011
3 01/25/2011 02/01/2011
4 02/10/2011 02/15/2011
5 02/16/2011 02/27/2011
I want to merge the records where the start_date is just next day of end_date of another record: So the end record should be something like this:
new_id START_DATE end_date
1 01/01/2011 01/20/2011
2 01/25/2011 02/01/2011
3 02/10/2011 02/27/2011
One way that I know to do this will be to create a row based temp table with various rows as dates (each record for one date, between the total range of days) and thus making the table flat.
But there has to be a cleaner way to do this in a single query... e.g. something using row_num?
Thanks guys.
declare #T table
(
id int,
start_date datetime,
end_date datetime
)
insert into #T values
(1, '01/01/2011', '01/10/2011'),
(2, '01/11/2011', '01/20/2011'),
(3, '01/25/2011', '02/01/2011'),
(4, '02/10/2011', '02/15/2011'),
(5, '02/16/2011', '02/27/2011')
select row_number() over(order by min(dt)) as new_id,
min(dt) as start_date,
max(dt) as end_date
from (
select dateadd(day, N.Number, start_date) as dt,
dateadd(day, N.Number - row_number() over(order by dateadd(day, N.Number, start_date)), start_date) as grp
from #T
inner join master..spt_values as N
on N.number between 0 and datediff(day, start_date, end_date) and
N.type = 'P'
) as T
group by grp
order by new_id
You can use a numbers table instead of using master..spt_values.
Try This
Declare #chgRecs Table
(updId int primary key not null,
delId int not null,
endt datetime not null)
While Exists (Select * from Table a
Where Exists
(Select * from table
Where start_date =
DateAdd(day, 1, a.End_Date)))
Begin
Insert #chgRecs (updId, delId , endt)
Select a.id, b.id, b.End_Date,
From table a
Where Exists
(Select * from table
Where start_date =
DateAdd(day, 1, a.End_Date)))
And Not Exists
(Select * from table
Where end_Date =
DateAdd(day, -1, a.Start_Date)))
Delete table Where id In (Select delId from #chgRecs )
Update table set
End_Date = u.endt
From table t join #chgRecs u
On u.updId = t.Id
Delete #delRecs
End
No, was not looking for a loop...
I guess this is a good solution:
taking all the data in a #temp table
SELECT * FROM #temp
SELECT t2.start_date , t1.end_date FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date)
UNION
SELECT START_DATE,end_date FROM #temp WHERE start_date NOT IN (SELECT t2.START_DATE FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date))
AND end_date NOT IN (SELECT t1.end_Date FROM #temp t1 JOIN #temp t2 ON t1.start_date = DATEADD(DAY,1,t2.end_date))
DROP TABLE #temp
Please let me know if there is anything better than this.
Thanks guys.
A recursive solution:
CREATE TABLE TestData
(
Id INT PRIMARY KEY,
StartDate DATETIME NOT NULL,
EndDate DATETIME NOT NULL
);
SET DATEFORMAT MDY;
INSERT TestData
SELECT 1, '01/01/2011', '01/10/2011'
UNION ALL
SELECT 2, '01/11/2011', '01/20/2011'
UNION ALL
SELECT 3, '01/25/2011', '02/01/2011'
UNION ALL
SELECT 4, '02/10/2011', '02/15/2011'
UNION ALL
SELECT 5, '02/16/2011', '02/27/2011'
UNION ALL
SELECT 6, '02/28/2011', '03/06/2011'
UNION ALL
SELECT 7, '02/28/2011', '03/03/2011'
UNION ALL
SELECT 8, '03/10/2011', '03/18/2011'
UNION ALL
SELECT 9, '03/19/2011', '03/25/2011';
WITH RecursiveCTE
AS
(
SELECT t.Id, t.StartDate, t.EndDate
,1 AS GroupID
FROM TestData t
WHERE t.Id=1
UNION ALL
SELECT crt.Id, crt.StartDate, crt.EndDate
,CASE WHEN DATEDIFF(DAY,prev.EndDate,crt.StartDate)=1 THEN prev.GroupID ELSE prev.GroupID+1 END
FROM TestData crt
JOIN RecursiveCTE prev ON crt.Id-1=prev.Id
--WHERE crt.Id > 1
)
SELECT cte.GroupID, MIN(cte.StartDate) AS StartDate, MAX(cte.EndDate) AS EndDate
FROM RecursiveCTE cte
GROUP BY cte.GroupID
ORDER BY cte.GroupID;
DROP TABLE TestData;
Following is the sample data. I need to make 3 copies of this data in t sql without using loop and return as one resultset. This is sample data not real.
42 South Yorkshire
43 Lancashire
44 Norfolk
Edit: I need multiple copies and I have no idea in advance that how many copies I need I have to decide this on the basis of dates. Date might be 1st jan to 3rd Jan OR 1st jan to 8th Jan.
Thanks.
Don't know about better but this is definatley more creative! you can use a CROSS JOIN.
EDIT: put some code in to generate a date range, you can change the date range, the rows in the #date are your multiplier.
declare #startdate datetime
, #enddate datetime
create table #data1 ([id] int , [name] nvarchar(100))
create table #dates ([date] datetime)
INSERT #data1 SELECT 42, 'South Yorkshire'
INSERT #data1 SELECT 43, 'Lancashire'
INSERT #data1 SELECT 44, 'Norfolk'
set #startdate = '1Jan2010'
set #enddate = '3Jan2010'
WHILE (#startdate <= #enddate)
BEGIN
INSERT #dates SELECT #startdate
set #startdate=#startdate+1
END
SELECT [id] , [name] from #data1 cross join #dates
drop table #data1
drop table #dates
You could always use a CTE to do the dirty work
Replace the WHERE Counter < 4 with the amount of duplicates you need.
CREATE TABLE City (ID INTEGER PRIMARY KEY, Name VARCHAR(32))
INSERT INTO City VALUES (42, 'South Yorkshire')
INSERT INTO City VALUES (43, 'Lancashire')
INSERT INTO City VALUES (44, 'Norfolk')
/*
The CTE duplicates every row from CTE for the amount
specified by Counter
*/
;WITH CityCTE (ID, Name, Counter) AS
(
SELECT c.ID, c.Name, 0 AS Counter
FROM City c
UNION ALL
SELECT c.ID, c.Name, Counter + 1
FROM City c
INNER JOIN CityCTE cte ON cte.ID = c.ID
WHERE Counter < 4
)
SELECT ID, Name
FROM CityCTE
ORDER BY 1, 2
DROP TABLE City
This may not be the most efficient way of doing it, but it should work.
(select ....)
union all
(select ....)
union all
(select ....)
Assume the table is named CountyPopulation:
SELECT * FROM CountyPopulation
UNION ALL
SELECT * FROM CountyPopulation
UNION ALL
SELECT * FROM CountyPopulation
Share and enjoy.
There is no need to use a cursor. The set-based approach would be to use a Calendar table. So first we make our calendar table which need only be done once and be somewhat permanent:
Create Table dbo.Calendar ( Date datetime not null Primary Key Clustered )
GO
; With Numbers As
(
Select ROW_NUMBER() OVER( ORDER BY S1.object_id ) As [Counter]
From sys.columns As s1
Cross Join sys.columns As s2
)
Insert dbo.Calendar([Date])
Select DateAdd(d, [Counter], '19000101')
From Numbers
Where [Counter] <= 100000
GO
I populated it with a 100K dates which goes into 2300. Obviously you can always expand it. Next we generate our test data:
Create Table dbo.Data(Id int not null, [Name] nvarchar(20) not null)
GO
Insert dbo.Data(Id, [Name]) Values(42,'South Yorkshire')
Insert dbo.Data(Id, [Name]) Values(43, 'Lancashire')
Insert dbo.Data(Id, [Name]) Values(44, 'Norfolk')
GO
Now the problem becomes trivial:
Declare #Start datetime
Declare #End datetime
Set #Start = '2010-01-01'
Set #End = '2010-01-03'
Select Dates.[Date], Id, [Name]
From dbo.Data
Cross Join (
Select [Date]
From dbo.Calendar
Where [Date] >= #Start
And [Date] <= #End
) As Dates
By far the best solution is CROSS JOIN. Most natural.
See my answer here: How to retrieve rows multiple times in SQL Server?
If you have a Numbers table lying around, it's even easier. You can DATEDIFF the dates to give you the filter on the Numbers table