Complex SQL query, not sure where to start - sql-server

I have a tough one here I think. I have the following tables:
[Assets]
AssetId | Name
1 | Acura NSX
2 | Dodge Ram
[Assignments]
AssignmentId | AssetId | StartMileage | EndMileage | StartDate | EndDate
1 | 1 | 8000 | 10000 | 4/1/2015 | 5/1/2015
2 | 1 | 10000 | 16000 | 9/15/2015 | 1/5/2016
3 | 2 | 51000 | NULL | 1/1/2016 | NULL
[Reminders]
ReminderId | AssetId | Name | Distance | Time | Active
1 | 1 | Oil Change | 3000 (miles)| 3 (months)| 1
2 | 1 | Tire Rotation | 5000 | 6 | 0
3 | 2 | Oil Change | 3000 | 3 | 1
4 | 2 | Air Filter | 50000 | 48 | 1
[Maintenance]
MaintenanceId | AssetId | ReminderId | Mileage | Date | Vendor
1 | 1 | 1 | 10000 | 5/1/2015 | Jiffy Lube
2 | 2 | 3 | 51000 | 6/1/2015 | Dealership
I need a query that will join these 4 tables and return something like the following.
Name | Name | Current Mileage | Last Mileage | Last Date
Acura NSX | Oil Change | 16000 | 10000 | 5/1/2015
Dodge RAM | Oil Change | 51000 | 51000 | 6/1/2015
Dodge RAM | Air Filter | 51000 | -- | --
I need to take the distance threshold from the Reminders table and add it to the mileage from the Maintenance table then compare it to the start and end mileage from the Assignments table. If the threshold is greater than the start or end mileage then select the asset name, the name of the reminder, the current mileage (start or end mileage from Assignments, whichever is greater), and mileage and date from the last maintenance for that reminder. I need to do the same for time threshold. Add it to the date from the Maintenance table then compare it to today's date. If it's greater then display the asset.
Can one of you SQL gurus help me with this please?
UPDATE:
SELECT
v.Name,
r.Name AS Reminder,
a.CurrentMileage,
i.MaintenanceMileage,
i.MaintenanceDate
FROM
Assets v
LEFT JOIN
(SELECT AssetId,
COALESCE(EndMileage, StartMileage) AS CurrentMileage,
ROW_NUMBER() OVER (PARTITION BY AssetId
ORDER BY AssignmentId DESC) AS window_id
FROM Assignments) a
ON v.AssetId = a.AssetId
AND a.window_id = 1
JOIN
Reminders r
ON v.AssetId = r.AssetId
AND r.ActiveFlag = 1
LEFT JOIN
(SELECT AssetId,
ReminderId,
MAX(Mileage) AS MaintenanceMileage,
MAX([Date]) AS MaintenanceDate
FROM Maintenances
GROUP BY AssetId, ReminderId) i
ON r.ReminderId = i.ReminderId
AND (a.CurrentMileage > (NULLIF(i.MaintenanceMileage, 0) + r.DistanceThreshold))
OR (GETDATE() > DATEADD(m, r.[TimeThreshold], i.MaintenanceDate))

Here is a starting point:
SELECT v.Name AS [Asset Name], r.Name AS Reminder, a.CurrentMileage,
m.Mileage + r.Distance AS [Last Mileage], m.[Date] AS [Last Date]
FROM Assets v
JOIN ( -- get the latest relevant row as window_id = 1
SELECT AssetId, COALESCE(EndMileage, StartMileage) AS CurrentMileage,
COALESCE(EndDate, StartDate) AS AssignDate,
ROW_NUMBER() OVER (partition by AssetId
order by COALESCE(EndDate, StartDate) DESC) AS window_id
FROM Assignments
) a
ON v.AssetId = a.AssetId
AND a.window_id = 1
JOIN Reminders r
ON v.AssetId = r.AssetId
AND r.Active = 1
LEFT JOIN Maintenance m
ON r.AssetId = m.AssetId
AND r.ReminderId = m.ReminderId
-- corrected
AND ((a.CurrentMileage > (NULLIF(m.Mileage, 0) + r.Distance))
-- slightly oversimplified
OR (GETDATE() > DATEADD(m, r.[Time], COALESCE(m.[Date], a.AssignDate))))
The date calculations are slightly oversimplified because they use the latest assignment dates. What you would really want is a column Assets.InServiceDate that would anchor the time before the first maintenance would be due. But this will get you started.

