SQL reflect data for all days - sql

I am trying to create a table in SQL where I reflect data for all days of a particular month.
For example, even if there is no Sale transaction for a particular day, the day for the particular employee should still be reflective in the table.
Scenario for the question. I am working with the following dates for this example: 1st, 2nd, 3rd and 4th Jan 2022.
The raw data is as follows:
Name
Date
SaleAmount
John
2022-01-01
154875
John
2022-01-03
598752
As seen above, we only have data for the 1st and 3rd.
The outcome should look as follows:
Name
Date
SaleAmount
John
2022-01-01
154875
John
2022-01-02
NULL
John
2022-01-03
598752
John
2022-01-04
NULL
As seen above, the 2nd and 4th should be included even though there was no activity for those days.
What I am currently trying:
I have a master date table which is being used as a RIGHT JOIN on the transaction table. However, the final outcome of my table is as follows:
Name
Date
SaleAmount
John
2022-01-01
154875
NULL
2022-01-02
NULL
John
2022-01-03
598752
NULL
2022-01-04
NULL
As seen above, the 'Name' field returns as NULL. The SaleAmount however should reflect NULL to indicate no transactions happening.
I would appreciate any assistance on this.

Seems like you want to
Start with the date table
Cross join to your employee/salesperson table so you now have one row for each salesperson on each date
Left join the sales orders for that date + salesperson combo to get the sum of their sales for that day. If they have none, it'll show null:
select emp.Name
,dat.Date
,sum(ord.Amount) as SaleAmount
from dateList dat
cross join salesPerson emp
left join salesOrder ord on ord.OrderDate = dat.Date and ord.SalesPersonId = emp.SalesPersonId
group by emp.Name
,dat.Date

you can create a list of dates on the fly. as example per month
Declare #year int = 2022, #month int = 7;
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) Datum FROM numbers
then left join to your table.

You may use a Recursive CTE as the following:
Declare #startDate as date ='2022-01-01';
Declare #endDate as date ='2022-01-31';
With CTE As
(
Select Distinct Name_ nm,#startDate dt From SalesDates
Where Date_ Between #startDate And #endDate
Union All
Select nm, DateAdd(Day,1,dt) From CTE
Where DateAdd(Day,1,dt)<=#endDate
)
Select C.nm as [Name],C.dt as [Date], S.SaleAmount
From CTE C Left Join SalesDates S
On S.Date_=C.dt
And S.Name_=C.nm
Order By C.nm,C.dt
You can change the values of #startDate and #endDate according to the period you want.
See a demo from db<>fiddle.

Related

Period over period SQL script?

