Table with dates, table with week numbers, join together? - sql

I have two tables. Table 1:
StuAp_Id StuAp_StaffID StuAp_Date StuAp_Attended
16 77000002659366 2011-09-07 Yes
17 77000002659366 2011-09-14 Yes
18 77000002659366 2011-09-14 Yes
19 77000002659366 2011-09-14 No
20 77000001171783 2011-09-19 Yes
Table 2:
Year Week Start
2011 1 2011-09-05 00:00:00.000
2011 2 2011-09-12 00:00:00.000
2011 3 2011-09-19 00:00:00.000
2011 4 2011-09-26 00:00:00.000
2011 5 2011-10-03 00:00:00.000
2011 6 2011-10-10 00:00:00.000
2011 7 2011-10-17 00:00:00.000
2011 8 2011-10-24 00:00:00.000
2011 9 2011-10-31 00:00:00.000
How would I join these two tables to make something like this:
StuAp_Id StuAp_StaffID StuAp_Date StuAp_Attended Week
16 77000002659366 2011-09-07 Yes 1
17 77000002659366 2011-09-14 Yes 2
18 77000002659366 2011-09-14 Yes 2
19 77000002659366 2011-09-14 No 2
20 77000001171783 2011-09-19 Yes 3
Thanks in advance

You can write simple INNER JOIN using GROUP BY clause.
SELECT Table1.*
,MAX(WEEK) AS WEEK
FROM Table1
INNER JOIN Table2 ON STUAP_DATE >= START
GROUP BY STUAP_ID,STUAP_STAFFID,STUAP_DATE,STUAP_ATTENDED

don't know about specifics on sql2k5 (don't have one around to test) but I would use a sub select eg.
select table_1.*,
[week] = (select isnull(max([week]), 0)
from table_2
where table_1.StuAp_Date >= table_2.start)
from table_1

CTEs to the rescue!
create table StuAp (
StuAp_Id int,
StuAp_StaffID bigint,
StuAp_Date datetime,
StuAp_Attended varchar(3)
)
create table Weeks (
Year int,
Week int,
Start datetime
)
insert into StuAp
values (16, 77000002659366, {d '2011-09-07'}, 'Yes'),
(17, 77000002659366, {d '2011-09-14'}, 'Yes'),
(18, 77000002659366, {d '2011-09-14'}, 'Yes'),
(19, 77000002659366, {d '2011-09-14'}, 'No'),
(20, 77000001171783, {d '2011-09-19'}, 'Yes')
insert into Weeks
values (2011, 1, {d '2011-09-05'}),
(2011, 2, {d '2011-09-12'}),
(2011, 3, {d '2011-09-19'}),
(2011, 4, {d '2011-09-26'}),
(2011, 5, {d '2011-10-03'}),
(2011, 6, {d '2011-10-10'}),
(2011, 7, {d '2011-10-17'}),
(2011, 8, {d '2011-10-24'}),
(2011, 9, {d '2011-10-31'})
;with OrderedWeeks as (
select ROW_NUMBER() OVER (ORDER BY year, week) as row, w.*
from Weeks w
), Ranges as (
select w1.*, w2.Start as Finish
from OrderedWeeks w1 inner join
OrderedWeeks w2 on w1.row = w2.row - 1
)
select s.StuAp_Id, s.StuAp_StaffID, s.StuAp_Date, s.StuAp_Attended, r.Week
from StuAp s inner join
Ranges r on s.StuAp_Date >= r.Start and s.StuAp_Date < r.Finish
This should scale quite well too.
Honestly though, if you find yourself doing queries like this often, you should really consider changing the stucture of your Weeks table to include a finish date. You could even make it an indexed view, or (assuming that the data changes rarely), you could keep your original table and use triggers or a SQL Agent job to keep a copy that contains Finish up to date.

