Is there a more effecient way to run this sql script - sql-server

I have daily data in the 'portfoliodata' table. Not all days have data so some month ends don't have data.
I want to extract data from the latest day in each month that has data
declare #Portfolio as float
,#Date as date
,#Date12 as date
,#Date11 as date
,#Date10 as date
,#Date9 as date
,#Date8 as date
,#Date7 as date
,#Date6 as date
,#Date5 as date
,#Date4 as date
,#Date3 as date
,#Date2 as date
,#Date1 as date
set #Portfolio = 97900
select #Date = max(date) from portfoliodata
set #Date12 = EOMONTH(DATEADD(MONTH,-1,#Date))
set #Date11 = EOMONTH(DATEADD(MONTH,-2,#Date))
set #Date10 = EOMONTH(DATEADD(MONTH,-3,#Date))
set #Date9 = EOMONTH(DATEADD(MONTH,-4,#Date))
set #Date8 = EOMONTH(DATEADD(MONTH,-5,#Date))
set #Date7 = EOMONTH(DATEADD(MONTH,-6,#Date))
set #Date6 = EOMONTH(DATEADD(MONTH,-7,#Date))
set #Date5 = EOMONTH(DATEADD(MONTH,-8,#Date))
set #Date4 = EOMONTH(DATEADD(MONTH,-9,#Date))
set #Date3 = EOMONTH(DATEADD(MONTH,-10,#Date))
set #Date2 = EOMONTH(DATEADD(MONTH,-11,#Date))
set #Date1 = EOMONTH(DATEADD(MONTH,-12,#Date))
select * from portfoliodata
where Portfolio = #Portfolio and (
date = (select max(date) from portfoliodata where date < #Date12 and date > #Date11) or
date = (select max(date) from portfoliodata where date < #Date11 and date > #Date10) or
date = (select max(date) from portfoliodata where date < #Date10 and date > #Date9) or
date = (select max(date) from portfoliodata where date < #Date9 and date > #Date8) or
date = (select max(date) from portfoliodata where date < #Date8 and date > #Date7) or
date = (select max(date) from portfoliodata where date < #Date7 and date > #Date6) or
date = (select max(date) from portfoliodata where date < #Date6 and date > #Date5) or
date = (select max(date) from portfoliodata where date < #Date5 and date > #Date4) or
date = (select max(date) from portfoliodata where date < #Date4 and date > #Date3) or
date = (select max(date) from portfoliodata where date < #Date3 and date > #Date2) or
date = (select max(date) from portfoliodata where date < #Date2 and date > #Date1) or
date = (select max(date) from portfoliodata where date < #Date1))
The above code takes a long time to run

If you just need the max day of each month, just put it into a CTE with ROW_NUMBER(), and partition by the month of your date field. We can eliminate the need for querying against date at all:
with recent as
(
select *
, ROW_NUMBER() over (partition by month(t.date) order by t.date desc) rn
from portfoliodata t
)
select *
from recent
where rn = 1
and portfolio = 97900
The where rn = 1 clause will give us the most recent date for each month. However, if you have multiple rows with the same date, this will arbitrarily order those.
If you want this to work over multiple years, include a partition on the year as well:
with recent as
(
select *
, ROW_NUMBER() over (partition by month(t.date), year(t.date) order by t.date desc) rn
from portfoliodata t
)
select *
from recent
where rn = 1
and portfolio = 97900

Just another way is to use a subquery in the where clause to get all the target dates.
Select *
From portfoliodata
Where Portfolio = 97900
And [date] In (Select Max([date]) From portfoliodata
Where Portfolio = 97900
Group by Year([date]), Month([date]))
you can limit the number of months as follows:
Declare #n Int = 12
Select *
From portfoliodata
Where Portfolio = 97900
And [date] In (Select Top (#n) Max([date]) From portfoliodata
Where Portfolio = 97900
Group by Year([date]), Month([date])
Order by 1 Desc)

Related

How to combine a date series with my regular selection?

I am using the following MSSQL query to count timestamps within a table. Each row stands for one transaction (one car washed) at my carwash company.
SELECT
count(st.date) as NumberOfWashes,
cast(st.date as date) as DayOfWashes
FROM
POS.dbo.sales_transaction_line_item as stli
join POS.dbo.sales_transaction as st on st.sales_transaction_id = stli.fk_sales_transaction
join POS.dbo.sales_item as si on si.sales_item_id = stli.fk_sales_item
WHERE
st.fk_sales_status <> 3
and si.fk_sales_item_type = 1
and st.date BETWEEN #start_date and #end_date
Group by
cast(st.date as date)
order by
cast(st.date as date) desc
I am using the two joins to eliminate cancelled washes (sales_status) and transactions which sell products but no car wash (sales_item_type).
The result of this list looks like:
NumberofWashes DayOfWashes
42 2023-01-26
71 2023-01-25
57 2023-01-24
87 2023-01-23
104 2023-01-21
114 2023-01-20
As you can see the Date 2023-01-22 is missing (it's a sunday and we are closed).
However, I need that day as well with 0 washes.
Therefore I have the code like this:
DECLARE #start_date DATE = '2021-01-26';
DECLARE #end_date DATE = '2023-01-27';
WITH AllDays
AS ( SELECT #start_date AS [Date]
UNION ALL
SELECT DATEADD(DAY, 1, [Date])
FROM AllDays
WHERE [Date] < #end_date )
SELECT [Date]
FROM AllDays
OPTION (MAXRECURSION 0)
It produces a simple list with all dates:
2023-01-20
2023-01-21
2023-01-22
2023-01-23
2023-01-24
2023-01-25
2023-01-26
How can I combine those two statements so the DayOfWashes includes all available dates?
Just combine the 2 queries in a nice way, something like this:
DECLARE #start_date DATE = '2021-01-26';
DECLARE #end_date DATE = '2023-01-27';
WITH AllDays
AS ( SELECT #start_date AS [Date]
UNION ALL
SELECT DATEADD(DAY, 1, [Date])
FROM AllDays
WHERE [Date] < #end_date ),
WashData as (
SELECT
count(st.date) as NumberOfWashes,
cast(st.date as date) as DayOfWashes
FROM
POS.dbo.sales_transaction_line_item as stli
join POS.dbo.sales_transaction as st on st.sales_transaction_id = stli.fk_sales_transaction
join POS.dbo.sales_item as si on si.sales_item_id = stli.fk_sales_item
WHERE
st.fk_sales_status <> 3
and si.fk_sales_item_type = 1
and st.date BETWEEN #start_date and #end_date
Group by
cast(st.date as date)
)
SELECT
[Date]
,NumberOfWashes
FROM AllDays ad
left join WashData wd
on ad.[Date] = wd.DayOfWashes
OPTION (MAXRECURSION 0)

How to add dates within a range

I need to insert a range of dates into a table, along with corresponding column data. My table's earliest date stops at 2017-3-16, but I need to add dates going back to 2016-1-1. See screenshot below for reference:
I'm sure I can figure out how to attribute day of the week, type of day, etc. using functions such as datepart, datename, etc. What I'm not certain of is how to insert a range of dates, between 2016-1-1 and 2017-3-15.
I keep this snippet handy as it is often needed. Simply set the high and low date variables and join your date on the CalendarDate field. If date gaps are ok then INNER JOIN, otherwise LEFT JOIN.
DECLARE #StartDate DATETIME = '01/01/2015'
DECLARE #EndDate DATETIME = '12/01/2016'
;WITH OrderedDays as
(
SELECT CalendarDate = #StartDate
UNION ALL
SELECT CalendarDate = DATEADD(DAY, 1, CalendarDate)
FROM OrderedDays WHERE DATEADD (DAY, 1, CalendarDate) <= #EndDate
),
Calendar AS
(
SELECT
DayIndex = ROW_NUMBER() OVER(PARTITION BY 1 ORDER BY CalendarDate),
CalendarDate,
CalenderDayOfMonth = DATEPART(DAY, CalendarDate),
CalenderMonthOfYear = DATEPART(MONTH, CalendarDate),
CalendarYear = DATEPART(YEAR, CalendarDate),
CalenderWeekOfYear = DATEPART(WEEK, CalendarDate),
CalenderQuarterOfYear = DATEPART(QUARTER, CalendarDate),
CalenderDayOfYear = DATEPART(DAYOFYEAR, CalendarDate),
CalenderDayOfWeek = DATEPART(WEEKDAY, CalendarDate),
CalenderWeekday = DATENAME(WEEKDAY, CalendarDate)
FROM
OrderedDays
)
SELECT * FROM Calendar
OPTION (MAXRECURSION 0)

SQL Server counting hours between dates excluding Fri 6pm - Mon 6am

I am looking for a SQL server function which can count the number of hours between 2 given datetime values, but excludes the hours between 6pm on Friday and 6am on Monday.
I'd like to be able to use this as a custom datediff, but also as a custom dateadd (eg adding 4 hours to 5pm a Friday, will return following Monday 9am)
I currently have something which excludes Sat/Sun based on the weekday number but this doesn't take the Fri/Mon hours into account.
This uses a number table. Adjust weekend start/end in parmeters:
declare #d1 as datetime = '2018-06-01 05:30:00'
, #d2 as datetime = '2018-06-18 19:45:00'
, #FridayWE as int = 18 --6pm
, #MondayWS as int = 6 --6am
;WITH x AS (SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n))
select count(*) as HoursBetweenDatetimes
from (
SELECT dateadd(hour, ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n, dateadd(hour, datediff(hour, 0, #d1), 0)) as [DateHour]
FROM x ones, x tens, x hundreds, x thousands
) a
where not ((DATEPART(dw,[DateHour]) = 6 and DATEPART(hour,[DateHour]) >= #FridayWE)
or (DATEPART(dw,[DateHour]) = 7 )
or (DATEPART(dw,[DateHour]) = 1 )
or (DATEPART(dw,[DateHour]) = 2 and DATEPART(hour,[DateHour]) < #MondayWS))
and [DateHour] < #d2
This is another option you can use, with a calendar table.
This is the generation of the calendar table. For this case it has just days from monday to friday and from 9am to 6pm, one day per row (this can be additional columns for your regular calendar table).
IF OBJECT_ID('tempdb..#WorkingCalendar') IS NOT NULL
DROP TABLE #WorkingCalendar
CREATE TABLE #WorkingCalendar (
StartDateTime DATETIME,
EndDateTime DATETIME)
SET DATEFIRST 1 -- 1: Monday, 7: Sunday
DECLARE #StartDate DATE = '2018-01-01'
DECLARE #EndDate DATE = '2025-01-01'
;WITH RecursiveDates AS
(
SELECT
GeneratedDate = #StartDate
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, R.GeneratedDate)
FROM
RecursiveDates AS R
WHERE
R.GeneratedDate < #EndDate
)
INSERT INTO #WorkingCalendar (
StartDateTime,
EndDateTime)
SELECT
StartDateTime = CONVERT(DATETIME, R.GeneratedDate) + CONVERT(DATETIME, '09:00:00'),
EndDateTime = CONVERT(DATETIME, R.GeneratedDate) + CONVERT(DATETIME, '18:00:00')
FROM
RecursiveDates AS R
WHERE
DATEPART(WEEKDAY, R.GeneratedDate) BETWEEN 1 AND 5 -- From Monday to Friday
OPTION
(MAXRECURSION 0)
This is the query to calculate time differences between 2 datetimes. You can change the HOUR for anything you want, in all 3 places (MINUTE, SECOND, etc.) and the result will be displayed in that unit.
DECLARE #FromDate DATETIME = '2018-06-15 18:00:00'
DECLARE #ToDate DATETIME = '2018-06-18 10:00:00'
;WITH TimeDifferences AS
(
-- Date completely covered
SELECT
Difference = DATEDIFF(
HOUR,
W.StartDateTime,
W.EndDateTime)
FROM
#WorkingCalendar AS W
WHERE
W.StartDateTime >= #FromDate AND
W.EndDateTime <= #ToDate
UNION ALL
-- Filter start date partially covered
SELECT
Difference = DATEDIFF(
HOUR,
#FromDate,
CASE WHEN W.EndDateTime > #ToDate THEN #ToDate ELSE W.EndDateTime END)
FROM
#WorkingCalendar AS W
WHERE
#FromDate BETWEEN W.StartDateTime AND W.EndDateTime
UNION ALL
-- Filter end date partially covered
SELECT
Difference = DATEDIFF(
HOUR,
CASE WHEN W.StartDateTime > #FromDate THEN W.StartDateTime ELSE #FromDate END,
#ToDate)
FROM
#WorkingCalendar AS W
WHERE
#ToDate BETWEEN W.StartDateTime AND W.EndDateTime
)
SELECT
Total = SUM(T.Difference)
FROM
TimeDifferences AS T
This approach will consider each day from the calendar table, so if a particular day you have reduced hours (or maybe none from a Holiday) then the result will consider it.
You can use this query to add hours. Basically split each calendar range by hour, then use a row number to determine the amount of hours to add. Is this case you can't simply change the HOUR for MINUTE, it will require a few tweaks here and there if you need it.
DECLARE #FromDate DATETIME = '2018-06-14 12:23:12.661'
DECLARE #HoursToAdd INT = 15
;WITH RecursiveHourSplit AS
(
SELECT
StartDateTime = W.StartDateTime,
EndDateTime = W.EndDateTime,
HourSplitDateTime = W.StartDateTime
FROM
#WorkingCalendar AS W
UNION ALL
SELECT
StartDateTime = W.StartDateTime,
EndDateTime = W.EndDateTime,
HourSplitDateTime = DATEADD(HOUR, 1, W.HourSplitDateTime)
FROM
RecursiveHourSplit AS W
WHERE
DATEADD(HOUR, 1, W.HourSplitDateTime) < W.EndDateTime
),
HourRowNumber AS
(
SELECT
R.HourSplitDateTime,
RowNumber = ROW_NUMBER() OVER (ORDER BY R.HourSplitDateTime ASC)
FROM
RecursiveHourSplit AS R
WHERE
#FromDate < R.HourSplitDateTime
)
SELECT
DATETIMEFROMPARTS(
YEAR(R.HourSplitDateTime),
MONTH(R.HourSplitDateTime),
DAY(R.HourSplitDateTime),
DATEPART(HOUR, R.HourSplitDateTime),
DATEPART(MINUTE, #FromDate),
DATEPART(SECOND, #FromDate),
DATEPART(MILLISECOND, #FromDate))
FROM
HourRowNumber AS R
WHERE
R.RowNumber = #HoursToAdd
You can use similar logic to substract amount of hours by creating the row number with the rows that have datetimes before the supplied datetime (instead of after).

List of Employees who are not in the list one day before and one day after weekend

I need list of those employees who are absent one day before and one day after weekend in a week......like if some is absent in Friday and present on Monday should not be included in the list
Use datepart(weekday, ) to fetch all records relative to monday and friday.
Have a look at SET DATEFIRST function too.
select *
from your_table
where datepart(weekday, Date) = 5
or datepart(weekday, Date) = 1;
This will list all employee id that are absent on a Friday and the following Monday (+1 week). I set-up a calendar week from mininum date to maximum date from the table and get only Friday and Monday. Then get all empid that has no attendance in any of those dates.
with caldte as (
SELECT dateadd(day, rn - 1, t.mindte) as dates,
datepart(weekday, dateadd(day, rn - 1, t.mindte)) as weekday,
datepart(wk, dateadd(day, rn - 1, t.mindte)) as weeknum
FROM (
select row_number() OVER ( order by c.object_id ) AS rn
FROM sys.columns c) rns,
(select min(dte) as mindte, max(dte) as maxdte
from tbl) t
WHERE rn - 1 <= datediff(day, t.mindte, t.maxdte)
and datepart(weekday, dateadd(day, rn - 1, t.mindte)) in (2, 6)
)
select distinct empid
from tbl
where empid not in (
select t.empid
from caldte c, tbl t
where c.dates = t.dte)
order by empid

TSQL - How to optimize the query?

I have the table with News
News
-------
NewsId
NewsText
CREATED_DATE
I need to get news starting from a specified date to a unknown date, but result should contain news for 5 days.
For example:
if I have news related to these dates: 29th, 28th, 27th, 5th, 4th, 3rd
and the starting date specified to 29th, I need to get news where created date between 29 and 4.
I don't know how to get the low date (4th) in that case without brute force:
declare #highDate date = '2011-09-20';
declare #rows int = 0;
declare #lowDate date = #highDate;
declare #i int = 0;
--Querying while rows count != 5
WHILE (#rows != 5)
BEGIN
if (#i = 60)
break;
set #i = #i + 1;
set #lowDate = (select DATEADD(day, -1, #lowDate));
set #rows = (select COUNT(*) from
(SELECT DAY(CAST(CREATED_DATE AS date)) as c1
FROM .[dbo].[NEWS]
and CREATED_DATE > #lowDate
and CREATED_DATE < #highDate
group by DAY(CAST(CREATED_DATE AS date))) as rowsCount);
END
--then return news between to known dates
SELECT *
FROM [dbo].[NEWS]
and CREATED_DATE > #lowDate
and CREATED_DATE < #highDate
order by CREATED_DATE desc
I guess in that algorithm there are too much queries against a DB and I'd like to get rid of 60-days old limitation
declare #highDate date = '2011-09-20'
select * from (
select *,
dense_rank() over (order by cast(created_date as date) desc) rnk
from News
where CREATED_DATE <= #highDate
) as t
where t.rnk <= 5
This might do the trick for you.
declare #HighDate date = '2011-11-29'
declare #LowDate date
select #LowDate = min(N3.created_date)
from (
select top(5) N2.created_date
from (
select distinct cast(N1.created_date as date) as created_date
from news as N1
where cast(N1.created_date as date) <= #HighDate
) as N2
order by 1 desc
) as N3
Or you can use dense_rank
select #LowDate = N.created_date
from (
select created_date,
dense_rank() over(order by cast(created_date as date) desc) as rn
from News
where cast(created_date as date) <= #HighDate
) as N
where N.rn = 5

Resources