I have a dataset of 2 columns: 'Date' and 'Total Sales'. My dates are 01-01-2021, 02-01-2021... so on and so forth up until 12-01-2022. I basically want to add another row where I have a "previous month" column that gives me the total sales for the previous month in the same row as the current month (else null) -- e.g. say I have 2 rows in my date column 01-01-2021 and 02-01-2021 and total sales would be $10 and $20 respectively. How do can I create a column that would show the following:
Date |Sales | Previous Month Sales|
---------------------------------------------
01-01-2021 | $10 | null
02-01-2021 | $20 | $10
So on and so forth; this is my query:
CASE
WHEN `Date` > DATE_SUB(`Date`, INTERVAL 1 MONTH)
THEN `Monthly Sales`
ELSE 'null'
END
Thanks in advance
Well, Domo's back-end is running a MySQL back-engine (from what I recall the last time I touched Domo [2018])
I think this is just a SQL question, and I wonder if a simple windowing function would do the trick.
select Date,
Sales,
max (case when *month* = *this month -1* then Sales else null end) over (order by 1) as "Previous Month Sales"
from table
You just need to figure out how to break down the Date into the month based on whatever SQL dialect Domo uses nowadays.
Cheers
I think domo support MySQL-like language, so you could do something like this:
with cte as
(
select date,
date + interval 1 month as next_month,
sales
from sales
)
select a.date,
a.sales as current_sales,
b.sales as prior_month_sales
from sales a
left join cte b
on b.next_month = a.date
order by a.date
I do this by joining the table onto itself with a LEFT OUTER JOIN. The outer join allows you to keep the null value for previous month. You match the date such that 1 column is calculated to show the previous month (I do this with EOMONTH() to ensure I always get the previous month and account for the year, if say it is January).
IF OBJECT_ID('TEMPDB..#TEMP') IS NOT NULL
DROP TABLE #TEMP
CREATE TABLE #TEMP(
[Date] DATE
,[Sales] INT
)
INSERT INTO #TEMP([Date],[Sales])
VALUES ('2020-12-20',50)
,('2021-01-20',100)
,('2021-02-20',200)
,('2021-03-20',300)
,('2021-04-20',400)
,('2021-05-20',500)
SELECT #TEMP.[Date]
,#TEMP.Sales
,TEMPII.Date [PREV M]
,TEMPII.Sales [PREV M SALES]
FROM #TEMP
LEFT OUTER JOIN #TEMP TEMPII
ON YEAR(EOMONTH(#TEMP.[Date],-1))*100+MONTH(EOMONTH(#TEMP.[Date],-1)) = YEAR(TEMPII.[Date])*100+MONTH(TEMPII.[Date])
ORDER BY #TEMP.[Date]
Output:

SQL Server full outer group not getting all values

I am trying to full join two tables by their date with one table having columns call 'date', 'parties', 'total', etc and another table just having dates.
Below is the query I have:
SELECT
rangeDates.[ListOFDates],
partiesDetails.[Party], partiesDetails.[Amount]
FROM rangeDates
FULL OUTER JOIN partiesDetails
ON rangeDates.[ListOFDates] = partiesDetails.[Date]
Now is the table rangeDates I have. Also there's a date for everyday for a set period of dates, for exmaple, below it starts at '2017-02-02' and may have a date everyday till '2018-03-01'
Ref ListOFDates
1 2017-02-02
2 2017-02-03
3 2017-02-04
.........
And in the partiesDetails table
Date Party Amount
2017-02-03 Tuf 5000
2017-04-01 Tuf 2000
2017-05-22 Wing 3000
.................
The ideal results I would want is:
ListOfDates Party Amount
2017-02-02 NULL NULL
2017-02-03 Tuf 5000
2017-02-04 NULL NULL
............
I feel that maybe you should be using a calendar table here:
WITH dates AS (
SELECT CAST('20170202' AS date) AS [date]
UNION ALL
SELECT DATEADD(dd, 1, [date])
FROM dates
WHERE DATEADD(dd, 1, [date]) <= '2018-03-01'
)
SELECT
d.date,
p.[Party],
p.[Amount]
FROM dates d
LEFT JOIN partiesDetails p
ON d.date = p.[Date]
ORDER BY
d.date;
I make this suggestion because you used the language and may have a date everyday till, which seems to imply that maybe the rangeDates table does not in fact cover the entire date range you have in mind.

Get valid orders at the starting day of each year

In a table containing Order information (call it Order) we have the following fields:
OrderId int
OrderDate Date
BindingTime int
Binding time is in months.
An order is called "Active" between its OrderDate and DATEADD(mm, BindingTime, OrderDate).
What I'd like to do is to group the orders by year so that if an order is "active" on the first day of a year it would be taken into account. The aim is to calculate each year's inbound and outbound orders. So the query result will be COUNT of orders and the year. And by year we mean the number of orders which were active on the first day of that year.
Mind that, we would like to have all the years between two given numbers in our results. E.g. If there was no active order on the first day of 2016 we would still like to to have a row for (0, 2016).
I've used a recursive CTE to generate a range of years, so that a 'zero' year will not be omitted
declare #YEAR1 as date = '20110101';
declare #YEAR2 as date = '20190101';
WITH YEARS AS (SELECT #YEAR1 y
UNION ALL
SELECT dateadd(year,1,y) FROM YEARS WHERE y < #YEAR2)
SELECT YEARS.y,count(0) YearStartActiveOrders FROM YourTable
CROSS JOIN YEARS
WHERE YEARS.y BETWEEN CAST(orderdate as date)
AND CAST(DATEADD(mm, BindingTime, OrderDate) as date)
GROUP BY Years.y
Seems like what you need is a Date table (having a list of all days per year) and left joining that table with your grouped data (active order count, per day of the year).
You can use this date table from Aaron Bertrand. I generated the #dim table with the following params, to only generate two years data (2015, 2016):
DECLARE #StartDate DATE = '20150101', #NumberOfYears INT = 2;
Then you can do the following:
with ordertable as
(
select 1 as orderid, '20160101' as orderdate, 2 as bindingtime union all
select 2, '20160305', 3 union all
select 3, '20160305', 5 union all
select 4, '20150305', 5
)
select d.year, isnull(count(orderid), 0) nrActiveOrdersFirstDayOfYear
from #dim d
left join ordertable g on d.year = year(g.orderdate)
and g.orderdate = d.date
and d.FirstOfYear between g.orderdate and DATEADD(mm, g.bindingtime, OrderDate)
group by d.year
With the sample data I took as an example, you would get the result:
year nrActiveOrdersFirstDayOfYear
2015 0
2016 1
Working demo here.

Finding most recent date based on consecutive dates

I have s table that lists absences(holidays) of all employees, and what we would like to find out is who is away today, and the date that they will return.
Unfortunately, absences aren't given IDs, so you can't just retrieve the max date from an absence ID if one of those dates is today.
However, absences are given an incrementing ID per day as they are inputt, so I need a query that will find the employeeID if there is an entry with today's date, then increment the AbsenceID column to find the max date on that absence.
Table Example (assuming today's date is 11/11/2014, UK format):
AbsenceID EmployeeID AbsenceDate
100 10 11/11/2014
101 10 12/11/2014
102 10 13/11/2014
103 10 14/11/2014
104 10 15/11/2014
107 21 11/11/2014
108 21 12/11/2014
120 05 11/11/2014
130 15 20/11/2014
140 10 01/03/2015
141 10 02/03/2015
142 10 03/03/2015
143 10 04/03/2015
So, from the above, we'd want the return dates to be:
EmployeeID ReturnDate
10 15/11/2014
21 12/11/2014
05 11/11/2014
Edit: note that the 140-143 range couldn't be included in the results as they appears in the future, and none of the date range of the absence are today.
Presumably I need an iterative sub-function running on each entry with today's date where the employeeID matches.
So based on what I believe you're asking, you want to return a list of the people that are off today and when they are expected back based on the holidays that you have recorded in the system, which should only work only on consecutive days.
SQL Fiddle Demo
Schema Setup:
CREATE TABLE EmployeeAbsence
([AbsenceID] int, [EmployeeID] int, [AbsenceDate] DATETIME)
;
INSERT INTO EmployeeAbsence
([AbsenceID], [EmployeeID], [AbsenceDate])
VALUES
(100, 10, '2014-11-11'),
(101, 10, '2014-11-12'),
(102, 10, '2014-11-13'),
(103, 10, '2014-11-14'),
(104, 10, '2014-11-15'),
(107, 21, '2014-11-11'),
(108, 21, '2014-11-12'),
(120, 05, '2014-11-11'),
(130, 15, '2014-11-20')
;
Recursive CTE to generate the output:
;WITH cte AS (
SELECT EmployeeID, AbsenceDate
FROM dbo.EmployeeAbsence
WHERE AbsenceDate = CAST(GETDATE() AS DATE)
UNION ALL
SELECT e.EmployeeID, e.AbsenceDate
FROM cte
INNER JOIN dbo.EmployeeAbsence e ON e.EmployeeID = cte.EmployeeID
AND e.AbsenceDate = DATEADD(d,1,cte.AbsenceDate)
)
SELECT cte.EmployeeID, MAX(cte.AbsenceDate)
FROM cte
GROUP BY cte.EmployeeID
Results:
| EMPLOYEEID | Return Date |
|------------|---------------------------------|
| 5 | November, 11 2014 00:00:00+0000 |
| 10 | November, 15 2014 00:00:00+0000 |
| 21 | November, 12 2014 00:00:00+0000 |
Explanation:
The first SELECT in the CTE gets employees that are off today with this filter:
WHERE AbsenceDate = CAST(GETDATE() AS DATE)
This result set is then UNIONED back to the EmployeeAbsence table with a join that matches EmployeeID as well as the AbsenceDate + 1 day to find the consecutive days recursively using:
-- add a day to the cte.AbsenceDate from the first SELECT
e.AbsenceDate = DATEADD(d,1,cte.AbsenceDate)
The final SELECT simply groups the cte results by employee with the MAX AbsenceDate that has been calculated per employee.
SELECT cte.EmployeeID, MAX(cte.AbsenceDate)
FROM cte
GROUP BY cte.EmployeeID
Excluding Weekends:
I've done a quick test based on your comment and the below modification to the INNER JOIN within the CTE should exclude weekends when adding the extra days if it detects that adding a day will result in a Saturday:
INNER JOIN dbo.EmployeeAbsence e ON e.EmployeeID = cte.EmployeeID
AND e.AbsenceDate = CASE WHEN datepart(dw,DATEADD(d,1,cte.AbsenceDate)) = 7
THEN DATEADD(d,3,cte.AbsenceDate)
ELSE DATEADD(d,1,cte.AbsenceDate) END
So when you add a day: datepart(dw,DATEADD(d,1,cte.AbsenceDate)) = 7, if it results in Saturday (7), then you add 3 days instead of 1 to get Monday: DATEADD(d,3,cte.AbsenceDate).
You'd need to do a few things to get this data into a usable format. You need to be able to work out where a group begins and ends. This is difficult with this example because there is no straight forward grouping column.
So that we can calculate when a group starts and ends, you need to create a CTE containing all the columns and also use LAG() to get the AbsenceID and EmployeeID from the previous row for each row. In this CTE you should also use ROW_NUMBER() at the same time so that we have a way to re-order the rows into the same order again.
Something like:
WITH
[AbsenceStage] AS (
SELECT [AbsenceID], [EmployeeID], [AbsenceDate]
,[RN] = ROW_NUMBER() OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
,[AbsenceID_Prev] = LAG([AbsenceID]) OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
,[EmployeeID_Prev] = LAG([EmployeeID]) OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
FROM [HR_Absence]
)
Now that we have this we can compare each row to the previous to see if the current row is in a different "group" to the previous row.
The condition would be something like:
[EmployeeID_Prev] IS NULL -- We have a new group if the previous row is null
OR [EmployeeID_Prev] <> [EmployeeID] -- Or if the previous row is for a different employee
OR [AbsenceID_Prev] <> ([AbsenceID]-1) -- Or if the AbsenceID is not sequential
You can then use this to join the CTE to it's self to find the first row in each group with something like:
....
FROM [AbsenceStage] AS [Row]
INNER JOIN [AbsenceStage] AS [First]
ON ([First].[RN] = (
-- Get the first row before ([RN] Less that or equal to) this one where it is the start of a grouping
SELECT MAX([RN]) FROM [AbsenceStage]
WHERE [RN] <= [Row].[RN] AND (
[EmployeeID_Prev] IS NULL
OR [EmployeeID_Prev] <> [EmployeeID]
OR [AbsenceID_Prev] <> ([AbsenceID]-1)
)
))
...
You can then GROUP BY the [First].[RN] which will now act like a group id and allow you to get the start and end date of each absence group.
SELECT
[Row].[EmployeeID]
,MIN([Row].[AbsenceDate]) AS [Absence_Begin]
,MAX([Row].[AbsenceDate]) AS [Absence_End]
...
-- FROM and INNER JOIN from above
...
GROUP BY [First].[RN], [Row].[EmployeeID];
You could then put all that into a view giving you the EmployeeID with the Start and End date of each absence. You can then easily pull out the Employee's currently off with a:
WHERE CAST(CURRENT_TIMESTAMP AS date) BETWEEN [Absence_Begin] AND [Absence_End]
SQL Fiddle
Like another answer here, I'm going to create the leave intervals, but via a different method. First the code:
declare #today date = getdate(); --use whatever date here
with g as (
select *, dateadd(day, -1 * row_number() over (partition by employeeid order by absencedate), AbsenceDate) as group_number
from employeeabsence
) , leave_intervals as (
select employeeid, min(absencedate) as [start], max(absencedate) as [end]
from g
group by EmployeeID, group_number
)
select employeeid, [start], [end]
from leave_intervals
where #today between [start] and [end]
By way of explanation, we first put a date value into a variable. I chose today, but this code will work for any date passed in. Next, we create a common table expression (CTE) that will add on a grouping column to your table. This is the meat of the solution, so it bears some treatment. Within a given interval, the AbsenceDate increases at a rate of one day per row. row_number() also increases at a rate of one per row. So, if we subtract a row_number() number of days from the AbsenceDate, we'll get another (arbitrary) date. The key here is to realize that that arbitrary date will be the same for every row in the interval, so we can use it to group by. From there, it's just a matter of doing just that; get the min and max per interval. Lastly, we find what intervals contain #today.

Data appear at least once for every month in the last X month

My problem:
Table: trans_detail:
PhoneNo | Datetime
01234 | 2013-01-05 20:40:10
01245 | 2013-04-02 21:00:13
05678 | 2013-04-16 01:24:07
04567 | 2013-07-23 07:00:00
etc | etc
I want to get all phoneNo that appears at least once for every month in the last X month (X month can be any month between 1-12).
For example: get all phone no. that appears at least once for Every Month in the last 3 months.
I am using SQL Server 2005.
Here is a quick query that comes close to what you want:
select PhoneNo
from trans_detail d
where d.datetime >= dateadd(mm, -#X, getdate())
group by PhoneNo
having count(distinct year(datetime)*12+month(datetime)) = #X
The where clause filters the data to only include rows in the last #X months. the having clause checks that each month is in the data, by counting the number of distinct months.
The above version of the query assumes that you mean calendar months. So, it has boundary condition problems. If you run it on June 16th, then it looks back one month and makes sure that the phone number appears at least once since May 16th. I am unclear on whether you want to insist that the number appear twice (once in May and once in June) or if once (once during the time period). The solution to this is to move the current date back to the end of the previous month:
select PhoneNo
from trans_detail d cross join
(select cast(getdate() - day(getdate) + 1 as date) as FirstOfMonth const
where d.datetime >= dateadd(mm, -#X, FirstOfMonth) and
d.datetime < FirstOfMonth
group by PhoneNo
having count(distinct year(datetime)*12+month(datetime)) = #X
Here it is. First two CTEs are to find and prepare last X months, third CTE is to group your data by phones and months. At the end just join the two and return where number of matching rows are equal to number of months.
DECLARE #months INT
SET #Months = 3
;WITH CTE_Dates AS
(
SELECT GETDATE() AS Dt
UNION ALL
SELECT DATEADD(MM,-1,Dt) FROM CTE_Dates
WHERE DATEDIFF(MM, Dt,GETDATE()) < #months-1
)
, CTE_Months AS
(
SELECT MONTH(Dt) AS Mn, YEAR(Dt) AS Yr FROM CTE_Dates
)
, CTE_Trans AS
(
SELECT PhoneNo, MONTH([Datetime]) AS Mn, YEAR([Datetime]) AS Yr FROM dbo.trans_detail
GROUP BY PhoneNo, MONTH([Datetime]), YEAR([Datetime])
)
SELECT PhoneNo FROM CTE_Months m
LEFT JOIN CTE_Trans t ON m.Mn = t.Mn AND m.Yr = t.Yr
GROUP BY PhoneNo
HAVING COUNT(*) = #months
SQLFiddle Demo - with added some more data that will match for last 3 months