SET ANSI_WARNINGS ON;
GO
DECLARE #Table1 TABLE
(
StuAp_Id INT PRIMARY KEY
,StuAp_StaffID NUMERIC(14,0) NOT NULL
,StuAp_Date DATETIME NOT NULL
,StuAp_Attended VARCHAR(3) NOT NULL
,StuAp_DateOnly AS DATEADD(DAY, DATEDIFF(DAY,0,StuAp_Date), 0) PERSISTED
);
INSERT #Table1
SELECT 16,77000002659366 ,'2011-09-07','Yes'
UNION ALL
SELECT 17,77000002659366 ,'2011-09-14','Yes'
UNION ALL
SELECT 18,77000002659366 ,'2011-09-14','Yes'
UNION ALL
SELECT 19,77000002659366 ,'2011-09-14','No'
UNION ALL
SELECT 20,77000001171783 ,'2011-09-19','Yes';
DECLARE #Table2 TABLE
(
Year INT NOT NULL
,Week INT NOT NULL
,Start DATETIME NOT NULL
,[End] AS DATEADD(DAY,6,Start) PERSISTED
,PRIMARY KEY(Year, Week)
,UNIQUE(Start)
);
INSERT #Table2
SELECT 2011,1 ,'2011-09-05 00:00:00.000'
UNION ALL
SELECT 2011,2 ,'2011-09-12 00:00:00.000'
UNION ALL
SELECT 2011,3 ,'2011-09-19 00:00:00.000'
UNION ALL
SELECT 2011,4 ,'2011-09-26 00:00:00.000'
UNION ALL
SELECT 2011,5 ,'2011-10-03 00:00:00.000'
UNION ALL
SELECT 2011,6 ,'2011-10-10 00:00:00.000'
UNION ALL
SELECT 2011,7 ,'2011-10-17 00:00:00.000'
UNION ALL
SELECT 2011,8 ,'2011-10-24 00:00:00.000'
UNION ALL
SELECT 2011,9 ,'2011-10-31 00:00:00.000';
--Solution 1 : if StuAp_Date has only date part
SELECT a.*, b.Week
FROM #Table1 a
INNER JOIN #Table2 b ON a.StuAp_Date BETWEEN b.Start AND b.[End]
--Solution 2 : if StuAp_Date has only date part
SELECT a.*, b.Week
FROM #Table1 a
INNER JOIN #Table2 b ON a.StuAp_Date BETWEEN b.Start AND DATEADD(DAY,6,b.Start)
--Solution 3 : if StuAp_Date has date & time
SELECT a.*, b.Week
FROM #Table1 a
INNER JOIN #Table2 b ON a.StuAp_DateOnly BETWEEN b.Start AND b.[End]
--Solution 4 : if StuAp_Date has date & time
SELECT a.*, b.Week
FROM #Table1 a
INNER JOIN #Table2 b ON DATEADD(DAY, DATEDIFF(DAY,0,a.StuAp_Date), 0) BETWEEN b.Start AND DATEADD(DAY,6,b.Start)

Related

Display Average Billing Amount For Each Customer only between years 2019-2021

