How to use the minimum date from three available - SQL - sql

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

Related

Is there a way to optimize or replace a while loop in sql?

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)

How to Auto generate dates between date range using SQL Query?

I just want to generate the date between data range using SQL Query.
Source:
Result:
Thanks,
Lawrance A
Here is how to accomplish this by using a tally table to create a calendar table:
declare #source table
(
user_id int not null primary key clustered,
from_date date not null,
to_date date not null
);
insert into #source
values
(1, '02/20/2019', '02/23/2019'),
(2, '02/22/2019', '02/28/2019'),
(3, '03/01/2019', '03/05/2019');
with
rows as
(
select top 1000
n = 1
from sys.messages
),
tally as
(
select n = row_number() over(order by (select null)) - 1
from rows
),
calendar as
(
select
date = dateadd(dd, n, (select min(from_date) from #source))
from tally
)
select
s.user_id,
c.date
from #source s
cross join calendar c
where c.date between s.from_date and s.to_date;
Result set:

Query to merge continuous temporal records

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;

Need multiple copies of one resultset in sql without using loop

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

How to delete when the parameter varies by group without looping? (T-SQL)

Imagine I have these columns in a table:
id int NOT NULL IDENTITY PRIMARY KEY,
instant datetime NOT NULL,
foreignId bigint NOT NULL
For each group (grouped by foreignId) I want to delete all the rows which are 1 hour older than the max(instant). Thus, for each group the parameter is different.
Is it possible without looping?
Yep, it's pretty straightforward. Try this:
DELETE mt
FROM MyTable AS mt
WHERE mt.instant <= DATEADD(hh, -1, (SELECT MAX(instant)
FROM MyTable
WHERE ForeignID = mt.ForeignID))
Or this:
;WITH MostRecentKeys
AS
(SELECT ForeignID, MAX(instant) AS LatestInstant
FROM MyTable)
DELETE mt
FROM MyTable AS mt
JOIN MostRecentKeys mrk ON mt.ForeignID = mrt.ForeignID
AND mt.Instant <= DATEADD(hh, -1, mrk.LatestInstant)
DELETE
FROM mytable
FROM mytable mto
WHERE instant <
(
SELECT DATEADD(hour, -1, MAX(instant))
FROM mytable mti
WHERE mti.foreignid = mto.foreignid
)
Note double FROM clause, it's on purpose, otherwise you won't be able to alias the table you're deleting from.
The sample data to check:
DECLARE #mytable TABLE
(
id INT NOT NULL PRIMARY KEY,
instant DATETIME NOT NULL,
foreignID INT NOT NULL
)
INSERT
INTO #mytable
SELECT 1, '2009-22-07 10:00:00', 1
UNION ALL
SELECT 2, '2009-22-07 09:30:00', 1
UNION ALL
SELECT 3, '2009-22-07 08:00:00', 1
UNION ALL
SELECT 4, '2009-22-07 10:00:00', 2
UNION ALL
SELECT 5, '2009-22-07 08:00:00', 2
UNION ALL
SELECT 6, '2009-22-07 07:30:00', 2
DELETE
FROM #mytable
FROM #mytable mto
WHERE instant <
(
SELECT DATEADD(hour, -1, MAX(instant))
FROM #mytable mti
WHERE mti.foreignid = mto.foreignid
)
SELECT *
FROM #mytable
1 2009-07-22 10:00:00.000 1
2 2009-07-22 09:30:00.000 1
4 2009-07-22 10:00:00.000 2
I'm going to assume when you say '1 hour older than the max(instant)' you mean '1 hour older than the max(instant) for that foreignId'.
Given that, there's almost certainly a more succinct way than this, but it will work:
DELETE
TableName
WHERE
DATEADD(hh, 1, instant) < (SELECT MAX(instant)
FROM TableName T2
WHERE T2.foreignId = TableName.foreignId)
The inner subquery is called a 'correlated subquery', if you want to look for more info. The way it works is that for each row under consideration by the outer query, it is the foreignId of that row that gets referenced by the subquery.