I have a (slow) query that almost does what I want. The bold section is the pseudo-code I am trying to add. The goal of this query is to get a single row per custnumber with 5 columns of data:
colA: MKT
colB: custnumber
colC: most recent apptdate WHERE src_id = Loss Recovery AND dsp_id = Sale
colD: most recent apptdate WHERE src_id NOT= Loss Recovery AND dsp_id NOT= Sale and this apptdate is earlier than LRSale
colE: days between NoSale and LRSale
This is what I have built so far, close to working. I have tried several different ways to get the last bit in but am having real trouble parsing the syntax to make it work right.
SELECT DISTINCT *, DATEDIFF(day,NoSale,LRSold) AS 'Diff'
FROM(SELECT a.MKT,
a.custnumber,
(SELECT TOP 1 CAST(apptdate AS DATE)
FROM prospectissues b
WHERE b.custnumber = a.custnumber
**AND b.apptdate < c.apptdate**
AND b.src_id <> 'Loss Recovery' AND b.dsp_id <> 'Sale'
ORDER BY apptdate DESC) AS 'NoSale',
(SELECT TOP 1 CAST(apptdate AS DATE)
FROM prospectissues c
WHERE c.custnumber = a.custnumber
AND c.src_id = 'Loss Recovery' AND c.dsp_id = 'Sale'
ORDER BY apptdate DESC) AS 'LRSold'
FROM prospectissues a
WHERE CAST(apptdate AS DATE) >= DATEADD(yy, DATEDIFF(yy,0,GETDATE())-3,0)) z
WHERE LRSold IS NOT NULL
Thank you, I appreciate any insight! I'm sure it's something simple that I'm just too tired to see right now. Been banging my head against this for too long.
-edit 7/25
I cannot write to the database, only run Select queries.
Here is a sample of the table I'm working with (rows numbered just for easy reference):
| MKT | custnumber | dsp_id | src_id | ApptDate |
----------------------------------------------------------
1 | CLE | 14675 | PMPrice | Email | 3/7/20 |
2 | CLE | 14675 | Sale | Radio | 3/9/20 |
3 | CLE | 246466 | NH | Prior Customer | 3/7/20 |
4 | PIT | 284747 | PMPrice | Show | 3/7/20 |
5 | PIT | 284747 | Sale | Loss Recovery | 3/12/20 |
6 | PIT | 384603 | Sale | SelfGen | 3/3/20 |
7 | PIT | 384603 | Sale | Loss Recovery | 3/8/20 |
8 | COL | 384764 | PMPrice | Website | 3/15/22 |
9 | COL | 384921 | Sale | Website | 3/22/20 |
10 | COL | 385052 | PMPrice | Show | 3/7/20 |
11 | COL | 385052 | Sale | Aggregates | 3/10/20 |
12 | COL | 385052 | Sale | Loss Recovery | 3/18/20 |
13 | PIT | 385662 | OVB | Aggregates | 3/12/20 |
14 | PIT | 385662 | Sale | Loss Recovery | 3/21/20 |
15 | PIT | 385662 | PMPrice | Store | 4/15/20 |
Sample of desired output:
| MKT | custnumber | NoSale | LRSold | Diff |
----------------------------------------------
| PIT | 284747 | 3/7/20 | 3/12/20 | 5 |
| COL | 385052 | 3/7/20 | 3/18/20 | 11 |
| PIT | 385662 | 3/12/20 | 3/21/20 | 9 |
The goal is to visualize how long a time gap (days) exists between Non-sale (NS) records and Loss Recovery - Sale (LR) records.
NS = anything with dsp_id NOT equal to 'Sale' AND src_id NOT equal to 'Loss Recovery' counts as an NS record
LR = anything with dsp_id = 'Sale' AND src_id = 'Loss Recovery'
criteria are:
MKT: simple text, this will always be consistent between records with the same custnumber
custnumber: the "key column", always an integer
NoSale: the most recent NS apptdate prior to a LR apptdate sharing the same custnumber
LRSale: the most recent LR record grouped by custnumber
Diff: days between NS and LR
Seems simple in theory, but giving me a hell of a time.
I think this will do it. We use conditional aggregation to help simplify the initial pass, including grabbing the first computed column (LRSold), but we still use a couple layers of nesting to get NoSale and then the Diff(). We could definitely eliminate one layer of nesting, but then we'd have to repeat the entire nested query from the SELECT clause to compute the diff, and it's a small result set, so this is probably better:
SELECT *,
DATEDIFF(day, NoSale, LRSold) [Diff]
FROM (
SELECT *,
(
SELECT MAX(pi0.apptdate)
FROM prospectissues pi0
WHERE pi0.custnumber = t.custnumber AND pi0.MKT = t.MKT AND pi0.apptdate < t.LRSold
AND pi0.src_id <> 'Loss Recovery' and pi0.dsp_id <> 'Sale'
GROUP BY pi0.custnumber, pi0.MKT
) NoSale
FROM (
SELECT pi.custnumber, pi.MKT,
MAX(CASE WHEN pi.src_id = 'Loss Recovery' and pi.dsp_id = 'Sale' THEN apptdate END) As LRSold
FROM prospectissues pi
WHERE apptdate >= DATEADD(yy, DATEDIFF(yy,0,GETDATE())-3,0)
GROUP BY pi.custnumber, pi.MKT
HAVING MAX(CASE WHEN pi.src_id = 'Loss Recovery' and pi.dsp_id = 'Sale' THEN apptdate ELSE NULL END) IS NOT NULL
) t
)d
WHERE NoSale IS NOT NULL
See it here:
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=a41b9191cef92247b722788d6474edc2
I feel like we could do better with a lateral join (APPLY) to reduce the number of trips into the table, and often if that works there is also a Windowing function solution that will work even better. But this is what you get for free.
Finally, one thing I noticed is casting appdate into a DATE value. Given the reliance of the query on this field, this cast is very bad for performance, because it breaks any chance of using an index on the field and the way the query was structured forced the database you to run the conversion multiple times for every row in the table... even rows that will not be used for the results.
If the column is a datetime or datetime2 this is easily fixed: don't do the cast. It's not needed for the query to work properly. But if this is a varchar column, you need to FIX the schema, because it really is BROKEN. The performance issue here is just one of several reasons why, but it is effective at demonstrating why it's so important: just that one change to the column type would likely cut the execution time of the original query by more than an order of magnitude. NEVER use string-typed columns to store time-typed data.
I think this is what you are looking for. It should run quicker by using aggregates like Min instead of order by and top 1.
SELECT ns.MKT, ns.custnumber,
ns.MinDate AS NoSale, lr.MinDate AS LRSold
DATEDIFF(day, ns.MinDate, lr.MinDate) AS Diff
FROM (SELECT MKT, custnumber, min(apptdate) as MinDate
FROM prospectissues
WHERE src_id <> 'Loss Recovery' AND dsp_id <> 'Sale'
GROUP BY MKT, custnumber) ns
INNER JOIN --should be similar outcome as WHERE LRSold IS NOT NULL
(SELECT MKT, custnumber, min(apptdate) MinDate
FROM prospectissues b
WHERE src_id = 'Loss Recovery' AND dsp_id = 'Sale'
GROUP BY MKT, custnumber) lr
ON ns.custnumber = lr.custnumber AND ns.MKT = lr.MKT
AND ns.apptdate < lr.apptdate --The new part that you want
WHERE CAST(ns.MinDate AS DATE) >= DATEADD(yy, DATEDIFF(yy,0,GETDATE())-3,0)
I don't know if your data has any anomalies. You might want to consider an analytic approach.
with data1 as (
select MKT, custnumber, apptdate,
case when src_id <> 'Loss Recovery' and dsp_id <> 'Sale' then 'NS'
when src_id = 'Loss Recovery' and dsp_id = 'Sale' then 'LR'
end as flag
from prospectissues
where apptdate >= datefromparts(year(getdate()) - 3, 1, 1)
), data2 as (
select *,
case when flag = 'NS' then cast(apptdate as date) end NS,
case when flag = 'LR' then cast(apptdate as date) end LR,
count(case when flag = 'LR' then 1 end)
over (partition by MKT, custnumber order by apptdate) as LR_cnt
from data1
--where flag in ('NS', 'LR') /* possible optimization */
)
select MKT, custnumber,
max(NS) as NoSale, min(LR) as LRSold, datediff(day, max(NS), min(LR)) as Diff
from data2
where LR_cnt = 0 or flag = 'LR'
group by MKT, custnumber
having max(NS) is not null and min(LR) is not null;
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=9246435525b3ca67b3de6377b3a187be
Related
I have a table that shows the entry and exit of items into the warehouse. The Camera 1 and Camera 2 document the entry time and exit time respectively of that item. The cameras then classify the item as it enters and leaves the checkpoint with the help of lasers. Eg: Big box: Class 5, Medium Box: Class 3, Small Box: Class 2.
Sometimes, the cameras classification doesn't match each other. Eg: Classification at entry can be Medium box and on exit can be Small box.
I need to find the number of transactions where the class didn't match for the same TransactionDetail and then a percentage of those class mismatches against all the transaction for a certain time range.
My table looks somewhat like this:
---------------------------------------------------------------------------
| AVDetailID | TransDetailID | AVClassID | CamID | CreatedDate |
---------------------------------------------------------------------------
| 20101522 | 54125478 | 5 | 1 | 2017-05-08 10:15:01:560|
| 20101523 | 54125478 | 5 | 2 | 2017-05-08 10:15:01:620|
| 20101524 | 54125479 | 3 | 1 | 2017-05-08 10:15:03:120|
| 20101525 | 54125479 | 2 | 2 | 2017-05-08 10:15:03:860|
| 20101526 | 54125480 | 4 | 1 | 2017-05-08 10:15:06:330|
| 20101527 | 54125480 | 4 | 2 | 2017-05-08 10:15:06:850|
---------------------------------------------------------------------------
So, in the above case the class changes from 3 to 2 in record 3 and 4. That is one transaction where the class changed. I need to get a percentage of all transactions that where the class changed between each cameras.
The code I tried so far, unsuccessfully is:
SELECT
COUNT(TransDetailId)
FROM
[AVTransDetail]
WHERE
((SELECT AVCClassId WHERE CamId = 1) <> (SELECT AVCClassId WHERE DetectionZoneId = 2))
AND CreatedDate >= '2017-04-01'
AND CreatedDAte <= '2017-04-07'
GROUP BY
TransDetailId
You can try to join the table on itself like this:
SELECT tdBefore.TransDetailId
FROM AVTransDetail AS tdBefore
INNER JOIN AVTransDetail AS tdAfter
ON tdBefore.TransDetailID = tdAfter.TransDetailID
AND tdBefore.CamID = 1
AND tdAfter.CamID = 2
WHERE tdBefore.AVClassID <> tdAfter.AVClassID
AND tdBefore.CreatedDate >= '2017-04-01'
AND tdAfter.CreatedDate <= '2017-04-07'
Then to get the percentage:
DECLARE #MinDate DATE = '20170401',
#MaxDate DATE = '20170407';
SELECT tdBefore.TransDetailId,
COUNT(tdAfter.TransDetailID) OVER() AS NumDifferent,
((CONVERT(DECIMAL(3, 2), COUNT(tdAfter.TransDetailID) OVER())) / allRecords.Count) * 100 AS DiffPercent,
FROM AVTransDetail AS tdBefore
INNER JOIN AVTransDetail AS tdAfter
ON tdBefore.TransDetailID = tdAfter.TransDetailID
AND tdBefore.CamID = 1
AND tdBefore.CamID = 2
CROSS APPLY
(
SELECT COUNT(*) AS [Count]
FROM AVTransDetail
WHERE tdBefore.CreatedDate >= #MinDate
AND tdAfter.CreatedDate <= #MaxDate
) AS allRecords
WHERE tdBefore.AVClassID <> tdAfter.AVClassID
AND tdBefore.CreatedDate >= #MinDate
AND tdAfter.CreatedDate <= #MaxDate
I have a table with a history of assigning Eployee Type to a Work item, like follows:
| WorkItemID | EmployeeTypeID | ValidFrom | ValidTo |
| 1 | 1 | 2017-03-01 12:19:20.000 | 2017-03-05 14:11:20.000 |
| 1 | 1 | 2017-03-10 17:00:20.000 | NULL |
| 1 | 2 | 2017-05-12 12:19:20.000 | 2017-05-29 14:11:20.000 |
| 1 | 2 | 2017-07-01 12:19:20.000 | NULL |
| 2 | 1 | 2017-01-01 15:19:20.000 | 2017-03-01 11:29:20.000 |
| 2 | 1 | 2017-04-03 16:19:20.000 | NULL |
NULL means that there's no End date for the last assignment and it is still valid.
I also have a table with a history of assigning Eployee Type to an Employee:
| EmployeeID | EmployeeTypeID | ValidFrom | ValidTo |
| 1 | 1 | 2017-01-01 12:19:20.000 | 2017-03-05 14:11:20.000 |
| 1 | 2 | 2017-03-05 14:11:20.000 | NULL |
| 2 | 1 | 2016-05-05 15:19:20.000 | 2017-03-01 11:29:20.000 |
| 2 | 2 | 2017-03-01 11:29:20.000 | NULL |
For a given EmployeeID and WorkItemID, I need to select a minimum date within these date ranges where their EmployeeTypeID matched (if there is any).
For example, for EmployeeID = 1 And WorkItemID = 1 the minimum date when their Employeetypes matched is 2017-03-01 (disregard the time part).
How do I write an SQL query to join these two tables correctly and select the desired date?
The following way appeared to be correct for me:
Firstly, I select Min Date from table 1 that match with table 2 by date ranges and they should overlap as well:
DECLARE #MinDate1 datetime
DECLARE #MinDate2 datetime
SELECT #MinDate1 =
(SELECT MIN(t1.ValidFrom)
FROM Table1 t1
JOIN Table2 t2 ON t1.EmployeeTypeID = t2.EmployeeTypeID
WHERE t1.WorkItemID = 1 AND t2.EmployeeID = 1
AND (t1.ValidFrom <= t2.ValidTo OR t2.ValidTo IS NULL)
AND (t1.ValidTo >= t2.ValidFrom OR t1.ValidTo IS NULL))
Then I select Min Date from table 2 that match with table 1 by date ranges and they should overlap as well:
SELECT #MinDate2 =
(SELECT MIN(t2.ValidFrom)
FROM Table1 t1
JOIN Table2 t2 ON t1.EmployeeTypeID = t2.EmployeeTypeID
WHERE t1.WorkItemID = 1 AND t2.EmployeeID = 1
AND (t1.ValidFrom <= t2.ValidTo OR t2.ValidTo IS NULL)
AND (t1.ValidTo >= t2.ValidFrom OR t1.ValidTo IS NULL))
And finaly, I select the max date of two which would be the min date when the two ranges actually overlap and have the same EmployeeTypeID
SELECT CASE WHEN #MinDate1 > #MinDate2 THEN #MinDate1 ELSE #MinDate2 END AS MinOverlapDate
The output would be:
| MinOverlapDate |
| 2017-03-01 12:19:20.000 |
So it should be something like this:
SELECT MIN(Date)
FROM table1 t1
JOIN table2 t2 ON t1.EmployeeTypeID = t2.EmployeeTypeID
WHERE t1.EmployeeID = givenValue AND t2.WorkitemID = givenValue
But again if you dont know from which table the result goes you cant write a query for that.
What you should do is do at least 3 tables or maybe more
Would contain Employee informations
Items jobs dates whatever is connected to WORK
Some connection between them (Emp 1 has Work 2) (Emp 2 has Work 4) and so on
You CANNOT have same values in two tables without knowing from which one you want to get tha data!
OR .. You can do it into one table.
Columns: WorkItem | EmployeeID | EmployeeType | Date | Date
Actually, my variant still does not work correctly. The #MinDate1 and #MinDate2 should be compared by each EmployeeTypeID one by one. There it was compared independently.
Here is correct variant of solving this problem:
SELECT MIN(CASE WHEN t1.ValidFrom > t2.ValidFrom THEN t1.ValidFrom ELSE t2.ValidFrom END) AS MinOverlapDate
FROM Table1 t1
JOIN Table2 t2 ON t1.EmployeeTypeID = t2.EmployeeTypeID
WHERE t1.WorkItemID = 1 AND t2.EmployeeID = 1
AND (t1.ValidFrom <= t2.ValidTo OR t2.ValidTo IS NULL)
AND (t1.ValidTo >= t2.ValidFrom OR t1.ValidTo IS NULL)
Don't use >=, <=, = or between when comparing datetime fields. Since all of the mention operator would check against time as well. You would want to use datediff to check against the smallest interval according to your needs
select
Min_Overlap_Per_Section = (select MAX(ValidFrom)
FROM (VALUES (t1.ValidFrom), (t2.ValidFrom)) as ValidFrom(ValidFrom))
, Section_From = (select MAX(ValidFrom)
FROM (VALUES (t1.ValidFrom), (t2.ValidFrom)) as ValidFrom(ValidFrom))
, Section_To = (select MIN(ValidTo)
FROM (VALUES (t1.ValidTo), (t2.ValidTo)) as ValidTo(ValidTo))
from Table1
JOIN Table2 t2 ON t1.EmployeeTypeID = t2.EmployeeTypeID
where (
datediff(day, t1.ValidFrom, t2.ValidTo) >= 0
or t2.ValidTo IS NULL
)
and (
datediff(day, t2.ValidFrom, t1.ValidTo) >= 0
or t1.ValidTo IS NULL
)
I can use a traditional subquery approach to count the occurrences in the last ten minutes. For example, this:
drop table if exists [dbo].[readings]
go
create table [dbo].[readings](
[server] [int] NOT NULL,
[sampled] [datetime] NOT NULL
)
go
insert into readings
values
(1,'20170101 08:00'),
(1,'20170101 08:02'),
(1,'20170101 08:05'),
(1,'20170101 08:30'),
(1,'20170101 08:31'),
(1,'20170101 08:37'),
(1,'20170101 08:40'),
(1,'20170101 08:41'),
(1,'20170101 09:07'),
(1,'20170101 09:08'),
(1,'20170101 09:09'),
(1,'20170101 09:11')
go
-- Count in the last 10 minutes - example periods 08:31 to 08:40, 09:12 to 09:21
select server,sampled,(select count(*) from readings r2 where r2.server=r1.server and r2.sampled <= r1.sampled and r2.sampled > dateadd(minute,-10,r1.sampled)) as countinlast10minutes
from readings r1
order by server,sampled
go
How can I use a window function to obtain the same result ? I've tried this:
select server,sampled,
count(case when sampled <= r1.sampled and sampled > dateadd(minute,-10,r1.sampled) then 1 else null end) over (partition by server order by sampled rows between unbounded preceding and current row) as countinlast10minutes
-- count(case when currentrow.sampled <= r1.sampled and currentrow.sampled > dateadd(minute,-10,r1.sampled) then 1 else null end) over (partition by server order by sampled rows between unbounded preceding and current row) as countinlast10minutes
from readings r1
order by server,sampled
But the result is just the running count. Any system variable that refers to the current row pointer ? currentrow.sampled ?
This isn't a very pleasing answer but one possibility is to first create a helper table with all the minutes
CREATE TABLE #DateTimes(datetime datetime primary key);
WITH E1(N) AS
(
SELECT 1 FROM (VALUES(1),(1),(1),(1),(1),
(1),(1),(1),(1),(1)) V(N)
) -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b) -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b) -- 1*10^4 or 10,000 rows
, E8(N) AS (SELECT 1 FROM E4 a, E4 b) -- 1*10^8 or 100,000,000 rows
,R(StartRange, EndRange)
AS (SELECT MIN(sampled),
MAX(sampled)
FROM readings)
,N(N)
AS (SELECT ROW_NUMBER()
OVER (
ORDER BY (SELECT NULL)) AS N
FROM E8)
INSERT INTO #DateTimes
SELECT TOP (SELECT 1 + DATEDIFF(MINUTE, StartRange, EndRange) FROM R) DATEADD(MINUTE, N.N - 1, StartRange)
FROM N,
R;
And then with that in place you could use ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
WITH T1 AS
( SELECT Server,
MIN(sampled) AS StartRange,
MAX(sampled) AS EndRange
FROM readings
GROUP BY Server )
SELECT Server,
sampled,
Cnt
FROM T1
CROSS APPLY
( SELECT r.sampled,
COUNT(r.sampled) OVER (ORDER BY N.datetime ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS Cnt
FROM #DateTimes N
LEFT JOIN readings r
ON r.sampled = N.datetime
AND r.server = T1.server
WHERE N.datetime BETWEEN StartRange AND EndRange ) CA
WHERE CA.sampled IS NOT NULL
ORDER BY sampled
The above assumes that there is at most one sample per minute and that all the times are exact minutes. If this isn't true it would need another table expression pre-aggregating by datetimes rounded to the minute.
As far as I know, there is not a simple exact replacement for your subquery using window functions.
Window functions operate on a set of rows and allow you to work with them based on partitions and order.
What you are trying to do isn't the type of partitioning that we can work with in window functions.
To generate the partitions we would need to be able to use window functions in this instance would just result in overly complicated code.
I would suggest cross apply() as an alternative to your subquery.
I am not sure if you meant to restrict your results to within 9 minutes, but with sampled > dateadd(...) that is what is happening in your original subquery.
Here is what a window function could look like based on partitioning your samples into 10 minute windows, along with a cross apply() version.
select
r.server
, r.sampled
, CrossApply = x.CountRecent
, OriginalSubquery = (
select count(*)
from readings s
where s.server=r.server
and s.sampled <= r.sampled
/* doesn't include 10 minutes ago */
and s.sampled > dateadd(minute,-10,r.sampled)
)
, Slices = count(*) over(
/* partition by server, 10 minute slices, not the same thing*/
partition by server, dateadd(minute,datediff(minute,0,sampled)/10*10,0)
order by sampled
)
from readings r
cross apply (
select CountRecent=count(*)
from readings i
where i.server=r.server
/* changed to >= */
and i.sampled >= dateadd(minute,-10,r.sampled)
and i.sampled <= r.sampled
) as x
order by server,sampled
results: http://rextester.com/BMMF46402
+--------+---------------------+------------+------------------+--------+
| server | sampled | CrossApply | OriginalSubquery | Slices |
+--------+---------------------+------------+------------------+--------+
| 1 | 01.01.2017 08:00:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 08:02:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 08:05:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 08:30:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 08:31:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 08:37:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 08:40:00 | 4 | 3 | 1 |
| 1 | 01.01.2017 08:41:00 | 4 | 3 | 2 |
| 1 | 01.01.2017 09:07:00 | 1 | 1 | 1 |
| 1 | 01.01.2017 09:08:00 | 2 | 2 | 2 |
| 1 | 01.01.2017 09:09:00 | 3 | 3 | 3 |
| 1 | 01.01.2017 09:11:00 | 4 | 4 | 1 |
+--------+---------------------+------------+------------------+--------+
Thanks, Martin and SqlZim, for your answers. I'm going to raise a Connect enhancement request for something like %%currentrow that can be used in window aggregates. I'm thinking this would lead to much more simple and natural sql:
select count(case when sampled <= %%currentrow.sampled and sampled > dateadd(minute,-10,%%currentrow.sampled) then 1 else null end) over (...whatever the window is...)
We can already use expressions like this:
select count(case when sampled <= getdate() and sampled > dateadd(minute,-10,getdate()) then 1 else null end) over (...whatever the window is...)
so thinking would be great if we could reference a column that's in the current row.
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.
I am using SQL Server 2008. I have a table AdvanceEntry.
--------------------------------------------------------------------------------
Code | PaidDate | Amount | ReceiveDate | ReceiveAmount
--------------------------------------------------------------------------------
102 | 15-04-2004 | 3000 | 20-04-2004 | 2000
104 | 23-05-2006 | 1000 | NULL | 0.00
104 | 25-05-2005 | 1500 | 12-06-2005 | 500
When any person tack the Loan then Loan amount is stored in the Amount column and date is stored in PaidDate and person code is stored in Code column. When that person gives back the amount then that amount is stored in ReceiveAmount and date is stored in ReceiveDate.
Now I want to create a report like ledger of a specific code.
For example code 102
----------------------------------------------------------------------------
PaidDate / ReceiveDate | Amount | ReceiveAmount | Balance
----------------------------------------------------------------------------
15-04-2004 | 3000 | 0 | 3000
20-04-2004 | 0 | 2000 | 1000
And for code 104
----------------------------------------------------------------------------
PaidDate / ReceiveDate | Amount | ReceiveAmount | Balance
----------------------------------------------------------------------------
23-05-2006 | 1000 | 0 | 1000
25-05-2005 | 1500 | 0 | 2500
12-06-2005 | 0 | 500 | 2000
How can I do this? Please help me.. Thanks
Here's one way of doing it:
with Paid as
(
select Code
, PaidDate
, Amount
from AdvanceEntry
where PaidDate is not null
), Received as
(
select Code
, ReceiveDate
, ReceiveAmount
from AdvanceEntry
where ReceiveDate is not null
), Details as
(
select Code = coalesce(p.Code, r.Code)
, CodeDate = coalesce(p.PaidDate, r.ReceiveDate)
, Amount = sum(p.Amount)
, ReceiveAmount = sum(r.ReceiveAmount)
from Paid p
full join Received r on p.PaidDate = r.ReceiveDate and p.Code = r.Code
group by coalesce(p.Code, r.Code)
, coalesce(p.PaidDate, r.ReceiveDate)
)
select d.Code
, PayReceiveDate = d.CodeDate
, Amount = isnull(d.Amount, 0.0)
, ReceiveAmount = isnull(d.ReceiveAmount, 0.0)
, Balance = isnull(b.Balance, 0.0)
from Details d
outer apply (select Balance = sum(isnull(b.Amount, 0.0) - isnull(b.ReceiveAmount, 0.0))
from Details b where d.Code = b.Code and d.CodeDate >= b.CodeDate) b
order by d.Code, d.CodeDate
SQL Fiddle with demo.
It also looks like you had a slight typo in your data; I've changed it slightly in the fiddle to get your expected results.
Also worth mentioning that if you are only getting one pay/receive action per day per code you can get away without any GROUP BY in the query.
try this (untested):
;with cte as (
select Code, PaidDate as Date, Amount as Dr, 0 as Cr, Amount as Net
from Data where PaidDate is not null
union all
select Code, ReceivedData as Date, 0 as Dr, -ReceivedAmount as Cr, -ReceivedAmount as Net
from Data where ReceivedDate is not null
)
select
t1.*, sum(t2.Net) as Balance
from cte as t1
left join cte as t2 on t2.Code = t1.Code and t2.Date <= t1.Date
group by
t1.Code, t1.Date
having t1.Code = #Code