Related

SQL Server Stock age calculation - Alternative to nested cursors

I'm trying to do some kind of stock age calculation based on two tables.
I have a current stock for each reference and I'd like to match it with the most recent warehouse entrances until I have no units left.
My Stock_Table looks like this
| Product_Ref | Stock |
| ----------- | ------- |
| Prod_A | 100 |
| Prod_B | 50 |
My Entrances_Table (ordered by most recent date) looks like this
| Product_Ref | Month | Units |
| ----------- | ------- | ------- |
| Prod_A | July | 50 |
| Prod_A | June | 30 |
| Prod_A | May | 35 |
| Prod_B | May | 10 |
| Prod_B | April | 55 |
What I need (as a previous step to do some other calcuations that I have already figured out) is to build this results table:
| Product_Ref | Month | Units |
| ----------- | ------- | ------- |
| Prod_A | July | 50 |
| Prod_A | June | 30 |
| Prod_A | May | 20 | <-- previous 50+30 so only 20 "left" to achive 100 units
| Prod_B | May | 10 |
| Prod_B | April | 40 | <-- previous 10 so only 40 "left" to achive 100 units
I know I could iterate throught both tables with a nested cursor, but I would like to know if there's a more elegant solution (maybe via running sums, using lead() , CTE, or something that I'm missing ... )
Any ideas are appreciated!
Thank you very much in advance!
You can get the desired output by using Sum() over(order by). This function returns running total of the column in SQL. So the query will be:
SELECT A.Product_Ref ,
A.Month ,
CASE WHEN A.RT <= A.Stock THEN A.Units ELSE A.Stock - (LAG(A.RT) OVER (ORDER BY A.id)) END AS Units
FROM ( SELECT Product_Ref, Units, Month, Stock, Id ,
SUM(E.Units) OVER ( PARTITION BY E.Product_Ref ORDER BY E.Id ) AS RT
FROM Entrances E
INNER JOIN Stock S ON S.Prod = E.Product_Ref
) A;
To view the output of the query click on DEMO
Try with this query,
You can use Lead() in sql-server for check next row value of particular column.
select et.Product_Ref, et.Month, et.Unit,
case when SUM(et.Unit) OVER (PARTITION BY Product_Ref ORDER BY id) > st.Unit1
OR
(lead(et.id)over( order by et.id)) is null
then
case when lead(et.Product_Ref) over(order by id) <> et.Product_Ref
AND
( et.unit - ((SUM(et.Unit) OVER (PARTITION BY Product_Ref ORDER BY id) ) - st.Unit1) ) <= 0
then -- (-)ve Stock
st.Unit1 - ((SUM(et.Unit) OVER (PARTITION BY Product_Ref ORDER BY id) ))
else -- Stock Adjustment As per Stock_Table
et.unit - ((SUM(et.Unit) OVER (PARTITION BY Product_Ref ORDER BY id) ) - st.Unit1)
end
else et.unit -- Normal Stock
end as outputvalue
from Entrances_Table et
left join Stock_Table st on st.Product_Ref1 = et.Product_Ref
DB Fiddle

Lead/Lag syntax help - how do you order the columns?

