I try to loop one sql statement multiple times to retrieve the weekly revenue of on particular store. Here is my best guess which does not work.
SELECT *
DECLARE #i int = 0
WHILE #i < 52 BEGIN
SET #i = #i + 1
FROM dbo.revenue
WHERE DATEPART(WW, date) = #i
AND storenumber = '005'
END
You actually never want to write loops in SQL.
SELECT
SUM(earnings) weekly_earnings
FROM
dbo.revenue
WHERE
storenumber = '005'
AND date >= '2015-01-01'
AND date < '2016-01-01'
GROUP BY
DATEPART(WW, date)
Left join that against a helper table that contains 52 rows (1 through 52) representing the weeks to fill in the blanks (weeks with no revenue).
Note that the date >= '2015-01-01' AND date < '2016-01-01' exists because:
you must limit the query to one year, or the week number becomes ambiguous
it is superior to DATEPART(YY, date) = 2015 because, being a calculation, that expression would not be able to use an index on the date column, whereas >= and < can use an index
EDIT: Instead of a temporary table you can use a recursive CTE on SQL Server:
WITH WeekNumbers (WkNum) AS (
SELECT 1 AS WkNum
UNION ALL
SELECT w.WkNum + 1 FROM WeekNumbers w WHERE w.WkNum <= 52
)
SELECT
w.WkNum,
SUM(r.earnings) weekly_earnings
FROM
WeekNumbers w
LEFT JOIN dbo.revenue r ON w.WkNum = DATEPART(WW, r.date)
WHERE
r.storenumber = '005'
AND r.date >= '2015-01-01'
AND r.date < '2016-01-01'
GROUP BY
w.WkNum
You could just group your data and use some aggregation?
Select Sum(Revenue) AS [TotalRevenuePerWeek],
DatePart(ww, RevenueDate) as [WeekNumber]
From dbo.Revenue
Where StoreNumber = '005'
And DatePart(year, RevenueDate) = 2015
Group By DatePart(ww, RevenueDate)
Related
I have a table of Tasks where I have records for a particular date. I want to have all dates in one month displayed with numbers of tasks per date. If on some date there were no record of a task it should be written 0.
I have results with duplicating records from the same date when there were tasks on a given day.
Table:
Date Tasks
2021-08-01 0
2021-08-02 0
2021-08-03 0
2021-08-03 25
2021-08-04 0
2021-08-04 18
2021-08-05 0
2021-08-05 31
2021-08-06 0
SQL code I am using:
Declare #year int = 2021, #month int = 8;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value +1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year, #month, 1)))
)
SELECT datefromparts(#year, #month, numbers.value) AS 'Datum', 0 AS 'Tasks' FROM numbers
UNION
SELECT CONVERT(date, added_d) AS 'Datum', COUNT(*) AS 'Tasks' FROM Crm.Task
WHERE YEAR(added_d) = '2021' AND MONTH(added_d) = '8' GROUP BY CONVERT(date, added_d)
How can I remove duplicates that I will have only one date record 21-08-03 with 25 tasks?
Thank you for your help
You requires OUTER JOIN :
WITH numbers as (
Select datefromparts(#year, #month, 1) as value
UNION ALL
Select DATEADD(DAY, 1, value) as value
from numbers
where value < EOMONTH(value)
)
select num.value, COUNT(tsk.added_d) AS Tasks
from numbers num left join
Crm.Task tsk
on CONVERT(date, tsk.added_d) = num.value
GROUP BY num.value;
If you want all dates for one month, you can do:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
You can then incorporate this into the logic using an outer join or subquery:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
select d.dte, count(t.added_d)
from dates d left join
Crm.Task t
on convert(date, t.added_d) = d.dte
group by d.dte
order by d.dte;
You can easily extend the logic for the CTE for more than one month, by adjusting the where clause in the second clause.
Is there a way how to add null values rows, replacing with 0?
Ideally,I want to show data of all Month, if there is no data then count column set to zero.
Currently SQL query is not Returning Empty Rows and I'm getting output like this...
I'm using the code below to get the result set below that:
SELECT
DATENAME(MONTH,A.Date) 'Month', COUNT(LateStatus) 'Count', 'Late' 'Type'
FROM
Attendance1 A
WHERE
A.EnrollId = '10000' AND A.Date BETWEEN '2016-01-01' AND '2016-08-31' AND LateStatus = 'L'
GROUP BY
DATENAME(MONTH,A.Date)
Use a recursive CTE (dynamically makes a calendar table):
With Months(mon) As
(Select dateAdd(month, datediff(month, 365, getdate()), 0)
Union All
Select DateAdd(month, 1, mon) from Months
Where mon < getdate())
Select DATENAME(MONTH, m.mon ) 'Month',
COUNT(LateStatus) 'Count', 'Late' 'Type'
FROM months m
left join Attendance1 a
on a.Date between m.mon and dateadd(month, 1, m.mon)
and a.EnrollId = '10000'
and a.LateStatus = 'L'
GROUP BY DATENAME(MONTH, m.Mon)
A more generic function, to generate a range of integers which can be used for any expression, is this:
create function dbo.NumberList (#start integer, #end integer)
returns table
as Return
(
With ints(aInt) As
(Select #start Union All
Select aInt + 1 From ints
Where aInt < #end)
Select aInt from ints
)
Once this function has been created, you can use it to dynamically create any range of values of any type:
For months from 8 months ago to 3 months ago
Select i.aInt, Dateadd(month, i.aInt,
DateAdd(month, datediff(month(0, getdate()), 0))
From dbo.NumberList(-8, -3) i
Assuming you have some data for all months, you can do this using conditional aggregation. That is, move the WHERE conditions to the SELECT using CASE:
SELECT DATENAME(MONTH,A.Date) as Mon,
SUM(CASE WHEN A.EnrollId = '10000' AND LateStatus = 'L'
THEN 1 ELSE 0
END) as cnt,
'Late' as 'Type'
FROM Attendance1 A
WHERE AND A.Date BETWEEN '2016-01-01' AND '2016-08-31'
GROUP BY DATENAME(MONTH, A.Date)
ORDER BY MIN(A.Date);
There are other methods, usually involving LEFT JOIN. But this is the simplest if it solves the problem.
I have a tricky issue I am struggling with on a mental level.
In our db we have a table showing the UK Holidays for the next few years, and a stored function returns a recordset to my front end.
I have a flag in my recordset called 'deletable' which allows the frontend to decide if a context menu can be shown in the data grid, thus allowing that record to be deleted.
Currently the test (in my stored proc) just checks if the date column has a date from three days ago or more.
case when DATEDIFF(d,a.[date],GETDATE()) > 3 then 1 else 0 end as [deletable]
how can I modify that to find the previous working date by checking weekends and the Holidays table 'Holiday' column (which is a Datetime) and see if the [date] column in my recordset row is 3 working days before, taking into account Holidays from the Holidays table and weekends?
so if the [date] column is 23th May, and todays's date is 28th May, then that column returns 0, as the 27th was a bank holiday, whereas the next day it would return 1 because there would be more than 3 working days difference.
Is there an elegent way to do that?
thanks
Philip
Okay I'm totally refactoring this.
declare
#DeletablePeriodStart datetime,
#BusinessDays int
set #DeletablePeriodStart = dateadd(d,0,datediff(d,0,getdate()))
set #BusinessDays = 0
while #BusinessDays < 3
begin
set #DeletablePeriodStart = dateadd(d,-1,#DeletablePeriodStart)
if datepart(dw,#DeletablePeriodStart) not in (1,7) and
not exists (select * from HolidayTable where Holiday = #DeletablePeriodStart)
begin
set #BusinessDays = #BusinessDays + 1
end
end
This time it doesn't make any assumptions. It runs a quick loop checking whether each day is a valid business day and doesn't stop till it counts three of them. Then later just check whether a.[date] >= #DeletablePeriodStart
You should substract the number of holidays between a.[date] and GETDATE() from the DATEDIFF. Try something like this:
case when DATEDIFF(d,a.[date],GETDATE())-(
SELECT COUNT(*) FROM Holidays
WHERE HolidayDate BETWEEN a.[date] AND GETDATE()
)>3 then 1 else 0 end as [deletable]
Razvan
I am assuming that you don't have a Calendar table, although I'd highly recommend creating one, you can still achieve this without one:
The following will just get you a list of 2047 dates from yesterday going backwards (using the system table Master..spt_values):
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
)
SELECT Dates.Date
FROM Dates
ORDER BY Dates.Date DESC;
You then need to exclude weekends, and holidays from your table using this:
SET DATEFIRST 1;
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
)
SELECT Dates.Date
FROM Dates
WHERE DATEPART(WEEKDAY, Dates.Date) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE Dates.Date = h.HolidayDate
)
ORDER BY Dates.Date DESC;
N.B. You should explicitly set your DATEFIRST and not rely on server defaults
The above gives you a list of working days prior to today, you can then use the ROW_NUMBER() function, get the 3rd occurance in the list, giving a final query:
WITH Dates AS
( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE))
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
), WorkingDays AS
( SELECT Dates.Date, RN = ROW_NUMBER() OVER(ORDER BY Dates.Date DESC)
FROM Dates
WHERE DATEPART(WEEKDAY, Dates.Date) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE Dates.Date = h.HolidayDate
)
)
SELECT WorkingDays.Date
FROM WorkingDays
WHERE RN = 3;
Or if you prefer this can be done with one query (exact same principle as above):
SELECT d.Date
FROM ( SELECT Date = DATEADD(DAY, -number, CAST(GETDATE() AS DATE)), RN = ROW_NUMBER() OVER(ORDER BY number)
FROM Master..spt_values
WHERE type = 'P'
AND number > 0
AND DATEPART(WEEKDAY, DATEADD(DAY, -number, CAST(GETDATE() AS DATE))) <= 5
AND NOT EXISTS
( SELECT 1
FROM HolidayTable h
WHERE DATEADD(DAY, -number, CAST(GETDATE() AS DATE)) = h.HolidayDate
)
) d
WHERE rn = 3;
If I have a table with a StartDate column and an EndDate column can I produce a query that returns a set including every day in the range. I could use a table variable and do some procedural code but I'd like to know if there's a way to do it in a query.
E.g. StartDate = 1/1/2010, EndDate = 1/5/2010, result would be:
1/1/2010
1/2/2010
1/3/2010
1/4/2010
1/5/2010
...for every row in the table that has the StartDate and EndDate columns.
*I'm on SQL 2005
SQL Server 2005+:
WITH dates AS (
SELECT t.startdate 'date'
FROM TABLE t
WHERE t.startdate = '1/1/2010'
UNION ALL
SELECT DATEADD(dd, 1, t.date)
FROM dates t
WHERE DATEADD(dd, 1, t.date) <= (SELECT t.enddate FROM TABLE t WHERE t.enddate = '1/5/2010'))
SELECT ...
FROM TABLE t
JOIN dates d ON d.date = t.date
If your dates are no more than 2047 days apart:
SELECT DATEADD(day,
n.number,
(SELECT t.startdate FROM TABLE t WHERE t.startdate = '1/1/2010')
)
FROM (SELECT DISTINCT number
FROM MASTER.dbo.SPT_VALUES
WHERE name IS NULL) n
WHERE DATEADD(day, n.number, (SELECT t.startdate FROM TABLE t WHERE t.startdate = '1/1/2010')) <= (SELECT t.endate FROM TABLE t WHERE t.endate = '1/5/2010')
with DateList as
(
select cast('1/1/2010' as datetime) DateValue
union all
select DateValue + 1
from DateList
where DateValue + 1 >= '1/1/2010' AND DateValue +1 <= '1/5/2010'
)
select CONVERT(varchar, DateValue, 101)
from DateList
OPTION (MAXRECURSION 0)
Given a table:
create table #orders (
orderid int,
orderdatetime datetime
)
What is the best way to write sql to output a report containing the count of orders from the current and previous 24 hours, total orders for the current day and previous 7 days, total orders for the current week and previous 4 weeks, and total orders for the month and previous 6 months?
I'm wondering if this can be efficiently rolled up into a single sql using analytical functions, or if 4 sql statements generating the 4 groups of data is the only (or best) way.
Also, given the hourly/day/week grouping, how would one do that in sql server? Datetimes seem to be a pain in the ass everytime I have to do something like this with them...
Ideas? Put into a SSAS cube and do it from there maybe?
SELECT DATEPART(month, orderdatetime), DATEPART(week, orderdatetime), DATEPART(day, orderdatetime), COUNT(*)
FROM #orders
GROUP BY
DATEPART(month, orderdatetime), DATEPART(week, orderdatetime), DATEPART(day, orderdatetime) WITH ROLLUP
This will group the COUNT's by day, week and month in a single query.
The week rollups will have a NULL in DATEPART(day, orderdatetime) column, the month rollups will have a NULL in both DATEPART(day, orderdatetime) and DATEPART(week, orderdatetime) columns.
To make it for every hour, day, week or month from the current without gaps, use CTE's:
WITH q_hours AS
(
SELECT 0 AS col_hour
UNION ALL
SELECT col_hour + 1
FROM q_hours
WHERE col_hour < 22
),
q_days AS
(
SELECT 0 AS col_day
UNION ALL
SELECT col_day + 1
FROM q_days
WHERE col_day < 31
),
q_months AS
(
SELECT 0 AS col_month
UNION ALL
SELECT col_month + 1
FROM q_months
WHERE col_month < 12
)
SELECT col_month, col_day, col_hour, COUNT(orderid)
FROM q_hours
CROSS JOIN
q_days
CROSS JOIN
q_months
LEFT JOIN
#orders
ON DATEDIFF(month, orderdatetime, GETDATE()) = col_month
AND DATEDIFF(day, orderdatetime, GETDATE()) % 31 = col_day
AND DATEDIFF(hour, orderdatetime, GETDATE()) % 24 = col_hour
GROUP BY
col_month, col_day, col_hour WITH ROLLUP
HAVING (
col_month = 0
AND col_day = 0
AND col_hour IS NOT NULL
) -- all hours within 24 hours from now
OR
(
col_month = 0
AND col_day <= 7
AND col_hour IS NULL
) -- all days within 7 days from now
OR
(
col_month <= 6
AND col_day IS NULL
AND col_hour IS NULL
) -- all months within 6 months from now
You could run the four selects from a "dummy table" or an "identity" table that consists of a single row.
You could have:
SELECT
(<query count of orders current/prev 24 hours>) as <name1>,
(<total orders current + 7 days>) as <name2>,
(<total orders current week + 4 weeks>) as <name3>,
(<total orders month + 6 months>) as <name4>
FROM
<IDENTITY table>;
Because you want different timeframes for each datepart type, using a single query with rollup probably won't give you what you want. I'd consider just unioning them all together similar to something like this...
SELECT DatePartValue = DATEPART(HH, orderdatetime),
Type = 'Hourly',
COUNT(*)
FROM #orders
WHERE orderdatetime > DATEADD(HH, -25, GETDATE())
GROUP BY DATEPART(HH, orderdatetime)
UNION
SELECT DATEPART(DD, orderdatetime),
Type = 'Daily',
COUNT(*)
FROM #orders
WHERE orderdatetime > DATEADD(DD, -8, GETDATE())
GROUP BY DATEPART(DD, orderdatetime)
UNION
SELECT DATEPART(WEEK, orderdatetime),
Type = 'Weekly',
COUNT(*)
FROM #orders
WHERE orderdatetime > DATEADD(WEEK, -5, GETDATE())
GROUP BY DATEPART(WEEK, orderdatetime)
ORDER BY Type, DatePartValue
UNION
SELECT DATEPART(MM, orderdatetime),
Type = 'Monthly',
COUNT(*)
FROM #orders
WHERE orderdatetime > DATEADD(MM, -7, GETDATE())
GROUP BY DATEPART(MM, orderdatetime)
ORDER BY Type, DatePartValue
For results in one row, something like this:
select
orders_day = sum(case when datediff(hour,orderdatetime,getdate()) < 24 then 1 else 0 end)
, orders_week = sum(case when datediff(day,orderdatetime,getdate()) < 7 then 1 else 0 end)
, orders_month = sum(case when datediff(week,orderdatetime,getdate()) < 4 then 1 else 0 end)
, orders_half = sum(case when datediff(month,orderdatetime,getdate()) < 6 then 1 else 0 end)
from #orders
You may want to fine tune the date criteria to get appropriate behavior.
For multiple rows, take the results above and transpose it with UNPIVOT or CASE .. CROSS JOIN.
SELECT
sum(case when orderdatetime between GetDate() - 1 and GetDate() then 1 else 0 end) as Current24Hours,
sum(case when orderdatetime between GetDate() - 2 and GetDate() - 1 then 1 else 0 end) as Previous24Hours,
sum(case when orderdatetime between GetDate() - 7 and GetDate() then 1 else 0 end) as Current7Days,
sum(case when orderdatetime between GetDate() - 14 and GetDate() - 7 then 1 else 0 end) as Previous7Days,
sum(case when DATEDIFF (m, OrderDate, #now) <= 1 then 1 else 0 end) as PreviousMonth,
sum(case when DATEDIFF (m, OrderDate, #now) <= 6 then 1 else 0 end) as PreviousSixMonths
FROM orders
I think you want grouping sets. I've understand that sql server supports grouping sets.
EDIT1: I've read that sql server 2005 doesn't support grouping sets but sql server 2008 does. Here an interesting read on a presumed but not existing difference between mapreduce and an rdbms like Oracle and Sql Server. Please read the comments too!! http://www.data-miners.com/blog/2008/01/mapreduce-and-sql-aggregations.html