QUESTION : Display Average Billing Amount For Each Customer ONLY between YEAR(2019-2021).
If customer doesn't have any billing amount for any of the particular year then consider as 0.
-------: OUTPUT :
Customer_ID | Customer_Name | AVG_Billed_Amount
-------------------------------------------------------------------------
1 | A | 87.00
2 | B | 200.00
3 | C | 183.00
--------: EXPLANATION :
If any customer doesn't have any billing records for these 3 years then we need to consider as one record with billing_amount = 0
Like Customer C doesn't have any record for Year 2020, so for C Average will be
(250+300+0)/3 = 183.33 OR 183.00
TEMP TABLE HAS FOLLOWING DATA
DROP TABLE IF EXISTS #TEMP;
CREATE TABLE #TEMP
(
Customer_ID INT
, Customer_Name NVARCHAR(100)
, Billing_ID NVARCHAR(100)
, Billing_creation_Date DATETIME
, Billed_Amount INT
);
INSERT INTO #TEMP
SELECT 1, 'A', 'ID1', TRY_CAST('10-10-2020' AS DATETIME), 100 UNION ALL
SELECT 1, 'A', 'ID2', TRY_CAST('11-11-2020' AS DATETIME), 150 UNION ALL
SELECT 1, 'A', 'ID3', TRY_CAST('12-11-2021' AS DATETIME), 100 UNION ALL
SELECT 2, 'B', 'ID4', TRY_CAST('10-11-2019' AS DATETIME), 150 UNION ALL
SELECT 2, 'B', 'ID5', TRY_CAST('11-11-2020' AS DATETIME), 200 UNION ALL
SELECT 2, 'B', 'ID6', TRY_CAST('12-11-2021' AS DATETIME), 250 UNION ALL
SELECT 3, 'C', 'ID7', TRY_CAST('01-01-2018' AS DATETIME), 100 UNION ALL
SELECT 3, 'C', 'ID8', TRY_CAST('05-01-2019' AS DATETIME), 250 UNION ALL
SELECT 3, 'C', 'ID9', TRY_CAST('06-01-2021' AS DATETIME), 300
-----------------------------------------------------------------------------------
Here, 'A' has 3 transactions - TWICE in year 2020(100+150) and 1 in year 2021(100), but none in 2019(SO, Billed_Amount= 0).
so the average will be calculated as (100+150+100+0)/4
DECLARE #BILL_dATE DATE = (SELECT Billing_creation_date from #temp group by customer_id, Billing_creation_date) /*-- THIS THROWS ERROR AS #BILL_DATE WON'T ACCEPT MULTIPLE VALUES.*/
OUTPUT should look like this:
Customer_ID
Customer_Name
AVG_Billed_Amount
1
A
87.00
2
B
200.00
3
C
183.00
You just need a formula to count the number of missing years.
That's 3 - COUNT(DISTINCT YEAR(Billing_creation_Date)
Then the average = SUM() / (COUNT() + (3 - COUNT(DISTINCT YEAR)))...
SELECT
Customer_ID,
Customer_Name,
SUM(Billed_Amount) * 1.0
/
(COUNT(*) + 3 - COUNT(DISTINCT YEAR(Billing_creation_Date)))
AS AVG_Billed_amount
FROM
#temp
WHERE
Billing_creation_Date >= '2019-01-01'
AND Billing_creation_Date < '2022-01-01'
GROUP BY
Customer_ID,
Customer_Name
Demo : https://dbfiddle.uk/ILcfiGWL
Note: The WHERE clause in another answer here would cause a scan of the table, due to hiding the filtered column behind a function. The way I've formed the WHERE clause allows a "Range Seek" if the column is in an index.
Here is a query that can do that :
select s.Customer_ID, s.Customer_Name, sum(Billed_amount)/ ( 6 - count(1)) as AVG_Billed_Amount from (
select Customer_ID, Customer_Name, sum(Billed_Amount) as Billed_amount
from TEMP
where year(Billing_creation_Date) between 2019 and 2021
group by Customer_ID, year(Billing_creation_Date)
) as s
group by Customer_ID;
According to your description the customer_name C will be 137.5000 not 183.00 since 2018 is not counted and 2020 is not there.

Fill up date gap by month