Thanks for your help/advice. I'm unclear about the 1,0 within the LAG expression - what is that and why isn't mine working?
Do I have to do two Order by for both lead and lag?
Select
* Customer,
Prod,
day,
current sold,
date,
lag[current sold,1,0] OVER(PARTITION BY customer ORDER BY date DESC) as Previous Day,
lead[current sold,1,0] OVER(PARTITION BY customer ORDER BY date DESC) as Next Day
From
table1
Result:
| PROD | DAY | CURRENT SOLD | date customer |
+-------+-----+--------------+-----------------------
| SHIRT | M | 2 | 1-2018 A
| SHIRT | T | 9 | 2-2018 B
| SHIRT | W | 0 | 12-2018 C
| SHIRT | TH | 6 | 11-2018 D
| SHIRT | F | 7 | 3-2018 E
+-------+-----+--------------+--+----------------
+-------+-----+--------------+---------------+-----------+--+--------------
| PROD | DAY | CURRENT SOLD | PREVIOUS SOLD | NEXT SOLD | date |customer
+-------+-----+--------------+---------------+-----------+--+---------------
| SHIRT | M | 2 | | 9 | 1-2018 |A
| SHIRT | T | 9 | 2 | 0 | 2-2018 |B
| SHIRT | W | 0 | 9 | 6 | 12-2018|C
| SHIRT | TH | 6 | 0 | 7 | 11-2018|D
| SHIRT | F | 7 | 6 | | 3-2018 |E
+-------+-----+--------------+---------------+-----------+--+---------------
you can use LAG for previous sales and LEAD for next sales. I prepared sample with your example :
--DROP TABLE #Tbl;
--DROP TABLE #Days;
CREATE TABLE #Tbl
(
Prod VARCHAR(10)
,[DayName] VARCHAR(2)
,CurrentSold INT
);
CREATE TABLE #Days
(
DayNumber INT
,[DayName] VARCHAR(2)
);
INSERT INTO #Days
VALUES (1,'M'),(2,'T'),(3,'W'),(4,'TH'),(5,'F');
INSERT INTO #Tbl
VALUES ('SHIRT','M',2)
,('SHIRT','T',9)
,('SHIRT','W',0)
,('SHIRT','TH',6)
,('SHIRT','F',7);
SELECT T.Prod
,T.DayName
,T.CurrentSold
,LAG(CurrentSold, 1,0) OVER (ORDER BY DayNumber) AS PreviousSold
,LEAD(CurrentSold, 1,0) OVER (ORDER BY DayNumber) AS PreviousSold
FROM #Tbl T
INNER JOIN #Days D ON T.DayName = D.DayName;

T-SQL Pivot table

