Related
In SQL Server 2017 I have a table that looks like this https://i.stack.imgur.com/Ry106.png and I would like to get the amount of members at the end of each month, filling out the blank months with the data from the previous month.
So having this table
Create table #tempCenters (
OperationId int identity (1,1) primary key,
CenterId int,
members int,
Change_date date,
Address varchar(100), --non relevant
Sales float --non relevant
)
with this data
INSERT INTO #tempCenters VALUES
(1, 100, '2020-02-20', 'non relevant column', 135135),
(1, 110, '2020-04-15', 'non relevant column', 231635),
(1, 130, '2020-04-25', 'non relevant column', 3565432),
(1, 180, '2020-09-01', 'non relevant column', 231651),
(2, 200, '2020-01-20', 'non relevant column', 321365),
(2, 106, '2020-03-20', 'non relevant column', 34534),
(2, 135, '2020-06-25', 'non relevant column', 3224),
(2, 154, '2020-06-20', 'non relevant column', 2453453)
I am expecting this result
CenterId, Members, EOM_Date
1, 100, '2020-2-28'
1, 100, '2020-3-30'
1, 130, '2020-4-31'
1, 130, '2020-5-30'
1, 130, '2020-6-31'
1, 130, '2020-7-31'
1, 130, '2020-8-30'
1, 180, '2020-9-31'
2, 200, '2020-1-31'
2, 200, '2020-2-28'
2, 106, '2020-3-31'
2, 106, '2020-4-30'
2, 106, '2020-5-31'
2, 135, '2020-6-30'
And this is what I´ve got so far
SELECT
t.centerId,
EOMONTH(t.Change_date) as endOfMonthDate,
t.members
FROM #tempCenters t
RIGHT JOIN (
SELECT
S.CenterId,
Year(S.Change_date) as dateYear,
Month(S.Change_date) as dateMonth,
Max(s.OperationId) as id
FROM #tempCenters S
GROUP BY CenterId, Year(Change_date), Month(Change_date)
) A
ON A.id = t.OperationId
which returns the values per month, but not fill the blank ones.
First I get start date (min date) and finish date (max date) for each CenterId. Then I generate all end of months from start date to finish date for each CenterId. Finally I join my subuqery (cte) with your table (on cte.CenterId = tc.CenterId AND cte.EOM_Date >= tc.Change_date) and get last (previous or same date) members value for each date (end of month).
WITH cte AS (SELECT CenterId, EOMONTH(MIN(Change_date)) AS EOM_Date, EOMONTH(MAX(Change_date)) AS finish
FROM #tempCenters
GROUP BY CenterId
UNION ALL
SELECT CenterId, EOMONTH(DATEADD(MONTH, 1, EOM_Date)), finish
FROM cte
WHERE EOM_Date < finish)
SELECT DISTINCT cte.CenterId,
FIRST_VALUE(Members) OVER(PARTITION BY cte.CenterId, cte.EOM_Date ORDER BY tc.Change_date DESC) AS Members,
cte.EOM_Date
FROM cte
LEFT JOIN #tempCenters tc ON cte.CenterId = tc.CenterId AND cte.EOM_Date >= tc.Change_date
ORDER BY CenterId, EOM_Date;
I know it looks cumbersome and I'm sure there is a more elegant solution, but still you can use a combination of subqueries with union all and outer apply to get the desired result.
Select t.CenterId, Coalesce(t.members, tt.members), t.Change_date
From (
Select CenterId, Max(members) As members, Change_date
From
(Select t.CenterId, t.members, EOMONTH(t.Change_date) As Change_date
From #tempCenters As t Inner Join
(Select CenterId, Max(Change_date) As Change_date
From #tempCenters
Group by CenterId, Year(Change_date), Month(Change_date)
) As tt On (t.CenterId=tt.CenterId And
t.Change_date=tt.Change_date)
Union All
Select t.CenterId, Null As member, t.Change_date
From (
Select tt.CenterId, EOMONTH(datefromparts(tt.[YEAR], t.[MONTH], '1')) As Change_date,
Min_Change_date, Max_Change_date
From (Select [value] as [Month] From OPENJSON('[1,2,3,4,5,6,7,8,9,10,11,12]')) As t,
(Select CenterId, Year(Change_date) As [YEAR],
Min(Change_date) As Min_Change_date, Max(Change_date) As Max_Change_date
From #tempCenters Group by CenterId, Year(Change_date)) As tt) As t
Where Change_date Between Min_Change_date And Max_Change_date) As t
Group by CenterId, Change_date) As t Outer Apply
(Select members
From #tempCenters
Where CenterId=t.CenterId And
Change_date = (Select Max(Change_date)
From #tempCenters Where CenterId=t.CenterId And Change_date<t.Change_date Group by CenterId)) As tt
Order by t.CenterId, t.Change_date
I want to see user statics, so I made query:
SELECT l.partner AS Partner ,
bu.meno||' '||decode(substr(bu.priezvisko, 1, 2), 'Sz',
substr(bu.priezvisko, 1, 2), 'Gy',
substr(bu.priezvisko, 1, 2), 'Ny',
substr(bu.priezvisko, 1, 2), 'Zs',
substr(bu.priezvisko, 1, 2), 'Cs',
substr(bu.priezvisko, 1, 2),
substr(bu.priezvisko, 1, 1))
||'.' AS prod_man --hungarian names have 2letter (surname)
, SUM(CASE
WHEN o.pocet!=0 THEN 1
ELSE 0
END) AS obj_pocet -- counting items
, SUM(CASE
WHEN o.pocet=0 OR o.p_del+o.p_del_dod>=o.pocet THEN 1
ELSE 0
END) AS nedod_pocet -- counting items2
, ROUND(SUM(CASE
WHEN o.pocet=0 OR o.p_del+o.p_del_dod>=o.pocet THEN 1
ELSE 0
END)/count(*), 3) * 100 AS "%" --percentage
FROM obj_odb_o o
JOIN obj_odb_l l ON o.rid_o=l.rid
JOIN sklad_karta sk ON sk.id=o.kod_id
JOIN bartex_users bu ON bu.id=sk.id.prod_man
WHERE l.partner in (325,
326)
GROUP BY l.partner
, bu.meno||' '||decode(substr(bu.priezvisko, 1, 2), 'Sz',
substr(bu.priezvisko, 1, 2), 'Gy',
substr(bu.priezvisko, 1, 2), 'Ny',
substr(bu.priezvisko, 1, 2), 'Zs',
substr(bu.priezvisko, 1, 2), 'Cs',
substr(bu.priezvisko, 1, 2),
substr(bu.priezvisko, 1, 1))
||'.'
It's working. Here is the result:
But I want to make a pivot by Months (last 6 months)...
WITH MONTHS AS
(
SELECT ADD_MONTHS(TRUNC(SYSDATE,'MONTH'),-LEVEL+1) AS MONTH,
DECODE(LEVEL,1,'Akt_mesiac','minuly_mesiac'||(LEVEL-1)) AS MONTH_NAME FROM DUAL CONNECT BY LEVEL <=7)
SELECT
partner,
prod_man,
'%',
NVL(Akt_mesiac,0) AS Akt_mesiac,
NVL(minuly_mesiac1,0) AS minuly_mesiac1,
NVL(minuly_mesiac2,0) AS minuly_mesiac2,
NVL(minuly_mesiac3,0) AS minuly_mesiac3,
NVL(minuly_mesiac4,0) AS minuly_mesiac4,
NVL(minuly_mesiac5,0) AS minuly_mesiac5,
NVL(minuly_mesiac6,0) AS minuly_mesiac6
FROM (
SELECT
-- my query - HERE I HAVE PROBLEM HERE
FROM MONTHS M
JOIN obj_odb_l l ON M.MONTH=TRUNC(l.datum_p,'MONTH')
) PIVOT
( SUM(CNT)
FOR MONTH_NAME IN
('Akt_mesiac' AS Akt_mesiac,
'minuly_mesiac1' AS minuly_mesiac1,
'minuly_mesiac2' AS minuly_mesiac2,
'minuly_mesiac3' AS minuly_mesiac3,
'minuly_mesiac4' AS minuly_mesiac4,
'minuly_mesiac5' AS minuly_mesiac5,
'minuly_mesiac6' AS minuly_mesiac6)
);
Table: obj_odb_l l ->date column -> l.datum_p -> trunc(l.datum_p,'MONTH')
How can I make a pivot table ?
Consider adding the month expression, TRUNC(l.datum_p,'MONTH'), into above aggregate query. Then run the query as another CTE in pivot query for JOIN in pivot's data source.
WITH MONTHS AS (
SELECT ADD_MONTHS(TRUNC(SYSDATE,'MONTH'),-LEVEL+1) AS MONTH
, DECODE(LEVEL,1,'Akt_mesiac','minuly_mesiac'||(LEVEL-1)) AS MONTH_NAME
FROM DUAL CONNECT BY LEVEL <=7
)
, AGG AS (
-- SAME AGGREGATE QUERY WITH TRUNC(l.datum_p,'MONTH') ADDED TO SELECT AND GROUP BY
-- POSSIBLY ADD WHERE CONDITION FOR LAST SIX MONTHS (IF DATA GOES BACK YEARS)
)
SELECT *
FROM (
SELECT AGG.partner
, AGG.prod_man
, AGG.obj_pocet
, AGG.nedod_pocet
, AGG.'%' AS PCT -- AVOID SPECIAL CHARS AS NAME
, M.MONTH_NAME
FROM MONTHS M
INNER JOIN AGG
ON M.MONTH = AGG.MONTH -- NEW FIELD USED FOR JOIN
)
PIVOT
( SUM(PCT) -- ONLY PIVOTS ONE NUM AT A TIME
FOR MONTH_NAME IN
('Akt_mesiac' AS Akt_mesiac,
'minuly_mesiac1' AS minuly_mesiac1,
'minuly_mesiac2' AS minuly_mesiac2,
'minuly_mesiac3' AS minuly_mesiac3,
'minuly_mesiac4' AS minuly_mesiac4,
'minuly_mesiac5' AS minuly_mesiac5,
'minuly_mesiac6' AS minuly_mesiac6)
);
Objective:
I would like to know everyone who signed up on Day 0, how many of them logged in after D1, after D7 and after D30. I would like to make a table valued function where the users inserts a date and gets results for D0, D1, D7, D30.
Results should look like this:
Date TotalD0 TotalD1 TotalD7 TotalD30
2019-04-01 3 3 2 1
Situation:
I have one login table with email and login_time. I left joined three times on the same table and it worked on a dummy table. However, when using real data with millions of rows, it ran forever. There has to be a more efficient way of doing this.
What i tried:
CREATE FUNCTION fnTestData
(
#StartDate AS Date
)
RETURNS TABLE
AS
RETURN
select #startdate,
COUNT(distinct t1.id) As TotalD0,
COUNT(distinct t1a.id) As TotalD1,
COUNT(distinct t1b.id) As TotalD7,
COUNT(distinct t1c.id) As TotalD30
from #test1 t1
left join #test1 t1a on t1.id=t1a.id and t1a.login_time >=
DATEADD(day,1,t1.login_time)
left join #test1 t1b on t1.id=t1b.id and t1b.login_time >=
DATEADD(day,7,t1.login_time)
left join #test1 t1c on t1.id=t1c.id and t1c.login_time >=
DATEADD(day,30,t1.login_time)
where t1.login_time = #startdate
group by t1.login_time
Test data:
create table #test1 (id int, login_time date)
insert into #test1 values
(1, '2019-04-01'),
(1, '2019-04-01'),
(1, '2019-04-02'),
(1, '2019-04-19'),
(1, '2019-05-05'),
(2, '2019-04-01'),
(2, '2019-04-05'),
(2, '2019-04-10'),
(2, '2019-04-15'),
(3, '2019-04-01'),
(3, '2019-04-01'),
(3, '2019-04-02')
Your query could be translated to a GROUP BY:
DECLARE #StartDate Date = '2019-04-01'
SELECT COUNT(DISTINCT id) D0
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 1, #StartDate) THEN id END) AS D1
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 7, #StartDate) THEN id END) AS D7
, COUNT(DISTINCT CASE WHEN login_time >= DATEADD(DAY, 30, #StartDate) THEN id END) AS D30
FROM #test1 AS t
WHERE login_time >= #StartDate
AND EXISTS (
SELECT 1
FROM #test1 AS x
WHERE x.id = t.id
AND x.login_time = #StartDate
)
D0 D1 D7 D30
3 3 2 1
You need to create appropriate indexes to speed it up.
If you want to do cohort analysis based on the day people start:
select first_ld,
count(*) as num_d0,
sum(case when login_date >= dateadd(day, 1, firstld) then 1 else 0 end) as num_d1,
sum(case when login_date >= dateadd(day, 7, firstld) then 1 else 0 end) as num_d7,
sum(case when login_date >= dateadd(day, 30, firstld) then 1 else 0 end) as num_d30
from (select id, convert(date, login_time) as login_date,
min(convert(date, login_time)) over (partition by id) as first_ld
from #test1 t
group by id, convert(date, login_time)
) t
group by first_ld
order by first_ld;
I have a table in SQL Server with the following format
MType (Integer), MDate (Datetime), Status (SmallInt)
1, 10-05-2018, 1
1, 15-05-2018, 1
2, 25-3-2018, 0
3, 12-01-2018, 1
....
I want to get the MIN MDate for specific MTypes for future dates. In case there isn't one, then the MType should be returned but with NULL value.
Here is what I have done until now:
SELECT m.MType,
MIN(m.MDate)
FROM MyTypes m
WHERE m.MType IN ( 1, 2, 3, 4)
AND m.MDate > GETDATE()
AND m.Status = 1
GROUP BY m.MType
Obviously, the above will return only the following:
1, 10-05-2018
Since there are any other rows with future date and status equals to 1.
However, the results I want are:
1, 10-05-2018
2, NULL
3, NULL
4, NULL //this is missing in general from the table. No MType with value 4
The table is big, so performance is something to take into account. Any ideas how to proceed?
One way is to join the table to itself and filter the date in the ON clause.
SELECT a.Mtype, MIN(b.MDate)
FROM MyTypes a
LEFT JOIN MyTypes b
ON a.MType = b.MType
AND b.MDate > GETDATE()
AND b.Status = 1
WHERE a.MType IN ( 1, 2, 3)
GROUP BY a.MType
Here's a Demo.
I don't know what is logic behind but it seems to use of look-up tables
SELECT a.MType, l.MDate
FROM
(
values (1),(2),(3),(4)
)a (MType)
LEFT JOIN (
SELECT m.MType,
MIN(m.MDate) MDate
FROM MyTypes m
WHERE m.MDate > GETDATE()
AND m.Status = 1
GROUP BY m.MType
)l on l.MType = a.MType
Use a windows function and a union to a numbers table:
declare #t table (MType int, MDate datetime, [Status] smallint)
Insert into #t values (1, convert(date, '10-05-2018', 103), 1)
,(1, convert(date, '15-05-2018', 103), 1)
,(2, convert(date, '25-03-2018', 103), 0)
,(3, convert(date, '12-01-2018', 103), 1)
Select DISTINCT Mtype
, min(iiF(MDate>getdate() and status = 1, MDate, NUll)) over (Partition By Mtype) as MDate
from ( SELECT TOP 10000 row_number() over(order by t1.number) as MType
, '1900-01-01' as MDate, 0 as [Status]
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
union
Select Mtype, MDate, [Status] from #t
) x
where MType in (1,2,3,4)
order by x.MType
At work, one of my assignments is to calculate commission to the sales staff. One rule has been more challenging than the others.
Two sales teams A and B work together each selling different products. Team A can send leads to team B. The same customer might be send multiple times. The first time a customer (ex. lead 1)* is send a commission is paid to the salesperson in team A who created the lead. Now the customer is “locked” for the next 365 days (counting from the date lead 1 was created). Meaning that no one can get additional commission for that customer in that period by sending additional leads (ex. Lead 2 and 3 gets no commission). After the 365 days have expired. A new lead can be created and receive commission (ex. Lead 4). Then the customer is locked again for 365 days counting from the day lead 4 was created. Therefore, lead 5 gets no commission. The tricky part is to reset the date that the 365 days is counted from.
'* Reference to tables #LEADS and #DISERED result.
I have solved the problem in tSQL using a cursor, but I wonder if it was possible to use a recursive CTE instead. I have made several attempts the best one is pasted in below. The problem with my solution is, that I refer to the recursive table more than once. I have tried to fix that problem with nesting a CTE inside a CTE. That’s is not allowed. I have tried using a temporary table inside the CTE that is not allowed either. I tried several times to recode the recursive part of the CTE so that the recursive table is referenced only once, but then I am not able to get the logic to work.
I am using SQL 2008
IF OBJECT_ID('tempdb.dbo.#LEADS', 'U') IS NOT NULL
DROP TABLE #LEADS;
CREATE TABLE #LEADS (LEAD_ID INT, CUSTOMER_ID INT, LEAD_CREATED_DATE DATETIME, SALESPERSON_NAME varchar(20))
INSERT INTO #LEADS
VALUES (1, 1, '2013-09-01', 'Rasmus')
,(2, 1, '2013-11-01', 'Christian')
,(3, 1, '2014-01-01', 'Nadja')
,(4, 1, '2014-12-24', 'Roar')
,(5, 1, '2015-12-01', 'Kristian')
,(6, 2, '2014-01-05', 'Knud')
,(7, 2, '2015-01-02', 'Rasmus')
,(8, 2, '2015-01-08', 'Roar')
,(9, 2, '2016-02-05', 'Kristian')
,(10, 2, '2016-03-05', 'Casper')
SELECT *
FROM #LEADS;
IF OBJECT_ID('tempdb.dbo.#DISERED_RESULT', 'U') IS NOT NULL
DROP TABLE #DISERED_RESULT;
CREATE TABLE #DISERED_RESULT (LEAD_ID INT, DESIRED_COMMISION_RESULT CHAR(3))
INSERT INTO #DISERED_RESULT
VALUES (1, 'YES')
,(2, 'NO')
,(3, 'NO')
,(4, 'YES')
,(5, 'NO')
,(6, 'YES')
,(7, 'NO')
,(8, 'YES')
,(9, 'YES')
,(10, 'NO')
SELECT *
FROM #DISERED_RESULT;
WITH COMMISSION_CALCULATION AS
(
SELECT T1.*
,COMMISSION = 'YES'
,MIN_LEAD_CREATED_DATE AS COMMISSION_DATE
FROM #LEADS AS T1
INNER JOIN (
SELECT A.CUSTOMER_ID
,MIN(A.LEAD_CREATED_DATE) AS MIN_LEAD_CREATED_DATE
FROM #LEADS AS A
GROUP BY A.CUSTOMER_ID
) AS T2 ON T1.CUSTOMER_ID = T2.CUSTOMER_ID AND T1.LEAD_CREATED_DATE = T2.MIN_LEAD_CREATED_DATE
UNION ALL
SELECT T10.LEAD_ID
,T10.CUSTOMER_ID
,T10.LEAD_CREATED_DATE
,T10.SALESPERSON_NAME
,T10.COMMISSION
,T10.COMMISSION_DATE
FROM (SELECT ROW_NUMBER() OVER(PARTITION BY T5.CUSTOMER_ID ORDER BY T5.LEAD_CREATED_DATE ASC) AS RN
,T5.*
,T6.MAX_COMMISSION_DATE
,DATEDIFF(DAY, T6.MAX_COMMISSION_DATE, T5.LEAD_CREATED_DATE) AS ANTAL_DAGE_SIDEN_SIDSTE_COMMISSION
,CASE
WHEN DATEDIFF(DAY, T6.MAX_COMMISSION_DATE, T5.LEAD_CREATED_DATE) > 365 THEN 'YES'
ELSE 'NO'
END AS COMMISSION
,CASE
WHEN DATEDIFF(DAY, T6.MAX_COMMISSION_DATE, T5.LEAD_CREATED_DATE) > 365 THEN T5.LEAD_CREATED_DATE
ELSE NULL
END AS COMMISSION_DATE
FROM #LEADS AS T5
INNER JOIN (SELECT T4.CUSTOMER_ID
,MAX(T4.COMMISSION_DATE) AS MAX_COMMISSION_DATE
FROM COMMISSION_CALCULATION AS T4
GROUP BY T4.CUSTOMER_ID) AS T6 ON T5.CUSTOMER_ID = T6.CUSTOMER_ID
WHERE T5.LEAD_ID NOT IN (SELECT LEAD_ID FROM COMMISSION_CALCULATION)
) AS T10
WHERE RN = 1
)
SELECT *
FROM COMMISSION_CALCULATION;
I have made some assumptions where your description does not fully make sense as written, but the below achieves your desired result:
if object_id('tempdb.dbo.#leads', 'u') is not null
drop table #leads;
create table #leads (lead_id int, customer_id int, lead_created_date datetime, salesperson_name varchar(20))
insert into #leads
values (1, 1, '2013-09-01', 'rasmus')
,(2, 1, '2013-11-01', 'christian')
,(3, 1, '2014-01-01', 'nadja')
,(4, 1, '2014-12-24', 'roar')
,(5, 1, '2015-12-01', 'kristian')
,(6, 2, '2014-01-05', 'knud')
,(7, 2, '2015-01-02', 'rasmus')
,(8, 2, '2015-01-08', 'roar')
,(9, 2, '2016-02-05', 'kristian')
,(10, 2, '2016-03-05', 'casper')
if object_id('tempdb.dbo.#disered_result', 'u') is not null
drop table #disered_result;
create table #disered_result (lead_id int, desired_commision_result char(3))
insert into #disered_result
values (1, 'yes'),(2, 'no'),(3, 'no'),(4, 'yes'),(5, 'no'),(6, 'yes'),(7, 'no'),(8, 'yes'),(9, 'yes'),(10, 'no')
with rownum
as
(
select row_number() over (order by customer_id, lead_created_date) as rn -- This is to ensure an incremantal ordering id
,lead_id
,customer_id
,lead_created_date
,salesperson_name
from #leads
)
,cte
as
(
select rn
,lead_id
,customer_id
,lead_created_date
,salesperson_name
,'yes' as commission_result
,lead_created_date as commission_window_start
from rownum
where rn = 1
union all
select r.rn
,r.lead_id
,r.customer_id
,r.lead_created_date
,r.salesperson_name
,case when r.customer_id <> c.customer_id -- If the customer ID has changed, we are at a new commission window.
then 'yes'
else case when r.lead_created_date > dateadd(d,365,c.commission_window_start) -- This assumes the window is 365 days and not one year (ie. Leap years don't add a day)
then 'yes'
else 'no'
end
end as commission_result
,case when r.customer_id <> c.customer_id
then r.lead_created_date
else case when r.lead_created_date > dateadd(d,365,c.commission_window_start) -- This assumes the window is 365 days and not one year (ie. Leap years don't add a day)
then r.lead_created_date
else c.commission_window_start
end
end as commission_window_start
from rownum r
inner join cte c
on(r.rn = c.rn+1)
)
select lead_id
,commission_result
from cte
order by customer_id
,lead_created_date;