I have table of products and their sales quantity in months.
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-05-01 5
B 2018-08-01 10
B 2018-10-01 12
...
I'd like to first fill in the data gap between each product's min and max dates like below:
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-03-01 0
A 2018-04-01 0
A 2018-05-01 5
B 2018-08-01 10
B 2018-09-01 0
B 2018-10-01 12
...
Then I would need to perform an accumulation of each product's sales quantity by month.
Product Month total_Qty
A 2018-01-01 5
A 2018-02-01 8
A 2018-03-01 8
A 2018-04-01 8
A 2018-05-01 13
B 2018-08-01 10
B 2018-09-01 10
B 2018-10-01 22
...
I fumbled over the "cross join" clause, however it seems to generate some unexpected results for me. Could someone help to give a hint how I can achieve this in SQL?
Thanks a lot in advance.
I think a recursive CTE is a simple way to do this. The code is just:
with cte as (
select product, min(mon) as mon, max(mon) as end_mon
from t
group by product
union all
select product, dateadd(month, 1, mon), end_mon
from cte
where mon < end_mon
)
select cte.product, cte.mon, coalesce(qty, 0) as qty
from cte left join
t
on t.product = cte.product and t.mon = cte.mon;
Here is a db<>fiddle.
Hi i think this example can help you and perform what you excepted :
CREATE TABLE #MyTable
(Product varchar(10),
ProductMonth DATETIME,
Qty int
);
GO
CREATE TABLE #MyTableTempDate
(
FullMonth DATETIME
);
GO
INSERT INTO #MyTable
SELECT 'A', '2019-01-01', 214
UNION
SELECT 'A', '2019-02-01', 4
UNION
SELECT 'A', '2019-03-01', 50
UNION
SELECT 'B', '2019-01-01', 214
UNION
SELECT 'B', '2019-02-01', 10
UNION
SELECT 'C', '2019-04-01', 150
INSERT INTO #MyTableTempDate
SELECT '2019-01-01'
UNION
SELECT '2019-02-01'
UNION
SELECT '2019-03-01'
UNION
SELECT '2019-04-01'
UNION
SELECT '2019-05-01'
UNION
SELECT '2019-06-01'
UNION
SELECT '2019-07-01';
------------- FOR NEWER SQL SERVER VERSION > 2005
WITH MyCTE AS
(
SELECT T.Product, T.ProductMonth AS 'MMonth', T.Qty
FROM #MyTable T
UNION
SELECT T.Product, TD.FullMonth AS 'MMonth', 0 AS 'Qty'
FROM #MyTable T, #MyTableTempDate TD
WHERE NOT EXISTS (SELECT 1 FROM #MyTable TT WHERE TT.Product = T.Product AND TD.FullMonth = TT.ProductMonth)
)
-- SELECT * FROM MyCTE;
SELECT Product, MMonth, Qty, SUM( Qty) OVER(PARTITION BY Product ORDER BY Product
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as 'TotalQty'
FROM MyCTE
ORDER BY Product, MMonth ASC;
DROP TABLE #MyTable
DROP TABLE #MyTableTempDate
I have other way to perform this in lower SQL Server Version (like 2005 and lower)
It's a SELECT on SELECT if it's your case let me know and i provide some other example.
You can create the months with a recursive CTE
DECLARE #MyTable TABLE
(
ProductID CHAR(1),
Date DATE,
Amount INT
)
INSERT INTO #MyTable
VALUES
('A','2018-01-01', 5),
('A','2018-02-01', 3),
('A','2018-05-01', 5),
('B','2018-08-01', 10),
('B','2018-10-01', 12)
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SELECT #StartDate = MIN(Date), #EndDate = MAX(Date) FROM #MyTable
;WITH dates AS (
SELECT #StartDate AS Date
UNION ALL
SELECT DATEADD(Month, 1, Date)
FROM dates
WHERE Date < #EndDate
)
SELECT A.ProductID, d.Date, COALESCE(Amount,0) AS Amount, COALESCE(SUM(Amount) OVER(PARTITION BY A.ProductID ORDER BY A.ProductID, d.Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),0) AS Total
FROM
(
SELECT ProductID, MIN(date) as DateStart, MAX(date) as DateEnd
FROM #MyTable
GROUP BY ProductID -- As I read in your comments that you need different min and max dates per product
) A
JOIN dates d ON d.Date >= A.DateStart AND d.Date <= A.DateEnd
LEFT JOIN #MyTable T ON A.ProductID = T.ProductID AND T.Date = d.Date
ORDER BY A.ProductID, d.Date
Try this below
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
;WITH CTE(Product,[Month],Qty)
AS
(
SELECT 'A','2018-01-01', 5 UNION ALL
SELECT 'A','2018-02-01', 3 UNION ALL
SELECT 'A','2018-05-01', 5 UNION ALL
SELECT 'B','2018-08-01', 10 UNION ALL
SELECT 'D','2018-10-01', 12
)
SELECT ct.Product,[MonthDays],ct.Qty
INTO #Temp
FROM
(
SELECT c.Product,[Month],
ISNULL(Qty,0) AS Qty
FROM CTE c
)ct
RIGHT JOIN
(
SELECT -- This code is to get month data
CONVERT(VARCHAR(10),'2018-'+ RIGHT('00'+CAST(MONTH(DATEADD(MM, s.number, CONVERT(DATETIME, 0)))AS VARCHAR),2) +'-01',120) AS [MonthDays]
FROM master.dbo.spt_values s
WHERE [type] = 'P' AND s.number BETWEEN 0 AND 11
)DT
ON dt.[MonthDays] = ct.[Month]
SELECT
MAX(Product)OVER(ORDER BY [MonthDays])AS Product,
[MonthDays],
ISNULL(Qty,0) Qty,
SUM(ISNULL(Qty,0))OVER(ORDER BY [MonthDays]) As SumQty
FROM #Temp
Result
Product MonthDays Qty SumQty
------------------------------
A 2018-01-01 5 5
A 2018-02-01 3 8
A 2018-03-01 0 8
A 2018-04-01 0 8
A 2018-05-01 5 13
A 2018-06-01 0 13
A 2018-07-01 0 13
B 2018-08-01 10 23
B 2018-09-01 0 23
D 2018-10-01 12 35
D 2018-11-01 0 35
D 2018-12-01 0 35
First of all, i would divide month and year to get easier with statistics.
I will give you an example query, not based on your table but still helpful.
--here i create the table that will be used as calendar
Create Table MA_MonthYears (
Month int not null ,
year int not null
PRIMARY KEY ( month, year) )
--/////////////////
-- here i'm creating a procedure to fill the ma_monthyears table
declare #month as int
declare #year as int
set #month = 1
set #year = 2015
while ( #year != 2099 )
begin
insert into MA_MonthYears(Month, year)
select #month, #year
if #month < 12
set #month=#month+1
else
set #month=1
if #month = 1
set #year = #year + 1
end
--/////////////////
--here you are the possible result you are looking for
select SUM(Ma_saledocdetail.taxableamount) as Sold, MA_MonthYears.month , MA_MonthYears.year , item
from MA_MonthYears left outer join MA_SaleDocDetail on year(MA_SaleDocDetail.DocumentDate) = MA_MonthYears.year
and Month(ma_saledocdetail.documentdate) = MA_MonthYears.Month
group by MA_SaleDocDetail.Item, MA_MonthYears.year , MA_MonthYears.month
order by MA_MonthYears.year , MA_MonthYears.month

How to get all month names and need to show month data

Pnum Fdate description
==== ========== ===========
1024 2018-02-17 A
1024 2018-05-17 B
1024 2018-05-17 C
1024 2018-09-17 D
MY table PW have fields looks like this.
--> I want to show the result as
**Month Name Description**
January -
February A
March -
April -
May B
June -
July -
August C
September D
October -
November -
December -
Please help me how to achive this.
Join with a list of month names, there is only twelve of them:
SELECT monthname, description
FROM (VALUES
(1, 'January'),
(2, 'February'),
(3, 'March'),
(4, 'April'),
(5, 'May'),
(6, 'June'),
(7, 'July'),
(8, 'August'),
(9, 'September'),
(10, 'October'),
(11, 'November'),
(12, 'December')
) AS va(monthnumber, monthname)
LEFT JOIN yourdata ON DATEPART(MONTH, fdate) = va.monthnumber
ORDER BY monthnumber
Try this
;WITH CTE(Pnum, Fdate,description)
AS
(
SELect 1024,'2018-02-17','A' union all
SELect 1024,'2018-05-17','B' union all
SELect 1024,'2018-08-17','C' union all
SELect 1024,'2018-09-17','D'
)
SELECT MonthNames,ISNULL([Description],'-') AS [Description]
FROM CTE RIGHT JOIN
(
SELECT DATENAME(MONTH,DATEADD(MONTH,number-datepart(month,GETDATE()),GETDATE())) as MonthNames
FROM MASTER.DBO.spt_values
WHERE TYPE ='P'
AND number BETWEEN 1 AND 12
) dt
ON dt.MonthNames=DATENAME(MONTH,Fdate)
Result
MonthNames Description
--------------------------
January -
February A
March -
April -
May B
June -
July -
August C
September D
October -
November -
December -
You can try below
DEMO
with cte1 as (
select cast('2018-01-01' as date) dt
union all
select dateadd(month, 1, dt)
from cte1
where dateadd(month, 1, dt) < cast('2018-12-31' as date)
)
select DateName(month,dt),coalesce(Description,'-') as Description from cte1 a left join yourtable b
on month(a.dt)=month(b.Fdate)
This solution will allow an index on Fdate to be used (MONTH(column) will force a scan every time).
DECLARE #year int = 2018;
;WITH m AS
(
SELECT m = 1 UNION ALL SELECT m + 1 FROM m WHERE m < 12
),
months(b,e) AS
(
SELECT b = DATEFROMPARTS(#year, m, 1)
FROM m
)
SELECT DATENAME(MONTH, m.b), PW.Description
FROM months AS m
LEFT OUTER JOIN dbo.PW
ON PW.Fdate >= m.b AND PW.Fdate < DATEADD(MONTH, 1, m.b)
ORDER BY m.b;
Try the following query:
SELECT MONTHNAME(fdate), description FROM table
For more reference go through
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_month

sum of the average

working with this subquery and I am doing something wrong were I am not getting the right average in the LastSixMosAvg.
JOIN
(SELECT Avg(LastSix) as LastSixMosAvg,id
FROM (SELECT t.id,Sum(t.total) as LastSix
FROM table t
WHERE t.Month Between #6mosStart and #enddate
group by t.id)t
Group by id) d
ON d.ID=a.ID
that query yields this results in a particular ID.
Month Total LastSixMosAvg
Month 1 325 1,367
Month 2 30 1,367
Month 3 330 1,367
Month 4 336 1,367
Month 5 220 1,367
Month 6 126 1,367
Instead in the LasSixMos Avg I should see $228
Your query is averaging the sum of the Total column. The sum is 1,367, and it is a constant for each ID, so averaging it correctly returns 1,367.
Try selecting the average of the Total column itself:
INNER JOIN
(
SELECT t.ID, Avg(t.Total) as LastSixMosAvg
FROM MyTable t
WHERE t.Month Between #6mosStart and #enddate
GROUP BY t.ID
) d
ON d.ID = a.ID
Example:
declare #t table
(
[ID] int,
[Month] int,
Total int,
primary key clustered([ID], [Month])
)
insert into #t values
(1, 1, 325),
(1, 2, 30),
(1, 3, 330),
(1, 4, 336),
(1, 5, 220),
(1, 6, 127)
declare #6mosStart int = 1, #enddate int = 6
SELECT t.ID, Avg(t.Total) as LastSixMosAvg
FROM #t t
WHERE t.Month Between #6mosStart and #enddate
GROUP BY t.ID
-- Results:
-- ID LastSixMosAvg
-- 1 228

Split project date range into rows of work weeks for all projects in SQL

I have a projects table with a total_hours column and a startdate, enddate column.
If a project has a date range of 5 weeks, I need a query that returns 5 rows with the incremented work week number in a calculated field for all projects.
Here is my table data with a query showing the range in work week format.
drop table #temp
CREATE TABLE #Temp
(ProjectID int, Total_Hours int, StartDate datetime, EndDate datetime)
;
INSERT INTO #Temp
(ProjectID, Total_Hours, StartDate, EndDate)
VALUES
(645, 555, '2016-01-01 00:00:00', '2016-02-01 00:00:00'),
(700, 234, '2015-01-14 00:00:00', '2016-02-01 00:00:00')
Select datepart(week,startdate),datepart(week,Enddate) from #Temp
I need a query that will return the following values
ProjectID WW
645 1
645 2
645 3
645 4
645 5
645 6
700 3
700 4
700 5
700 6
I feel I should use recursion but don't know how.
You could do it with recursion but a numbers table is generally more efficient:
with n as (
select row_number() over (order by (select null)) - 1 as n
from master.spt_values
)
select t.projectid, dateadd(week, n.n, t.startdate) as ww
from #Temp t join
n
on dateadd(week, n.n, t.startdate) <= t.enddate;
If you prefer a recursive query, use
with t as (
select projectid,datepart(week,startdate) sw,datepart(week,enddate) ew from #Temp
union all
select projectid,sw+1,ew from t where sw < ew
)
select projectid, sw
from t
order by 1,2
Sample Demo