I’ve a table MachineStatus which stores the status history of a machine.
The table looks like this:
| MachineStatusId | From | To | State | MachineId |
----------------------------------------------------------------------------------------------------------------------------------------
| B065FC43-DBE7-E611-9BDB-801F02F47041 | 2017-01-30 07:00:00 | 2017-01-30 08:00:00 | 1 | 92649C7B-E962-4EB1-B631-00086EECA98A |
| B165FC43-DBE7-E611-9BDB-801F02F47041 | 2017-01-30 08:00:00 | 2017-01-30 09:00:00 | 200 | 92649C7B-E962-4EB1-B631-00086EECA98A |
| B265FC43-DBE7-E611-9BDB-801F02F47041 | 2017-01-30 07:00:00 | 2017-01-30 08:00:00 | 1 | A2649C7B-E962-4EB1-B631-00086EECA98A |
| B365FC43-DBE7-E611-9BDB-801F02F47041 | 2017-01-30 08:00:00 | 2017-01-30 09:00:00 | 500 | A2649C7B-E962-4EB1-B631-00086EECA98A |
It stores for each machine, for each status change a record with the information [From] when [To] when a certain [State] was valid.
I like to calculate the time each machine spent in each state.
The result should look like this:
| MachineId | Alias | State1 | State200 | State500 |
-------------------------------------------------------------------------------------------------
| 92649C7B-E962-4EB1-B631-00086EECA98A | Somename | 60 | 60 | 0 |
| A2649C7B-E962-4EB1-B631-00086EECA98A | Some other name | 60 | 0 | 60 |
Each state should be represented as a column.
Here is wat I have tried so far:
SELECT
MAX(mState.MachineId),
MAX(m.Alias),
SUM(CASE mState.State WHEN 1 THEN mState.Diff ELSE 0 END) AS CritTime,
SUM(CASE mState.State WHEN 200 THEN mState.Diff ELSE 0 END) AS OpTime,
SUM(CASE mState.State WHEN 500 THEN mState.Diff ELSE 0 END) AS OtherTime
FROM
(
SELECT
DATEDIFF(MINUTE, ms.[From], ISNULL(ms.[To], GETDATE())) AS Diff,
ms.State AS State,
MachineId
FROM
MachineStatus ms
WHERE
ms.[From] >= #rangeFrom AND
(ms.[To] <= #rangeEnd OR ms.[To] IS NULL)
) as mState
INNER JOIN Machines m ON m.MachineId = mState.MachineId
GROUP BY
mState.MachineId,
m.Alias,
mState.State
Calculating the time and grouping the result by machines works but I cannot figure out how to reduce the result set only contain one row per machine but with a column per state.
I started in your subquery without apply any sum to your calculated data:
SELECT m.MachineId,
m.Alias,
Minutes,
s.State
FROM machines m
INNER JOIN states s ON m.MachineId = s.MachineId
Then you can pivot() for [State] and calculate the sum() of every state in this form:
WITH Calc AS
(
SELECT m.MachineId,
m.Alias,
Minutes,
s.State
FROM machines m
INNER JOIN states s ON m.MachineId = s.MachineId
)
SELECT MachineId, Alias, [State1], [State2], [State500]
FROM
(SELECT MachineId, Alias, State, Minutes FROM Calc) AS SourceTable
PIVOT
(
SUM(Minutes) FOR State IN ([State1],[State2],[State500])
) AS PivotTable;
This is the result:
+--------------------------------------+---------+--------+--------+----------+
| MachineId | Alias | State1 | State2 | State500 |
+--------------------------------------+---------+--------+--------+----------+
| 92649C7B-E962-4EB1-B631-00086EECA98A | Alias 1 | 100 | 100 | 100 |
+--------------------------------------+---------+--------+--------+----------+
| A2649C7B-E962-4EB1-B631-00086EECA98A | Alias 2 | 10 | 20 | 70 |
+--------------------------------------+---------+--------+--------+----------+
Notice that you must know how many states return your data.
Can check it here: http://rextester.com/DHDX77489

Issue with Running Total Still

I still have an issue with working out the best way to calculate a running balance.
I am going to be using this code in a Rent Statement that I am going to produce in SSRS, but the problem I am having is that I can't seem to work out how to achieve a running balance.
SELECT rt.TransactionId,
rt.TransactionDate,
rt.PostingDate,
rt.AccountId,
rt.TotalValue,
rab.ClosingBalance,
ROW_NUMBER()OVER(PARTITION BY rt.AccountId ORDER BY rt.PostingDate desc) AS row,
CASE WHEN ROW_NUMBER()OVER(PARTITION BY rt.AccountId ORDER BY rt.PostingDate desc) = 1
THEN ISNULL(rab.ClosingBalance,0)
ELSE 0 end
FROM RentTransactions rt
--all accounts for the specific agreement
INNER JOIN (select raa.AccountId
from RentAgreementEpisode rae
inner join RentAgreementAccount raa on raa.AgreementEpisodeId = rae.AgreementEpisodeId
where rae.AgreementId=1981
) ij on ij.AccountId = rt.AccountId
LEFT JOIN RentBalance rab on rab.AccountId = rt.AccountId AND rt.PostingDate BETWEEN rab.BalanceFromDate AND isnull(rab.BalanceToDate,dateadd(day, datediff(day, 0, GETDATE()), 0))
What this gives me are the below results- I have included the results below -
So my code is sorting my transactions in the order I want and also is row numbering them in the correct order as well.
Where the Row Number is 1 - I need it to pull back the balance on that account at that point in time, which is what I am doing....BUT I am then unsure how I then get my code to start subtracting the proceeding row - so in this case The current figure of 1118.58 would need the Total Value in Row 2 = 91.65 subtracted from it - so the running balance for row 2 would be 1026.93 and so on...
Any help would be greatly appreciated.
Assuming you have all the transactions being returned in your query you can calculate a running total using the over clause, you just need to start at the beginning of your dataset rather than working backwards from your current balance:
declare #t table(d date,v decimal(10,2));
insert into #t values ('20170101',10),('20170102',20),('20170103',30),('20170104',40),('20170105',50),('20170106',60),('20170107',70),('20170108',80),('20170109',90);
select *
,sum(v) over (order by d
rows between unbounded preceding
and current row
) as RunningTotal
from #t
order by d desc
Output:
+------------+-------+--------------+
| d | v | RunningTotal |
+------------+-------+--------------+
| 2017-01-09 | 90.00 | 450.00 |
| 2017-01-08 | 80.00 | 360.00 |
| 2017-01-07 | 70.00 | 280.00 |
| 2017-01-06 | 60.00 | 210.00 |
| 2017-01-05 | 50.00 | 150.00 |
| 2017-01-04 | 40.00 | 100.00 |
| 2017-01-03 | 30.00 | 60.00 |
| 2017-01-02 | 20.00 | 30.00 |
| 2017-01-01 | 10.00 | 10.00 |
+------------+-------+--------------+

Select multiple values from same column with first and second latest period date

I am trying to get records from same column where the MARK 1 & GRED 1 select from SECOND LATEST RANGE PERIOD and the MARK 2 & GRED 2 is from LATEST RANGE PEROID.
Example..
Sam GRED 2 is from 01.01.2014 - 30.06.2014 period
Sam MARK 2 is from 01.07.2014 - 31.09.2014 period
Below is example table and desire output.
RESULT TABLE
| Name | Gred | Mark | SemPeriod |
|------|------|-------|-------------------------|
| Sam | C | 45.60 | 01.01.2013 - 30.06.2013 |
| Sam | B | 55.55 | 01.07.2013 - 31.12.2013 |
| Sam | A | 85.50 | 01.01.2014 - 30.06.2014 |
| Sam | C | 48.60 | 01.07.2014 - 31.09.2014 |
| Sean | C | 45.60 | 01.01.2014 - 30.06.2014 |
| Sean | B | 55.55 | 01.07.2014 - 31.12.2014 |
| Sean | A | 85.50 | 01.01.2015 - 30.06.2015 |
| Sean | C | 48.60 | 01.07.2015 - 31.12.2015 |
DESIRED OUTPUT
| Name | Gred 1| Mark 1 | Gred 2 | Mark 2 |
|------|-------|--------|--------|---------|
| Sam | A | 85.50 | C | 48.60 |
| Sean | A | 85.50 | C | 48.60 |
I just can't get my head wrapped around this and still stuck to display the output. Help would be greatly appreciated.
What is the data type for SemPeriod ?
; WITH CTE AS
(
SELECT *, RN = ROW_NUMBER() OVER (PARTITION BY Name ORDER BY SemPeriod_Start DESC)
FROM yourtable
)
SELECT Name,
Gred1 = MAX(CASE WHEN RN = 2 THEN Gred END),
Mark1 = MAX(CASE WHEN RN = 2 THEN Mark END),
Gred2 = MAX(CASE WHEN RN = 1 THEN Gred END),
Mark2 = MAX(CASE WHEN RN = 1 THEN Mark END)
FROM CTE
GROUP BY Name
if your SemPeriod is string, you should split into 2 different date column.
If not, the query will be ugly like
PARTITION BY Name
ORDER BY CONVERT(DATE, LEFT(SemPeriod , charindex('-', SemPeriod) - 1), 103) DESC
for large dataset, it will be slow. Also the query will break if the date range is formatted differently in the SemPeriod or there are bad data in there.
To ensure data quality, it is best to split into 2 separate date column.

Resources