I am still learning many new things about SQL such as PARTITION BY and CTEs. I am currently working on a query which I have cobbled together from a similar question I found online. However, I can not seem to get it to work as intended.
The problem is as follows -- I have been tasked to show rank promotions in an organization from the begining of 2022 to today. I am working with 2 primary tables, an EMPLOYEES table and a PERIODS table. This periods table captures a snapshot of any given employee each month - including their rank at the time. Each of these months is also assigned a PeriodID (e.g. Jan 2022 = PeriodID 131). Our EMPLOYEE table holds the employees current rank. These ranks are stored as an int (e.g. 1,2,3 with 1 being lowest rank). It is possible for an employee to rank up more than once in any given month.
I have simplified the used query as much as I can for the sake of this problem. Query follows as:
;WITH x AS
(
SELECT
e.EmployeeID, p.PeriodID, p.RankID,
rn = ROW_NUMBER() OVER (PARTITION BY e.EmployeeID ORDER BY p.PeriodID DESC)
FROM employees e
LEFT JOIN periods p on p.EmployeeID= e.EmployeeID
WHERE p.PeriodID <= 131 AND p.PeriodID >=118 --This is the time range mentioned above
),
rest AS (SELECT * FROM x WHERE rn > 1)
SELECT
main.EmployeeID,
PeriodID = MIN(
CASE
WHEN main.CurrentRankID = Rest.RankID
THEN rest.PeriodID ELSE main.PeriodID
END),
main.RankID, rest.RankID
FROM x AS main LEFT OUTER JOIN rest ON main.EmployeeID = rest.EmployeeID
AND rest.rn >1
LEFT JOIN periods p on p.EmployeeID = e.EmployeeID
WHERE main.rn = 1
AND NOT EXISTS
(
SELECT 1 FROM rest AS rest2
WHERE EmployeeID = rest.EmployeeID
AND rn < rest.rn
AND main.RankID <> rest.RankID
)
and p.PeriodID <= 131 AND p.PeriodID >=118
GROUP BY main.EmployeeID, main.PeriodID, main.RankID, rest.RankID
As mentioned before, this query was borrowed from a similar question and modified for my own use. I imagine the bones of the query is good and maybe I have messed up a variable somewhere but I can not seem to locate the problem line. The end goal is for the query to result in a table showing the EmployeeID, PeriodID, the rank they are being promoted from, and the rank they are being promoted to in the month the promotion was earned. Similar to the below.
EmployeeID
PeriodID
PerviousRankID
NewRank
123
131
1
2
123
133
2
3
Instead, my query is spitting out repeating previous/current ranks and the PeriodIDs seem to be static (such as what is shown below).
EmployeeID
PeriodID
PerviousRankID
NewRank
123
131
1
1
123
131
1
1
I am hoping someone with a greater knowledge base on these functions is able to quickly notice my mistake.
If we assume some example DML/DDL (it's really helpful to provide this with your question):
DECLARE #Employees TABLE (EmployeeID INT IDENTITY, Name VARCHAR(20), RankID INT);
DECLARE #Periods TABLE (PeriodID INT, EmployeeID INT, RankID INT);
INSERT INTO #Employees (Name, RankID) VALUES ('Jonathan', 10),('Christopher', 10),('James', 10),('Jean-Luc', 8);
INSERT INTO #Periods (PeriodID, EmployeeID, RankID) VALUES
(1,1,1),(2,1,1),(3,1,1),(4,1,8 ),(5,1,10),(6,1,10),
(1,2,1),(2,2,1),(3,2,1),(4,2,8 ),(5,2,8 ),(6,2,10),
(1,3,1),(2,3,1),(3,3,7),(4,3,10),(5,3,10),(6,3,10),
(1,4,1),(2,4,1),(3,4,1),(4,4,8 ),(5,4,9 ),(6,4,9 )
Then we can accomplish what I think you're looking for using a OUTER APPLY then aggregates the values based on the current-row values:
SELECT e.EmployeeID, e.Name, e.RankID AS CurrentRank, ap.PeriodID AS ThisPeriod, p.PeriodID AS LastRankChangePeriodID, p.RankID AS LastRankChangedFrom, ap.RankID - p.RankID AS LastRankChanged
FROM #Employees e
LEFT OUTER JOIN #Periods ap
ON e.EmployeeID = ap.EmployeeID
OUTER APPLY (
SELECT EmployeeID, MAX(PeriodID) AS PeriodID
FROM #Periods
WHERE EmployeeID = e.EmployeeID
AND RankID <> ap.RankID
AND PeriodID < ap.PeriodID
GROUP BY EmployeeID
) a
LEFT OUTER JOIN #Periods p
ON a.EmployeeID = p.EmployeeID
AND a.PeriodID = p.PeriodID
ORDER BY e.EmployeeID, ap.PeriodID DESC
Using the correlated subquery we get a view of the data which we can filter using the current-row values, and we aggregate that to return the period we're looking for (where it's before this period, and it's not the same rank). Then it's just a join back to the Periods table to get the values.
You used an LEFT JOIN, so I've preserved that using an OUTER APPLY. If you wanted to filter using it, it would be a CROSS APPLY instead.
EmployeeID
Name
CurrentRank
ThisPeriod
LastRankChangePeriodID
LastRankChangedFrom
LastRankChanged
1
Jonathan
10
6
4
8
2
1
Jonathan
10
5
4
8
2
1
Jonathan
10
4
3
1
7
1
Jonathan
10
3
1
Jonathan
10
2
1
Jonathan
10
1
2
Christopher
10
6
5
8
2
2
Christopher
10
5
3
1
7
2
Christopher
10
4
3
1
7
2
Christopher
10
3
2
Christopher
10
2
2
Christopher
10
1
3
James
10
6
3
7
3
3
James
10
5
3
7
3
3
James
10
4
3
7
3
3
James
10
3
2
1
6
3
James
10
2
3
James
10
1
4
Jean-Luc
8
6
5
9
-1
4
Jean-Luc
8
5
4
8
1
4
Jean-Luc
8
4
3
1
7
4
Jean-Luc
8
3
4
Jean-Luc
8
2
4
Jean-Luc
8
1
Now we can see what the previous change looked like for each period. Currently Jonathan is has RankID 10. Last time that was different was in PeriodID 4 when it was 8. The same was true for PeriodID 5. In PeriodID 4 he had RankID 8, and prior to that he had RankID 1. Before that his Rank hadn't changed.
Jean-Luc was actually demoted as his last change. I don't know if this is possible within your model.
I am using CURSOR to implement the following in SQL Server, I am only iterating through the table - The time complexity will be O(n) I think (?). But everywhere I read about CURSOR, it says CURSOR is a bad practice. So is there a better way to implement the following ?
Existing Table
month value
1 92
4 20
9 92
New Table
month value
1 92
2 92
3 92
4 20
5 20
6 20
7 20
8 20
9 92
10 92
11 92
12 92
The use of cursor isn't (primarily) bad because it has poor time complexity, but because it is more error-prone and harder to read than a simple query. You are correct that iterating over a table via cursor is O(n).
On to your problem at hand. If you have the months (1..12) stored somewhere, say Months, then you can do it like this:
WITH matchingMonths AS (
SELECT m.month, MAX(mav.month) as matchedMonth
FROM Months m, MonthsAndValues mav
WHERE m.month >= mav.month
GROUP BY m.month
)
SELECT mm.month, mav.value
FROM matchingMonths mm
JOIN MonthsAndValues mav on mav.month = mm.matchedMonth
Without such a table Months, you could generate it on-the-fly:
WITH Months(month) AS (
SELECT 1
UNION ALL
SELECT month + 1 FROM Months WHERE month < 12
),
matchingMonths AS (
SELECT m.month, MAX(mav.month) as matchedMonth
FROM Months m, MonthsAndValues mav
WHERE m.month >= mav.month
GROUP BY m.month
)
SELECT mm.month, mav.value
FROM matchingMonths mm
JOIN MonthsAndValues mav on mav.month = mm.matchedMonth
I have 3 tables. The first table 'Status_Mapping' has following columns
Status_original Status_Site
accepted Call Verified
duplicate Duplicate Leads
dq DQ
'Lead_transaction' has the columns:
Lead_transaction_id Rate Status
11 0.01 accepted
12 0.02 accepted
13 0.01 newstatus
'Lead_Instance' table:
Lead_Instance_id Lead_transaction_id product_id affiliate_id
1 11 6 10
2 12 7 11
3 13 6 10
What I want to do is get the count(lead_instance_id) and sum(rate) for status which are not present in status_mapping table and should display status as "other", with product_id = 6 and affiliate_id = 10 My End result should be like
Total Sum Status
1 0.01 Other
you can start with this query:
select count(distinct a.Lead_Instance_id), sum(b.Rate)
from
Lead_Instance as a
inner join
Lead_transaction as b
on (a.Lead_transaction_id = b.Lead_transaction_id)
where
b.Status not in (select distinct Status_original from Status_Mapping)
and a.product_id = 6
and a.affiliate_id = 10
In SQL Server 2008 R2, I have a table like this:
ID Dates Count
1 03-02-2014 2
2 04-02-2014 1
3 05-02-2014 NULL
4 06-02-2014 1
5 07-02-2014 3
6 08-02-2014 NULL
7 09-02-2014 2
8 10-02-2014 NULL
9 11-02-2014 1
10 12-02-2014 3
11 13-02-2014 NULL
12 14-02-2014 1
I have an INT variable having some value such as #XCount = 15.
My requirement is to update the count column with (#XCount - Count) such as the result of previous record will be subtracted by the Count value in the next record.
Result:
ID Dates Count
1 03-02-2014 13 (15-2)
2 04-02-2014 12 (13-1)
3 05-02-2014 12 (12-0)
4 06-02-2014 11 (12-1)
5 07-02-2014 8 (11-3)
6 08-02-2014 8 (8-0)
7 09-02-2014 6 (8-2)
8 10-02-2014 6 (6-0)
9 11-02-2014 5 (6-1)
10 12-02-2014 2 (5-3)
11 13-02-2014 2 (2-0)
12 14-02-2014 1 (2-1)
I'm reluctant to use cursors as a solution. Can somebody help me?
How about something like
DECLARE #XCount INT = 15
;WITH Vals AS(
SELECT ID, Dates, [Count] OriginalCount, #XCount - ISNULL([COUNT],0) NewCount
FROM Table1
WHERE ID = 1
UNION ALL
SELECT t.ID, t.Dates, t.[Count], v.NewCount - ISNULL(t.[Count],0)
FROM Table1 t INNER JOIN Vals v ON t.ID = v.ID + 1
)
SELECT *
FROM Vals
SQL Fiddle DEMO
Do note thought that this is a recursive query, and that sometimes (until the tech allows for it, such as SQL SERVER 2012 LAG or Running totals) old does work.
In SQL Server, I have a table (let's call it TransList) that looks like:
TPN Start End TDate DoneBy
10 6 7 2003-03-17 14:48:42.750 User2 *
10 1 6 2003-03-13 08:02:09.317 User3
11 3 6 2003-03-21 08:15:45.410 User3 ** yes
11 6 3 2003-03-13 08:13:13.920 User4 <--
11 5 6 2003-03-08 17:39:51.460 User4
12 13 3 2003-03-19 10:58:23.187 User8 *
12 6 13 2003-03-17 14:48:42.750 User7
12 3 6 2003-03-13 08:02:09.317 User6
12 1 3 2003-03-01 14:09:17.167 User1
13 3 6 2003-03-19 10:58:23.187 User1 *** no
13 1 3 2003-03-01 14:09:17.167 User2 <--
14 3 6 2003-03-21 08:15:45.410 User5 ** yes
14 13 3 2003-03-13 08:13:13.920 User6 <--
14 6 13 2003-03-08 17:39:51.460 User7
15 6 3 2003-03-17 14:48:42.750 User2 *
15 1 6 2003-03-13 08:02:09.317 User3
This is the result of a pretty complex query which joins two separate SELECT statements which joins several tables each. Rows are ordered by TPN ASC, TDate DESC.
I would now like to filter this table and obtain:
TPN Start End TDate DoneBy
10 6 7 2003-03-17 14:48:42.750 User2
11 3 6 2003-03-21 08:15:45.410 User3
12 13 3 2003-03-19 10:58:23.187 User8
14 3 6 2003-03-21 08:15:45.410 User5
15 6 3 2003-03-17 14:48:42.750 User2
That is:
the newest transaction for each selected TPN
TPN is selected based on conditions on its newest transaction and/or its two newest transactions
Rows marked with * are there because (Start=6 and End=7) or (Start=13 and End=3) or (Start=6 and End=3) therefore I do not care of other transactions for these TPN
Rows marked with ** are there because (Start=3 and End=6) and for previous transaction (Start=6 and End=3)
Rows marked with *** are not there because (Start=3 and End=6) but for previous transaction is not (Start=6 and End=3)
I might need to further select based on which User did the previous transaction and/or have more complex logical conditions for * and ** (the state machine is complex and I have not yet finished checking it) but *** is always a not **.
I always need to check only the top 2 transactions for each TPN.
I am new to SQL and I spent already a couple of days to try figure out how I can achieve this. I considered self joining TransList, using LIMIT or TOP or walking through the table but I did not manage to make any of this solution work.
Can anyone help?
Edit:
The table above was an "extract", but to respond to the request of Andre, here is the query:
SELECT Items.[TPN],
[StartStatus],
[EndStatus],
[TransactionDate],
[TransDoneBy],
[CurrentStatus],
[Title],
[Severity],
[LastChangeDate],
[ChangeDoneBy],
[Planned],
[Remaining]
FROM
(SELECT WORKITEM as 'TPN',
TFIELDCHANGE.NEWSYSTEMOPTIONID as 'StartStatus',
TFIELDCHANGE.OLDSYSTEMOPTIONID as 'EndStatus',
THISTORYTRANSACTION.LASTEDIT as 'TransactionDate',
trans_person.LASTNAME as 'TransDoneBy'
FROM dbo.THISTORYTRANSACTION JOIN dbo.TFIELDCHANGE ON (dbo.THISTORYTRANSACTION.OBJECTID = dbo.TFIELDCHANGE.HISTORYTRANSACTION)
JOIN dbo.TPERSON trans_person ON (dbo.THISTORYTRANSACTION.CHANGEDBY = trans_person.PKEY)
WHERE dbo.TFIELDCHANGE.FIELDKEY = 4 -- Only transactions regarding status
) Transactions
JOIN
(SELECT WORKITEMKEY as 'TPN',
TSTATE.LABEL as 'CurrentStatus',
PACKAGESYNOPSYS as 'Title',
TSEVERITY.LABEL as 'Severity',
TWORKITEM.LASTEDIT as 'LastChangeDate',
item_person.LASTNAME as 'ChangeDoneBy',
max(CASE TATTRIBUTEVALUE.FIELDKEY WHEN 60 THEN TATTRIBUTEVALUE.INTEGERVALUE END) as 'Planned',
max(CASE TATTRIBUTEVALUE.FIELDKEY WHEN 72 THEN TATTRIBUTEVALUE.INTEGERVALUE END) as 'Remaining'
FROM dbo.TWORKITEM JOIN dbo.TSTATE ON (dbo.TWORKITEM.STATE = dbo.TSTATE.PKEY)
JOIN dbo.TSEVERITY ON (dbo.TWORKITEM.SEVERITYKEY = dbo.TSEVERITY.PKEY)
JOIN dbo.TPERSON item_person ON (dbo.TWORKITEM.CHANGEDBY = item_person.PKEY)
JOIN dbo.TATTRIBUTEVALUE ON (dbo.TWORKITEM.WORKITEMKEY = dbo.TATTRIBUTEVALUE.WORKITEM)
WHERE dbo.TWORKITEM.STATE = 2 OR -- Current state: analyzed
dbo.TWORKITEM.STATE = 3 OR -- Current state: assigned
dbo.TWORKITEM.STATE = 4 OR -- Current state: suspended
dbo.TWORKITEM.STATE = 6 OR -- Current state: implemented
dbo.TWORKITEM.STATE = 7 OR -- Current state: verified
dbo.TWORKITEM.STATE = 13 -- Current state: verifying
GROUP BY WORKITEMKEY,
TSTATE.LABEL,
PACKAGESYNOPSYS,
TSEVERITY.LABEL,
TWORKITEM.LASTEDIT,
item_person.LASTNAME
) Items
ON Items.TPN = Transactions.TPN
ORDER BY Items.[TPN] ASC, [TransactionDate] DESC
It seems you are looking for a general way to compare the latest transaction of a group with its predecessor.
This can be done using the ROW_NUMBER() function and a self join like this:
SELECT foo
FROM (SELECT foo,
Row_number() OVER (PARTITION BY TRN ORDER BY TDate) AS RN
FROM TranTable) AS Latest
LEFT JOIN (SELECT foo,
Row_number() OVER (PARTITION BY TRN ORDER BY TDate) AS RN
FROM TranTable) AS Previous
ON Latest.RN = Previous.RN - 1
WHERE Latest.RN = 1 /* Get only the latest */
OR (/* your criteria for two latest */ AND Latest.RN IN (1,2))