When I run this query, it seems to work great when I run it against the date 2015-4-11. However, if I query against the last 3 days it returns the values in year, month, and week as the same. The day is accurate, but it doesn't seem to correctly add everything else.
declare #today date = '2015-1-1'
select
[Day Total] = sum(case when [AccountingDate] < dateadd(DAY, 1, #today) then [Amount] else 0 end),
[Week Total] = sum(case when [AccountingDate] < dateadd(WEEK, 1, #today) then [Amount] else 0 end),
[Month Total] = sum(case when [AccountingDate] < dateadd(MONTH, 1, #today) then [Amount] else 0 end),
[Year Total] = sum([Amount])
from
[Accounting].[dbo].[HandPay]
where
[AccountingDate] >= #today and [AccountingDate] < dateadd(year, 1, #today);
When you use a recent date, you're looking for values where the [AccountingDate] is less than some future date.
If you add a day/week/month to Today, then all data will be prior to those dates unless you have future data in your table:
declare #today date = '2015-05-12'
SELECT dateadd(DAY, 1, #today)
,dateadd(week, 1, #today)
,dateadd(month, 1, #today)
Returns: 2015-05-13, 2015-05-19, 2015-06-12
So [AccountingDate] is always less than those dates if your table only has data through today.
If you actually want to aggregate from Today to 1 day back, 1 week back, and 1 month back, you need to use -1 in your DATEADD(), use >, and modify your WHERE criteria, something like:
declare #today date = '2015-1-1'
select
[Day Total] = sum(case when [AccountingDate] > dateadd(DAY, -1, #today) then [Amount] else 0 end),
[Week Total] = sum(case when [AccountingDate] > dateadd(WEEK, -1, #today) then [Amount] else 0 end),
[Month Total] = sum(case when [AccountingDate] > dateadd(MONTH, -1, #today) then [Amount] else 0 end),
[Year Total] = sum([Amount])
from
[Accounting].[dbo].[HandPay]
where
[AccountingDate] <= #today and [AccountingDate] > dateadd(MONTH, -1, #today);
Note: When comparing a DATETIME to a DATE any time after midnight will be considered greater than the DATE value, so you'll want to make sure you're not excluding values, either by casting the DATETIME as DATE or ensuring you've accounted for the range properly, i.e.: < tomorrow's DATE instead of <= today's DATE to include records from today that have a time after 00:00:00.000
Related
This is part of a query that is calculating the total revenue for a contract based on the time-frame "This Week" along with the start and end dates of the contract (billed hourly).
SELECT (ChargeRate - PayRate) * 8 *
CASE
WHEN ContractStartDate <= DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
AND ContractEndDate >= CURRENT_TIMESTAMP
THEN DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
WHEN ContractEndDate <= CURRENT_TIMESTAMP
AND ContractEndDate >= DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
AND ContractStartDate <= CURRENT_TIMESTAMP
THEN DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, ContractEndDate), ContractEndDate), ContractEndDate)
WHEN ContractStartDate >= DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
AND ContractStartDate <= CURRENT_TIMESTAMP
AND ContractStartDate >= CURRENT_TIMESTAMP
THEN DATEDIFF(DAY, ContractStartDate, CURRENT_TIMESTAMP)
WHEN ContractEndDate <= CURRENT_TIMESTAMP
AND ContractEndDate >= DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
AND ContractStartDate >= CURRENT_TIMESTAMP
THEN DATEDIFF(DAY, ContractStartDate, ContractEndDate)
ELSE DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
END
What i am struggling with is how i can exclude Saturday and Sunday so the count never goes past 5 and where Sunday is not the start of the week but instead Monday. So the number of days worked would end up being [ Monday = 1, Tuesday = 2 ... Friday = 5, Saturday = 5, Sunday = 5 ] based on what day the query is run.
What happens at the moment is that if these graphs are run on Saturday the calculation uses 6 days, if its run on Sunday the calculation uses 0 days. Every day during the week is correct, this can be seen with the following query:
DECLARE #StartDate DATETIME = '2019-12-1'
DECLARE #StartDate2 DATETIME = '2019-11-30'
SELECT DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, #StartDate), #StartDate), #StartDate)
SELECT DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, #StartDate2), #StartDate2), #StartDate2)
Results should be 0 and 6.
The only solution i can come up with is to nest the case statement and check if the value = 0 or 6 and change it to a 5, like so:
WHEN ContractStartDate <= DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
AND ContractEndDate >= CURRENT_TIMESTAMP
THEN
CASE
WHEN DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP) IN (0,6)
THEN 5
ELSE DATEDIFF(DAY, DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP)
END
which works but is a bit messy and i am interested to see if there is a better solution to this.
Here's one way, borrowed from Jeff Moden via SQLServerCentral. (I'd comment this reply, but I don't have enough whacky points :( )
--count weekdays between two dates
DECLARE #StartDate DATETIME = '2019-11-01'
DECLARE #EndDate DATETIME = '2019-11-30'
SELECT (DATEDIFF(DD, #StartDate, #EndDate) + 1) --Total days in period, including weekends
-(DATEDIFF(WK, #StartDate, #EndDate) * 2) --minus number of whole weekends in the period * 2 days
-(CASE WHEN DATENAME(DW, #StartDate) = 'Sunday' THEN 1 ELSE 0 END) --minus 1 if the period starts on a Sunday
-(CASE WHEN DATENAME(DW, #EndDate) = 'Saturday' THEN 1 ELSE 0 END) --minus 1 if the period ends on a Saturday
Of course, there are issues here, such as the use of English day names that won't travel well, but those can be worked around if necessary.
I have variation of this built into a "WeekdayCount" function that comes in very handy!
Way more detail here: https://www.sqlservercentral.com/articles/calculating-work-days
additional solution to already posted by #tim-monfries:
DECLARE #StartDate DATETIME = '2019-11-01';
DECLARE #EndDate DATETIME = '2019-11-30';
WITH cte AS
(
SELECT #StartDate AS SomeDate
UNION ALL
SELECT SomeDate+1 FROM cte WHERE SomeDate < #EndDate
)
SELECT COUNT(*)
FROM cte
WHERE DATENAME(dw, SomeDate) NOT IN ('Sunday', 'Saturday');
How can I get the date of specific day ? Like if I have Thursday or month number ?
If I give 12 for instance I want to get the date of 12th day of this month. Or if I give 'Sun' or 'Sat' is it possible to get the dates of these days ?
DATEFROMPARTS function can construct a date from day, month and year.
DATEPARTS does the opposite - gives you the day, month, year, hour, etc. of a date. Or you can use functions like YEAR, MONTH and DAY.
You can deconstruct the value returned by GETDATE function and construct whatever date you want. Here is for example how to get the date for 12th day of the current month:
select DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 12)
Converting 'Sun' or 'Sat' to date is a bit more difficult. First, they aren't quite deterministic. If today is Friday, "Sunday this week" means "next Sunday" in some parts of the world and "last Sunday" in others. You should implement your own logic based on the value returned by DATEPART(dw, GETDATE()) (which will give you the day of the week).
To find the weekday of the current month
DECLARE #daynumber INT = 12
SELECT datename(weekday, dateadd(d, #daynumber - 1, getdate()))
To find the dates of the current month of a given weekday
DECLARE #dayname char(3) = 'sat'
;WITH CTE as
(
SELECt TOP
(datediff(D, eomonth(getdate(), -1),eomonth(getdate())))
dateadd(d,row_number()over(ORDER BY 1/0),
eomonth(getdate(),-1))date
FROM
(values(1),(2),(3),(4),(5),(6))x(x),
(values(1),(2),(3),(4),(5),(6))y(x)
)
SELECT day(date) monthday, date
FROM CTE
WHERE left(datename(weekday, date),3) = #dayname
select sysdatetime(); --2018-12-13 16:29:56.0560574
---If I give 12 for instance I want to get the date of 12th day of this month.
declare #numDate int = 12;
select dateadd(m, datediff(m,0,getdate()),#numDate - 1 ); --2018-12-12 00:00:00.000
--Or if I give 'Sun' or 'Sat' is it possible to get the dates of these days ?
declare #text nvarchar(20) = 'Sunday';
declare #dateStart date = dateadd(month, datediff(month, 0, sysdatetime()), 0),
#days int =( select (DAY(dateadd(dd,-1,DATEADD(m,1,cast(2018 as varchar(4)) + '-' + cast(12 as varchar(2)) +'-01')))));
declare #dateEnd date = DATEADD(day,#days-1,#dateStart);
;WITH CTE (Dates,EndDate) AS
(
SELECT #dateStart AS Dates,#dateEnd AS EndDate
UNION ALL
SELECT DATEADD(day,1,Dates),EndDate
FROM CTE
WHERE DATEADD(day,1,Dates) <= EndDate
)
SELECT CTE.Dates, DATENAME(DW, CTE.Dates)
FROM CTE
where DATENAME(DW, CTE.Dates) = #text;
Result:
Dates,Day
2018/12/2,Sunday
2018/12/9,Sunday
2018/12/16,Sunday
2018/12/23,Sunday
2018/12/30,Sunday
-- Here is how to get week day name to week day number
DECLARE #T TABLE (Dow INT, NameOfDay VARCHAR(15), ShortName CHAR(3));
WITH Days AS
(
SELECT TOP 7
ROW_NUMBER() OVER(PARTITION BY object_id ORDER BY object_id) AS RowNo
FROM
sys.all_columns
)
INSERT INTO #T
SELECT
RowNo,
DATENAME(WEEKDAY, RowNo - 1),
LEFT(DATENAME(WEEKDAY, RowNo - 1), 3)
FROM
Days
SELECT
*
FROM
#T;
-- Here is how to get start of period
SELECT
DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 0) AS StartOfDay,
DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()), 0) AS StartOfWeek,
DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AS StartOfMonth,
DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()), 0) AS StartOfYear;
-- An example
WITH
StartPeriods AS
(
SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()), 0) AS StartOfWeek
),
SelectedDay AS
(
SELECT
Dow - 1 AS Dow,
(SELECT StartOfWeek FROM StartPeriods) AS StartOfWeek
FROM
#T
WHERE
ShortName = 'Wed'
)
SELECT
DATEADD(DAY, Dow, StartOfWeek)
FROM
SelectedDay;
I have a table dbo.participation:
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
User VARCHAR(MAX) NOT NULL,
ParticipationLevel TINYINT NOT NULL,
Selector VARCHAR(MAX) NOT NULL,
DateCreated DATETIME NOT NULL
I created the code below but unfortunately it shows bad performance for #DateStart and #DateStop
SELECT
dateadd(month, datediff(month, 0, DateCreated), 0) AS MDate
,COUNT(CASE WHEN ParticipationLevel >= 10 THEN Selector ELSE NULL END) AS ParticipationLevel1
,COUNT(CASE WHEN ParticipationLevel >= 30 THEN Selector ELSE NULL END) AS ParticipationLevel2
FROM
Participation
WHERE
(#DateStart IS NULL OR (#DateStart IS NOT NULL
AND DateCreated >= #DateStart))
AND (#DateEnd IS NULL OR (#DateEnd IS NOT NULL
AND DateCreate < #DateEnd))
GROUP BY
Dateadd(month, datediff(month, 0, DateCreate), 0)
Do you happen to have any ideas how to improve my code or alternatively how to modify the table to improve performance?
You need an index along the following lines
CREATE INDEX ix
ON dbo.Participation(DateCreated)
INCLUDE (ParticipationLevel);
And you should rewrite the query to get rid of the OR and to avoid the unnecessary reference to a column defined as NOT NULL.
(Note a simple COUNT(Selector) would not look up the value as SQL Server recognizes it can't be NULL but wrapping in an expression defeats this logic)
SELECT DATEADD(month, DATEDIFF(month, 0, DateCreated), 0) AS MDate,
COUNT(CASE
WHEN ParticipationLevel >= 10 THEN 1
END) AS ParticipationLevel1,
COUNT(CASE
WHEN ParticipationLevel >= 30 THEN 1
END) AS ParticipationLevel2
FROM Participation
WHERE DateCreated >= ISNULL(#DateStart, '17530101')
AND DateCreated <= ISNULL(#DateEnd, '99991231')
GROUP BY DATEDIFF(month, 0, DateCreated)
This can give a plan with a seek as below
Note that it would be possible to get rid of the sort by processing chunks of the index a month at the time (possibly in a recursive CTE) but this may be overkill.
Code for that could look something like
/*Cheap to find out from the index*/
IF #DateStart IS NULL
SELECT #DateStart = MIN(DateCreated)
FROM dbo.Participation
IF #DateStart IS NULL
SELECT #DateEnd = MAX(DateCreated)
FROM dbo.Participation
/*Adjust to start of month*/
SELECT #DateStart = DATEADD(month, DATEDIFF(month, 0, #DateStart), 0),
#DateEnd = DATEADD(month, 1 + DATEDIFF(month, 0, #DateEnd), 0);
WITH Dates
AS (SELECT #DateStart AS MDate
UNION ALL
SELECT dateadd(MONTH, 1, MDate) AS MDate
FROM Dates
WHERE dateadd (MONTH, 1, MDate) <= #DateEnd)
SELECT D.MDate,
CA.ParticipationLevel1,
CA.ParticipationLevel2
FROM Dates D
CROSS APPLY (SELECT COUNT(CASE
WHEN ParticipationLevel >= 10
THEN 1
END) AS ParticipationLevel1,
COUNT(CASE
WHEN ParticipationLevel >= 30
THEN 1
END) AS ParticipationLevel2
FROM Participation P WITH (INDEX = ix)
WHERE P.DateCreated >= D.MDate
AND P.DateCreated < DATEADD(MONTH, 1, D.MDate)
GROUP BY () /* So no grouping row returned for empty months */
) CA(ParticipationLevel1, ParticipationLevel2)
OPTION (MAXRECURSION 0);
Which gives a plan with repeated seeks and no sorts
Below two checks not needed in your WHERE clause
#DateStart IS NOT NULL AND
#DateEnd IS NOT NULL AND
SELECT dateadd(month, datediff(month, 0, DateCreated), 0) AS MDate
,COUNT(CASE WHEN ParticipationLevel >= 10 THEN Tracking ELSE NULL END) AS ParticipationLevel1
,COUNT(CASE WHEN ParticipationLevel >= 30 THEN Tracking ELSE NULL END) AS ParticipationLevel2
FROM Participation
WHERE (#DateStart IS NULL OR DateCreated >= #DateStart) AND (#DateEnd IS NULL OR DateCreate < #DateEnd)
GROUP BY Dateadd(month, datediff(month, 0, DateCreate), 0)
I am tryig to write what must be a fairly common audit report; number of rows added to a table over time; reported back against previous cycles to understand the trends in the data.
I have a table that audits creation of rows in the database. It has a field RowEnteredDate date time. I am looking to create an audit report Week/ Month/ Current Quarter / Year.
In my head I am looking at this as multiple passes over the data around the dates; which is quite costly in my database. My reasoning at the moment is
I started out with working out the dates for my year / month / quarter
set datefirst 1
declare #dateranges table (
rangelabel varchar(100),
startdate datetime,
enddate datetime,
myrowcount integer identity(1,1)
)
insert into #dateranges (Rangelabel, startdate, enddate)
select
'Current Year',
DATEADD(yy, DATEDIFF(yy,0,GETDATE()), 0),
DATEADD(ms,-3,DATEADD(yy, DATEDIFF(yy,0,GETDATE() )+1, 0))
insert into #dateranges (Rangelabel, startdate, enddate)
select
'Current Quarter',
DATEADD(qq, DATEDIFF(qq,0,GETDATE()), 0),
DATEADD(qq, DATEDIFF(qq, - 1, getdate()), - 1)
insert into #dateranges (Rangelabel, startdate, enddate)
select
'Current Month',
DATEADD(month, DATEDIFF(month, 0, getdate()), 0),
DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+1,0))
If my table is tblOfUsefullFacts and my date row is RowEnteredDate what is the best way to get the aggregate; broken by Day.
Date Range Mon Tues Wed Thu Fri Sat Sun
Year To date 12000 13000 12000 3200 98000 8900 4000
Quarter 1 302 407 201 97 1732 120 37
Month ...
I can get the totals by day easily enough using a query like this
select
count(*) ,
datepart(weekday, RowEnteredDate)
from
tblOfUsefullFacts aa
Where
datepart(weekday, RowEnteredDate) is not null
group by datepart(weekday, RowEnteredDate)
order by datepart(weekday, RowEnteredDate) sac
This selects the data out row by row; which i could pivot and loop round to get the data. Im slightly nervous as the real numbers are in the 10's of millions in them and would like to not impact the underlying processing if i can avoid it.
As i need to do this in multiple passes is there a lighter way to do this without running the loops to get the totals? Or a mechanism in SQL my fuzzy brain is ignoring.
This should give you an idea how to do it. Sorry for any syntax errors, it isn't tested.
;with cte as
(
select
d.rangelabel,
datepart(weekday, RowEnteredDate) as WkDay,
count(*) as RowCt
from tblOfUsefullFacts f
join #dateranges d on f.RowEnteredDate between d.startdate and d.enddate
Where datepart(weekday, RowEnteredDate) is not null
group by d.rangelabel,datepart(weekday, RowEnteredDate)
)
select
RangeLabel,
sum(case when WkDay = 1 then RowCt else 0 end) as Sunday,
sum(case when WkDay = 2 then RowCt else 0 end) as Monday,
sum(case when WkDay = 3 then RowCt else 0 end) as Tuesday,
sum(case when WkDay = 4 then RowCt else 0 end) as Wednesday,
sum(case when WkDay = 5 then RowCt else 0 end) as Thursday,
sum(case when WkDay = 6 then RowCt else 0 end) as Friday,
sum(case when WkDay = 7 then RowCt else 0 end) as Saturday
from cte
group by RangeLabel
I have a table in SQL Server which have following attributes.
ProjectID || Start_Date || End Date || Duration(Days)
1 10-Jan-2013 5
2 02-FEB 2013 16
3 26-Mar-2013 50
. . .
I want to find start dates based upon the qualified days (Monday-Friday). For example for End Date: 10 January start date will be 04 January as 5 and 6 January are Saturday and Sunday.
I want to know how this could be possible in T-SQL (Function,Custom T-SQL Block). Any guidance and help is highly appreciated.
This should do it:
WITH tblProjects2 AS (
SELECT ProjectId, DATEADD(DAY, -Duration, EndDate) AS StartDate FROM tblProjects
)
SELECT ProjectId,
CASE WHEN DATENAME(DW, StartDate) = 'Sunday' THEN DATEADD(day, -2, StartDate)
WHEN DATENAME(DW, StartDate) = 'Saturday' THEN DATEADD(day, -1, StartDate)
ELSE StartDate END AS ProperStartDate
FROM tblProjects2
The approach is rather simple - when your new date falls on a weekend, subtract 1 or 2 days, depending on whether it's Saturday or Sunday respectively.
Test case structure for tblProjects:
CREATE TABLE [dbo].[tblProjects](
[ProjectId] [int] NULL,
[StartDate] [date] NULL,
[EndDate] [date] NULL,
[Duration] [int] NULL
)
Test case data for same:
INSERT INTO tblProjects VALUES (1, NULL, '10-Jan-2013', 5);
INSERT INTO tblProjects VALUES (2, NULL, '02-FEB-2013', 16);
INSERT INTO tblProjects VALUES (3, NULL, '26-Mar-2013', 50);
EDIT - Same functionality, using a function:
CREATE FUNCTION dbo.getStartDate(#EndDate Date, #Duration int)
RETURNS DATE
AS
BEGIN
DECLARE #newDate DATE;
SET #newDate = DATEADD(day, -#Duration, #EndDate);
RETURN (CASE
WHEN DATENAME(DW, #newDate) = 'Sunday' THEN DATEADD(day, -2, #newDate)
WHEN DATENAME(DW, #newDate) = 'Saturday' THEN DATEADD(day, -1, #newDate)
ELSE #newDate END)
END;
Then you can rewrite the above query like this:
SELECT ProjectId, dbo.getStartDate(EndDate, Duration) AS StartDate
FROM tblProjects
A bit harder to read, but slightly faster
SELECT ProjectId,
DATEADD(d, -Duration - CASE DATEDIFF(d, +Duration, EndDate) % 7
WHEN 5 THEN 1
WHEN 6 THEN 2
ELSE 0 END
, EndDate)
FROM tblProjects