How to get #StartDate - 30 in SQL Server 2014? - sql-server

I have a query that I use to generate statements that shows Amount Due for the month which is calculated based on date parameters- #StartDate and #EndDate
Included in the statement, I would like to add the Amount Due from the previous month (Previous Month's Balance owing) for a date range #StartDate - 30 to #EndDate - 30. What would be the code to run that?
My code:
set nocount on
Declare #S AS DateTime = ISNULL(#StartDate,DateAdd(d,-60,GETDATE()))
Declare #anum as nvarchar(8) = ISNULL(#panum,'25991275')
Declare #E AS DateTime = ISNULL(#EndDate,DateAdd(d,-0,GETDATE()))
SELECT A.AccountNumber
,C.FirstName + ' ' + C.LastName CustName
,[InvoiceNumber]
,[StatementDate]
,[NewCharges]
,[AmountDue]
,[Charges]
,[AccountFee]
,[Interest]
,[Payments]
,[Refunds]
,[DueDate]
FROM [StatementSummary] S
INNER JOIN Account A ON S.AccountID = A.Accountid
INNER JOIN Contact C ON A.AccountId = C.AccountId
WHERE A.AccountNumber = #anum
AND StatementDate >= #S
AND StatementDate <= #E
ORDER BY StatementDate DESC
I was thinking of making another Dataset to run the following code:
SELECT Top 1 AcctBalance
FROM [FinMaster]
WHERE AcctID = #anum
AND BusinessDay >= #S - 30
AND BusinessDay <= #E - 30
ORDER BY AcctBalance DESC
How do I add the date range to back to the previous month's?
If I could add this second code as a line in the first code then I won't need to create a second dataset for my report.

Using OUTER APPLY and EOMonth function to get the Last Month value
Just a logic and not using your fields
declare #reportdate date = getdate()
select a.*, x.field....
from table1 A
OUTER apply ( --- to get last month data, can be null.. similar to left outer join but in a cartesian way of display
select b.field1, b.field2, b....
from table1 B
where
b.product_id = a.product_id and
trans_date
between -- between last month based on the #reportdate
dateadd(day,1,eomonth(dateadd(month,-2,#reportdate))) -- or a.trans_date
and
eomonth(dateadd(month,-1,#reportdate))
) x
where trans_date
between -- your reporting date, can be any date
dateadd(day,1,eomonth(dateadd(month,-1,#reportdate)))
and eomonth(#reportdate)

Related

Build timeline from start and end dates

I have a subscription table with a user ID, a subscription start date and a subscription end date. I also have a calendar table with a datestamp field, that is every single date starting from the first subscription date in my subscription table.
I am trying to write something that would give me a table with a date column and three numbers: number of total active (on that day), number of new subscribers, number of unsubscribers.
(N.B. I tried to insert sample tables using the suggested GitHub Flavoured Markdown but it just all goes into one row.)
Currently I am playing with a query that creates multiple joins between the two tables, one for each number:
select a.datestamp
,count(distinct case when b_sub.UserID is not null then b_sub.UserID end) as total_w_subscription
,count(distinct case when b_in.UserID is not null then b_in.UserID end) as total_subscribed
,count(distinct case when b_out.UserID is not null then b_out.UserID end) as total_unsubscribed
from Calendar as a
left join Subscription as b_sub -- all those with subscription on given date
on b_sub.sub_dt <= a.datestamp
and (b_sub.unsub_dt > a.datestamp or b_sub.unsub_dt is null)
left join Subscription as b_in -- all those that subscribed on given date
on b_in.sub_dt = a.datestamp
left join Subscription as b_out -- all those that unsubscribed on given date
on b_out.unsub_dt = a.datestamp
where a.datestamp > '2021-06-10'
group by a.datestamp
order by datestamp asc
;
I have indexed the date fields in both tables. If I only look at one day, it runs in 3 seconds. Two days already takes forever. The Sub table is over 2.6M records and ideally I'll need my timeline to begin sometime in 2012.
What would be the most time efficient way to do this?
You're on the right track. I created some table variables and assumed a data structure that has each subscription include a start and end date.
--Create #dates table variable for calendar
DECLARE #startDate DATETIME = '2018-01-01'
DECLARE #endDate DATETIME = '2021-06-18'
DECLARE #dates TABLE
(
reportingdate DATETIME
)
WHILE #startDate <= #endDate
BEGIN
INSERT INTO #dates SELECT #startDate
SET #startDate += 1
END
--Create #subscriptions table variable for subcriptions to join onto calendar
DECLARE #subscriptions TABLE
(
id INT
,startDate DATETIME
,endDate DATETIME
)
INSERT INTO #subscriptions
VALUES
(1,'2018-01-01 00:00:00.000','2019-10-07 00:00:00.000')
,(2,'2018-01-11 00:00:00.000','2019-12-21 00:00:00.000')
,(3,'2019-04-21 00:00:00.000','2020-03-19 00:00:00.000')
,(4,'2019-12-09 00:00:00.000','2020-05-14 00:00:00.000')
,(5,'2020-04-26 00:00:00.000','2020-07-06 00:00:00.000')
,(6,'2020-05-02 00:00:00.000',NULL)
,(7,'2020-08-31 00:00:00.000','2020-10-29 00:00:00.000')
,(8,'2020-12-13 00:00:00.000','2021-01-13 00:00:00.000')
,(9,'2021-02-12 00:00:00.000','2021-04-19 00:00:00.000')
,(10,'2021-06-10 00:00:00.000',NULL)
;
Then I join the subscription onto the calendar table.
--CTE to join subscription onto calendar and use ROW_NUMBER functions
WITH cte AS (
SELECT
s.id AS SubID
,d.ReportingDate
,ROW_NUMBER() OVER (PARTITION BY s.id ORDER BY d.ReportingDate) AS asc_rn --used to identify 1st
,ROW_NUMBER() OVER (PARTITION BY s.id ORDER BY d.ReportingDate DESC) AS desc_rn --used to identify last
,CASE WHEN s.endDate IS NULL THEN 1 ELSE 0 END AS ActiveSub
FROM #subscriptions s
LEFT JOIN #dates d ON
d.reportingdate BETWEEN s.startDate AND ISNULL(s.endDate,'9999-12-31')
)
I used ROW_NUMBER to identify the first and last date rows of the subscription, as well as checking if the subscription endDate is NULL (still active). I then query the CTE to count subscriptions grouped by day, as well as summing new and terminated subscriptions grouped by day.
--Query CTE using asc_rn, desc_rn, and ActiveSub to identify new subscribers and unsubscribers.
SELECT
ReportingDate
,COUNT(*) AS TotalSubscribers
,SUM(CASE WHEN asc_rn = 1 THEN 1 ELSE 0 END) AS NewSubscribers
,SUM(CASE WHEN desc_rn = 1 AND ActiveSub = 0 THEN 1 ELSE 0 END) AS UnSubscribers
FROM cte
GROUP BY ReportingDate
ORDER BY ReportingDate

Determine Days Not Covered Based on Fields Containing "From" and "To" Dates

Server: Microsoft SQL Server
SQLFiddle: http://www.sqlfiddle.com/#!18/cdfa3/1/0
If I have rows containing a "start" date and an "end date", how can write a SQL query that will list the days that are not contained between those dates.
Example (see SQLFiddle link above for a playable demo):
startdate enddate
2019-06-06 00:00:00.000 2019-06-08 00:00:00.000
2019-06-10 00:00:00.000 2019-06-11 00:00:00.000
2019-06-12 00:00:00.000 2019-06-13 00:00:00.000
We have a coverage gap on June 9th, because we have coverage from June 6th-June 8th, then on June 10th-June 13th.
How is it possible to identify the date of June 9th as having no coverage based on rows that have date ranges?
You could use generated calendar table and LEFT JOIN:
DECLARE #min DATE, #max DATE;
SELECT #min = MIN(workingdatestart), #max = MAX(workingdateend) FROM workingdates;
WITH cte AS (
SELECT DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY 1/0), #min) AS d
FROM sys.objects s, sys.objects s2
)
SELECT c.d AS gap
FROM cte c
LEFT JOIN workingdates w ON c.d BETWEEN w.workingdatestart and w.workingdateend
WHERE c.d < #max AND w.workingDateId IS NULL;
db<>fiddle demo
#Lukasz Szozda stole my thunder. My answer is similar but does not use variables (I'm not suggesting that's good or bad.. just calling it out).
You can create a calendar table function (see example below) then perform a LEFT ANTI SEMI JOIN against your working days table. The benefit to this solution is the calendar table generates 0 IO.
Solution:
WITH r(L,H) AS
(
SELECT CAST(MIN(w.workingdatestart) AS DATE), CAST(MAX(w.workingdateend) AS DATE)
FROM dbo.workingdates AS w
),
cal AS
(
SELECT c.Dt
FROM r
CROSS APPLY dbo.calendar(r.L,r.H) AS c
)
SELECT c.Dt
FROM cal AS c
EXCEPT
SELECT c.Dt
FROM cal AS c
JOIN dbo.workingdates AS w
ON c.Dt BETWEEN w.workingdatestart AND w.workingdateend;
.. and the function:
CREATE FUNCTION dbo.calendar(#startdate DATE, #enddate DATE)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH E1(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS x(x)),
iTally(N) AS
(
SELECT 0 UNION ALL
SELECT TOP (DATEDIFF(DAY,#startDate,#endDate)) ROW_NUMBER() OVER (ORDER BY (SELECT 1))
FROM E1 a, E1 b, E1 c
)
SELECT sortKey = i.N, Dt = DATEADD(DAY, i.N, #startDate)
FROM iTally AS i;
To achieve this you need a table that contains all dates between your minimal and maximal dates. Then just filter rows that don't exist in workingdates using LEFT JOIN.
declare #minDate date = (select min([workingdatestart]) from [workingdates])
declare #maxDate date = (select max([workingdateend]) from [workingdates])
declare #Date date = #minDate
create table #rangeOfDates (dat date)
while #Date <= #maxDate
begin
insert into #rangeOfDates values (#Date)
set #Date = dateadd(day , 1, #Date)
end
select r.dat
from #rangeOfDates as r
left join workingdates as w
on r.dat between w.workingdatestart and w.workingdateend
where w.workingdateID is null
Result:
dat
2019-06-09

How to improve speed of the query using SQL Server?

I have created a stored procedure to get data. In this stored procedure, I have returned 1 table and table stores the data above 1 lakh + data. So right now I have run the stored procedure that time I get the data in above 1 minute time take. I want just with in 1 second get data. I have set also SET NOCOUNT ON; and also create missing index. Still I am getting same time for the get data.
This is my query:
DECLARE #CurMon int
DECLARE #year nvarchar(max)
SELECT #CurMon = month(getdate())
SELECT #year = year(getdate())
SELECT
FORMAT(dateadd(MM, T.i, getdate()),'MMM-yy') AS DateColumn,
ISNULL(uf.TotalCount, 0) as TotalCount
FROM
(VALUES (12-#CurMon),(11-#CurMon),(10-#CurMon),(9-#CurMon),(8-#CurMon),(7-#CurMon),(6-#CurMon), (5-#CurMon), (4-#CurMon), (3-#CurMon), (2-#CurMon), (1-#CurMon)) AS T(i)
OUTER APPLY
(SELECT DISTINCT
COUNT(datepart(MM,UF.InsertDateTime)) OVER (partition by datepart(MM,UF.InsertDateTime)) AS TotalCount
FROM dbo.UserFollowers UF
INNER JOIN dbo.Users U on U.UserId = UF.FollowerId
WHERE DATEDIFF(mm,UF.InsertDateTime, DATEADD(mm, T.i, GETDATE())) = 0 and UF.IsFollowed = 1
) uf
order by DATEPART(MM,convert(datetime,FORMAT(dateadd(MM, T.i, getdate()),'MMMM') +'01 '+#year,110))
i am also try some other query for the improve speed of query but still i am getting same time. here this query also print.
declare #StartDate datetime = dateadd(year , datediff(year , 0, getdate() ) , 0)
declare #tempT2 table
(
MNo int,
[Month] datetime,
NextMonth datetime)
;with Months as (
select top (12)
MNo = row_number() over (order by number)
,[Month] = dateadd(month, row_number() over (order by number) -1, #StartDate)
, NextMonth = dateadd(month, row_number() over (order by number), #StartDate)
from master.dbo.spt_values
)
insert into #tempT2
select * from Months
select
m.MNo
, Month = format(m.Month, 'MMM-yy')
, tally = count(UF.InsertDateTime)
from #tempT2 m
left join dbo.UserFollowers UF
INNER JOIN dbo.Users U on U.UserId = UF.FollowerId
on UF.InsertDateTime >= m.Month
and UF.InsertDateTime < m.NextMonth where UF.IsFollowed = 1
group by m.MNo,format(m.Month, 'MMM-yy')
order by MNo
here this is my both query i have try but still i am not getting success for the improve the speed of the query. and sorry but i can not see here my execution plan of the query actually i have not permission for that.
You can gain a little bit of performance by switching to a temporary table instead of a table variable, and by getting rid of format():
declare #StartDate datetime = dateadd(year , datediff(year , 0, getdate() ) , 0)
create table #Months (
MNo int not null primary key
, Month char(6) not null
, MonthStart datetime not null
, NextMonth datetime not null
)
;with Months as (
select top (12)
MNo = row_number() over (order by number)
, MonthStart = dateadd(month, row_number() over (order by number) -1, #StartDate)
, NextMonth = dateadd(month, row_number() over (order by number), #StartDate)
from master.dbo.spt_values
)
insert into #Months (MNo, Month, MonthStart, NextMonth)
select
MNo
, Month = stuff(convert(varchar(9),MonthStart,6),1,3,'')
, MonthStart
, NextMonth
from Months;
select
m.MNo
, m.Month
, tally = count(UF.InsertDateTime)
from #tempT2 m
inner join dbo.Users U
on UF.InsertDateTime >= m.MonthStart
and UF.InsertDateTime < m.NextMonth
inner join dbo.UserFollowers UF
on U.UserId = UF.FollowerId
and UF.IsFollowed = 1
group by
m.MNo
, m.Month
order by MNo
After that, you should evaluate the execution plan to determine if you need a better indexing strategy.
If you still need it to go faster, you could create an actual calendar table and look into creating an indexed view. An indexed view can be a chore get it to behave correctly depending on your sql server version, but will be faster.
Reference:
format performance - Aaron Bertrand
What is the difference between a temp table and table variable in SQL Server? - Answer by Martin Smith
When should I use a table variable vs temporary table in sql server? - Answer by Martin Smith
Indexed Views and Statistics - Paul White
Generate a set or sequence without loops - 2 - Aaron Bertrand
The "Numbers" or "Tally" Table: What it is and how it replaces a loop - Jeff Moden
Creating a Date Table/Dimension in sql Server 2008 - David Stein
Calendar Tables - Why You Need One - David Stein
Creating a date dimension or calendar table in sql Server - Aaron Bertrand

Select count with 0 count

Lets say I have following query:
SELECT top (5) CAST(Created AS DATE) as DateField,
Count(id) as Counted
FROM Table
GROUP BY CAST(Created AS DATE)
order by DateField desc
Lets say it will return following data set
DateField Counted
2016-01-18 34
2016-01-17 99
2016-01-14 1
2015-12-28 1
2015-12-27 6
But when I have Counted = 0 for certain Date I would like to get that in result set. So for example it should look like following
DateField Counted
2016-01-18 34
2016-01-17 99
2016-01-16 0
2016-01-15 0
2016-01-14 1
Thank you!
Expanding upon KM's answer, you need a date table which is like a numbers table.
There are many examples on the web but here's a simple one.
CREATE TABLE DateList (
DateValue DATE,
CONSTRAINT PK_DateList PRIMARY KEY CLUSTERED (DateValue)
)
GO
-- Insert dates from 01/01/2015 and 12/31/2015
DECLARE #StartDate DATE = '01/01/2015'
DECLARE #EndDatePlus1 DATE = '01/01/2016'
DECLARE #CurrentDate DATE = #StartDate
WHILE #EndDatePlus1 > #CurrentDate
BEGIN
INSERT INTO DateList VALUES (#CurrentDate)
SET #CurrentDate = DATEADD(dd,1,#CurrentDate)
END
Now you have a table
then you can rewrite your query as follows:
SELECT top (5) DateValue, isnull(Count(id),0) as Counted
FROM DateList
LEFT OUTER JOIN Table
on DateValue = CAST(Created AS DATE)
GROUP BY DateValue
order by DateValue desc
Two notes:
You'll need a where clause to specify your range.
A join on a cast isn't ideal. The type in your date table should match the type in your regular table.
One more solution as a single query:
;WITH dates AS
(
SELECT CAST(DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY [object_id]) - 1, '2016-01-14') as date) 'date'
FROM sys.all_objects
)
SELECT TOP 5
[date] AS 'DateField',
SUM(CASE WHEN Created IS NULL THEN 0 ELSE 1 END) AS 'Counted'
FROM dates
LEFT JOIN Table ON [date]=CAST(Created as date)
GROUP BY [date]
ORDER BY [date]
For a more edgy solution, you could use a recursive common table expression to create the date list. PLEASE NOTE: do not use recursive common table expressions in your day job! They are dangerous because it is easy to create one that never terminates.
DECLARE #StartDate date = '1/1/2016';
DECLARE #EndDate date = '1/15/2016';
WITH DateList(DateValue)
AS
(
SELECT DATEADD(DAY, 1, #StartDate)
UNION ALL
SELECT DATEADD(DAY, 1, DateValue)
FROM DateList
WHERE DateList.DateValue < #EndDate
)
SELECT DateValue, isnull(Count(id),0) as Counted
FROM DateList
LEFT OUTER JOIN [Table]
ON DateValue = CAST(Created AS DATE)
GROUP BY DateValue
ORDER BY DateValue DESC

Return 0 for NULL values in a simple count and group by date

I'm doing a simple count query for an SSRS chart, and while it looks like a common question, I've not really found an answer that suits my situation. This is my query:
SELECT TOP 30 CAST(qa.Created As Date) As 'Date',
COUNT(qa.Created) As 'Count'
FROM QAs qa
GROUP BY CAST(qa.Created As Date)
ORDER BY 'Date' DESC
This returns something like:
Date | Count
2014-11-10 | 2
2014-11-08 | 3
2014-11-07 | 8
Which when put into a line chart, doesn't show the dip down to 0 on the 9th and is a bit confusing for my users. What I want to do is have all of the last 30 days appear in order, even if they are at 0. I've been told to do this with COALESCE() but I can't seem to get that working either. Where am I going wrong?
Use a Recursive CTE to generate dates for last 30 days.
;WITH cte
AS (SELECT Cast(dateadd(day,-30,Getdate()) AS DATE) AS dates
UNION ALL
SELECT Dateadd(day, 1, dates)
FROM cte
WHERE dates < cast(Getdate() as date)
SELECT a.Dates AS [Date],
Count(qa.Created) AS [Count]
FROM cte a
LEFT JOIN QAs qa
ON a.dates = Cast(qa.Created AS DATE)
GROUP BY a.Dates
ORDER BY a.Dates
I'd probably go with NoDisplayName's recursive CTE but for an alternative, if you happen to be stuck in something prior to 2005 which I was for a long time.
--Build a table of dates
DECLARE #dates AS TABLE([date] date)
DECLARE #i int
SET #i = 30
WHILE #i > 0
BEGIN
INSERT INTO #dates([date])
SELECT DATEADD(d, -1 * #i, GETDATE())
SET #i = (#i - 1)
END
--Join into those dates so that no date is excluded
SELECT [date], SUM(dateCount)
FROM (
SELECT d.[date], CASE WHEN qa.Created IS NULL THEN 0 ELSE 1 END AS dateCount
FROM #dates d
LEFT JOIN QAs qa ON CAST(qa.Created AS date) = d.[date]
) AS dateCounts
GROUP BY [date]

Resources