How can i write this query more efficiently? - sql

This Query works in a loop and thus its performance is too slow.
FUID is provided by a while loop.
SELECT (SELECT TOP 1 AmountPaid
from [xyz].[dbo].AmountReceived
WHERE C.IID = [xyz].[dbo].AmountReceived.IID
order by ReceivingDate asc)
FROM [xyz].[dbo].Customer C
Where C.BuisnessDate >= DATEADD(m,DATEDIFF(m,0,'2015-03-31'),0)
AND C.BuisnessDate <= DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,'2015-03-31')+1,0)) AND C.FUID=16
AND DATEDIFF(M,C.RiskDate,'2015-03-31') <=3
Customer table contains these related columns:
+------------+----+----------+----------+
| IID | FUID |BusinessDate|RiskDate |
+--------+------+------------+----------+
| 22433 | 13 |2013-05-02 |2007-05-23|
| 22443 | 26 |2014-02-18 |2011-09-07|
| 22906 | 32 |2014-12-22 |2015-01-12|
AmountReceived table:
+--------+---------------+-------------+
| IID |AmountPaid |ReceivingDate|
+--------+---------------+-------------+
| 22433 | 13800 |2015-02-02 |
| 22443 | 1290 |2014-12-18 |
| 22906 | 408 |2014-10-22 |

If I understand your question and you only get FUID in your WHILE, you need something like this
;WITH CTE AS
(
SELECT C.FUID,AR.AmountPaid,
ROW_NUMBER()OVER(PARTITION BY C.FUID ORDER BY AR.ReceivingDate ASC) rn
FROM [xyz].[dbo].Customer C
INNER JOIN [xyz].[dbo].AmountReceived AR
ON C.IID = AR.IID
WHERE C.BuisnessDate >= DATEADD(m,DATEDIFF(m,0,'2015-03-31'),0)
AND C.BuisnessDate <= DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,'2015-03-31')+1,0))
AND C.FUID BETWEEN 1 AND 47
AND C.RiskDate >= '2014-12-01'
)
SELECT C.FUID,AR.AmountPaid
FROM CTE
WHERE rn = 1
Also added the suggestion by thepirat000 in comments above to change DATEDIFF(M,C.RiskDate,'2015-03-31') <=3 to C.RiskDate >= '2014-12-01'

Related

How to make sure the sql result is continued range?

I have table like:
id | low_number | high_number
-------------------------------
1 | 12 | 32
-------------------------------
2 | 13 | 33
-------------------------------
3 | 15 | 36
-------------------------------
4 | 33 | 50
-------------------------------
5 | 35 | 52
...
-------------------------------
17 | 52 | 80
I want to get result like:
id | low_number | high_number
-------------------------------
1 | 12 | 32
-------------------------------
4 | 33 | 50
-------------------------------
17 | 52 | 80
that is because the low_number bigger than the pervious row high_number.
How to write sql to get these result? I use postgresql
This seems like a recursive CTE problem. You want to choose the first row (by id) and then choose the next row based on that.
The idea is to cycle through the rows, one at a time. Then when the condition is met, transition to that row. And so on.
As a query, this looks like:
with recursive tt as (
select id, low_number, high_number, row_number() over (order by id) as seqnum
from t
),
cte as (
select id, low_number, high_number, seqnum, true as is_change, id as grouping_id
from tt
where seqnum = 1
union all
select tt.id, tt.low_number, tt.high_number, tt.seqnum, tt.low_number > t.high_number,
(case when tt.low_number > t.high_number then tt.id else cte.grouping_id end)
from cte join
t
on cte.grouping_id = t.id join
tt
on tt.seqnum = cte.seqnum + 1
)
select *
from cte
where is_change;
Here is a db<>fiddle.
Use the window function LAG() to get a value of a previous row, e.g.
WITH j AS (
SELECT
id,low_number,high_number,
LAG(high_number) OVER (ORDER BY id) AS prev_high_number
FROM t)
SELECT id,low_number,high_number FROM j
WHERE low_number > prev_high_number OR prev_high_number IS NULL;
Demo: db<>fiddle

SQL logic to determine unsold inventory and corresponding available dates (Available to sell)

I am looking for advice on how to generate SQL to be used in SQL Server that will show available inventory to sell and the corresponding date that said inventory will be available. I am easily able to determine if we have inventory that is available immediately but can't wrap my head around what logic would be needed to determine future available quantities.
In the below table. The +/- column represents the weekly inbound vs outbound and the quantity available is a rolling SUM OVER PARTITION BY of the +/- column. I was able to get the immediate quantity available through this simple logic:
Case when Min(X.Qty_Available) > 0 Then Min(X.Qty_Available) else 0 END
AS Immediate_available_Qty
Table:
+-------------+---------------+---------------+------+---------------+
| Item Number | Item Name | week_end_date | +/- | Qty_Available |
+-------------+---------------+---------------+------+---------------+
| 123456 | Fidget Widget | 7/13/2019 | 117 | 117 |
| 123456 | Fidget Widget | 7/20/2019 | 49 | 166 |
| 123456 | Fidget Widget | 7/27/2019 | -7 | 159 |
| 123456 | Fidget Widget | 8/3/2019 | -12 | 147 |
| 123456 | Fidget Widget | 8/10/2019 | -1 | 146 |
| 123456 | Fidget Widget | 8/17/2019 | 45 | 191 |
| 123456 | Fidget Widget | 8/24/2019 | -1 | 190 |
| 123456 | Fidget Widget | 8/31/2019 | -1 | 189 |
| 123456 | Fidget Widget | 9/7/2019 | 50 | 239 |
+-------------+---------------+---------------+------+---------------+
My desired results of this query would be as follows:
+-----------+-----+
| Output | Qty |
+-----------+-----+
| 7/13/2019 | 117 |
| 7/20/2019 | 29 |
| 8/17/2019 | 43 |
+-----------+-----+
the second availability is determined by taking the first available quantity of 117 out of each line in Qty_Available column and finding the new minimum. If the new min is Zero, find the next continuously positive string of data (that runs all the way to the end of the data). Repeat for the third_available quantity and then stop.
I was on the thought train of pursuing RCTE logic but don't want to dive into that rabbit hole if there is a better way to tackle this issue and I'm not even sure the RCTE work for this problem?
This should return your expected result:
SELECT Item_Number, Min(week_end_date), Sum("+/-")
FROM
(
SELECT *
-- put a positive value plus all following negative values in the same group
-- using a Cumulative Sum over 0/1
,Sum(CASE WHEN "+/-" > 0 THEN 1 ELSE 0 end)
Over (PARTITION BY Item_Number
ORDER BY week_end_date
ROWS UNBOUNDED PRECEDING) AS grp
FROM my_table
) AS dt
WHERE grp <= 3 -- only the 1st 3 groups
GROUP BY Item_Number, grp
So here's what I came up with. I know this is poor, I didn't want to leave this thread high and dry and maybe I can get more insight on a better path. Please know that I've never had any real training so I don't know what I don't know.
I ended up running this into a temp table and altering the commented out section in table "A". then re-running that into a temp table.
Select
F.Upc,
F.name,
F.Week_end_date as First_Available_Date,
E.Qty_Available_1
From
(
Select Distinct
D.Upc,
D.name,
Case When Min(D.Rolling_Qty_Available) Over ( PARTITION BY D.upc) < 1 then 0 else
Min(D.Rolling_Qty_Available) Over ( PARTITION BY D.upc) END as Qty_Available_1,
Case When Max(D.Look_up_Ref) Over ( PARTITION BY D.upc) = 0 then '-1000' else
Max(D.Look_up_Ref) Over ( PARTITION BY D.upc) END as Look_up_Ref_1
From
(
Select
A.Upc,
A.name,
A.Week_end_Date,
A.Rolling_Qty_Available,
CASE WHEN
C.Max_Row = A.Row_num and A.[Rolling_Qty_Available] >1 THEN 1
ELSE
CASE WHEN
Sum(A.Calc_Row_Thing) OVER (Partition by A.UPC Order by A.Row_Num DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND Current ROW
) = (C.Max_Row - A.Row_num + 1)
THEN
C.Max_Row - A.Row_num + 1
ELSE 0 END
END as Look_up_Ref
FROM (
Select
G.Upc,
G.Name,
G.Week_End_Date,
G.Row_num,
G.Calc_Row_Thing,
G.Rolling_Qty_Available
--CASE When (G.Rolling_Qty_Available -
--isnull(H.Qty_Available_1,0)) > 0 then 1 else - 0 END as
--Calc_Row_Thing,
From [dbo].[ATS_item_detail_USA_vw] as G
--Left Join [dbo].[tmp_ats_usa_qty_1] as H on G.upc = H.upc
) AS A --Need to subtract QTY 1 out of here and below
join (
SELECT
B.upc,
Max(Row_num) AS Max_Row
FROM [dbo].[ATS_item_detail_USA_vw] AS B
GROUP BY B.upc
) as C on A.upc = C.upc
) as D
GROUP BY
D.Upc,
D.name,
D.Rolling_Qty_Available,
D.Look_up_Ref
HAVING Max(D.Look_up_Ref) > 1
) as E
Left join
(
SELECT
A.Upc,
A.name,
A.Week_end_Date,
A.Rolling_Qty_Available,
CASE WHEN
C.Max_Row = A.Row_num and A.[Rolling_Qty_Available] >1 THEN 1
ELSE
CASE WHEN
Sum(A.Calc_Row_Thing) OVER (Partition by A.UPC Order by A.Row_Num DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND Current ROW
) = (C.Max_Row - A.Row_num + 1)
THEN
C.Max_Row - A.Row_num + 1
ELSE 0 END
END as Look_up_Ref
From (
Select
G.Upc,
G.Name,
G.Week_End_Date,
G.Row_num,
G.Calc_Row_Thing,
G.Rolling_Qty_Available
--CASE When (G.Rolling_Qty_Available -
--isnull(H.Qty_Available_1,0)) > 0 then 1 else - 0 END as
--Calc_Row_Thing,
From [dbo].[ATS_item_detail_USA_vw] as G
--Left Join [dbo].[tmp_ats_usa_qty_1] as H on G.upc = H.upc
) as A --subtract qty_1 out the start qty 2 calc
join (
SELECT
B.upc,
Max(Row_num) as Max_Row
FROM [dbo].[ATS_item_detail_USA_vw] as B
GROUP BY B.upc
) AS C ON A.upc = C.upc
) AS F ON E.upc = F.upc and E.Look_up_Ref_1 = F.Look_up_Ref

SQL Server - Insert lines with null values when month doesn't exist

I have a table like this one:
Yr | Mnth | W_ID | X_ID | Y_ID | Z_ID | Purchases | Sales | Returns |
2015 | 10 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2015 | 12 | 1 | 5210 | 1402 | 2 | 12000.00 | etc | etc |
2016 | 1 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2016 | 3 | 1 | 5210 | 1402 | 2 | etc | etc | etc |
2014 | 3 | 9 | 880 | 2 | 7 | etc | etc | etc |
2014 | 12 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 5 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 7 | 9 | 880 | 2 | 7 | etc | etc | etc |
For each combination of (W, X, Y, Z) I would like to insert the months that don't appear in the table and are between the first and last month.
In this example, for combination (W=1, X=5210, Y=1402, Z=2), I would like to have additional rows for 2015/11 and 2016/02, where Purchases, Sales and Returns are NULL. For combination (W=9, X=880, Y=2, Z=7) I would like to have additional rows for months between 2014/4 and 2014/11, 2015/01 and 2015/04, 2016/06.
I hope I have explained myself correctly.
Thank you in advance for any help you can provide.
The process is rather cumbersome in this case, but quite possible. One method uses a recursive CTE. Another uses a numbers table. I'm going to use the latter.
The idea is:
Find the minimum and maximum values for the year/month combination for each set of ids. For this, the values will be turned into months since time 0 using the formula year*12 + month.
Generate a bunch of numbers.
Generate all rows between the two values for each combination of ids.
For each generated row, use arithmetic to re-extract the year and month.
Use left join to bring in the original data.
The query looks like:
with n as (
select row_number() over (order by (select null)) - 1 as n -- start at 0
from master.spt_values
),
minmax as (
select w_id, x_id, y_id, z_id, min(yr*12 + mnth) as minyyyymm,
max(yr*12 + mnth) as maxyyyymm
from t
group by w_id, x_id, y_id, z_id
),
wxyz as (
select minmax.*, minmax.minyyyymm + n.n,
(minmax.minyyyymm + n.n) / 12 as yyyy,
((minmax.minyyyymm + n.n) % 12) + 1 as mm
from minmax join
n
on minmax.minyyyymm + n.n <= minmax.maxyyyymm
)
select wxyz.yyyy, wxyz.mm, wxyz.w_id, wxyz.x_id, wxyz.y_id, wxyz.z_id,
<columns from t here>
from wxyz left join
t
on wxyz.w_id = t.w_id and wxyz.x_id = t.x_id and wxyz.y_id = t.y_id and
wxyz.z_id = t.z_id and wxyz.yyyy = t.yr and wxyz.mm = t.mnth;
Thank you for your help.
Your solution works, but I noticed it is not very good in terms of performance, but meanwhile I have managed to get a solution for my problem.
DECLARE #start_date DATE, #end_date DATE;
SET #start_date = (SELECT MIN(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
SET #end_date = (SELECT MAX(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
DECLARE #tdates TABLE (Period DATE, Yr INT, Mnth INT);
WHILE #start_date <= #end_date
BEGIN
INSERT INTO #tdates(PEriod, Yr, Mnth) VALUES(#start_date, YEAR(#start_date), MONTH(#start_date));
SET #start_date = EOMONTH(DATEADD(mm,1,DATEFROMPARTS(YEAR(#start_date), MONTH(#start_date), 1)));
END
DECLARE #pks TABLE (W_ID NVARCHAR(50), X_ID NVARCHAR(50)
, Y_ID NVARCHAR(50), Z_ID NVARCHAR(50)
, PerMin DATE, PerMax DATE);
INSERT INTO #pks (W_ID, X_ID, Y_ID, Z_ID, PerMin, PerMax)
SELECT W_ID, X_ID, Y_ID, Z_ID
, MIN(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMin
, MAX(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMax
FROM Table1
GROUP BY W_ID, X_ID, Y_ID, Z_ID;
INSERT INTO Table_Output(W_ID, X_ID, Y_ID, Z_ID
, ComprasLiquidas, RTV, DevManuais, ComprasBrutas, Vendas, Stock, ReceitasComerciais)
SELECT TP.DB, TP.Ano, TP.Mes, TP.Supplier_Code, TP.Depart_Code, TP.BizUnit_Code
, TA.ComprasLiquidas, TA.RTV, TA.DevManuais, TA.ComprasBrutas, TA.Vendas, TA.Stock, TA.ReceitasComerciais
FROM
(
SELECT W_ID, X_ID, Y_ID, Z_ID
FROM #tdatas CROSS JOIN #pks
WHERE Period BETWEEN PerMin And PerMax
) AS TP
LEFT JOIN Table_Input AS TA
ON TP.W_ID = TA.W_ID AND TP.X_ID = TA.X_ID AND TP.Y_ID = TA.Y_ID
AND TP.Z_ID = TA.Z_ID
AND TP.Yr = TA.Yr
AND TP.Mnth = TA.Mnth
ORDER BY TP.W_ID, TP.X_ID, TP.Y_ID, TP.Z_ID, TP.Yr, TP.Mnth;
I do the following:
Get the Min and Max date of the entire table - #start_date and #end_date variables;
Create an auxiliary table with all dates between Min and Max - #tdates table;
Get all the combinations of (W_ID, X_ID, Y_ID, Z_ID) along with the min and max dates of that combination - #pks table;
Create the cartesian product between #tdates and #pks, and in the WHERE clause I filter the results between the Min and Max of the combination;
Compute a LEFT JOIN of the cartesian product table with the input data table.

Calculate total time spent by group and one datetime column

I have a workflow application where the workflow is written to the DB as shown below when the status changes. There is no end time as it is a sequence of events. I want to create a query that will group by the WorkFlowID and total the amount of time spent in each. I am not sure how to even begin
My table and data looks like this
+------------+---------------------+
| WorkFlowID | EventTime |
+------------+---------------------+
| 1 | 07/15/2015 12:00 AM |
| 2 | 07/15/2015 12:10 AM |
| 3 | 07/15/2015 12:20 AM |
| 2 | 07/15/2015 12:30 AM |
| 3 | 07/15/2015 12:40 AM |
| 4 | 07/15/2015 12:50 AM |
+------------+---------------------+
My end result should be like:
+------------+-----------------+
| WorkFlowID | TotalTimeInMins |
+------------+-----------------+
| 1 | 10 |
| 2 | 20 |
| 3 | 20 |
| 4 | 10 |
+------------+-----------------+
In SQL Server 2012+, you would just use lead(). There are several ways to approach this in SQL Server 2008. Here is one using `outer apply:
select t.WorkFlowId,
sum(datediff(second, EventTime, nextTime)) / 60.0 as NumMinutes
from (select t.*, t2.EventTime as nextTime
from table t outer apply
(select top 1 t2.*
from table t2
where t2.EventTime > t.EventTime
order by t2.EventTime
) t2
) tt
group by t.WorkFlowId;
The only question is how you get "10" for event 4. There is no following event, so that value doesn't make sense. You can use datediff(second, EventTime coalesce(NextEvent, getdate()) to handle the NULL value.
As an alternative:
;WITH t AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY EventTime) As rn
FROM
yourTable)
SELECT
t1.WorkFlowID,
SUM(DATEDIFF(SECOND, t1.EventTime, ISNULL(t2.EventTime, GETDATE()) / 60) As TotalTimeInMins
FROM t t1
LEFT JOIN t t2
ON t1.rn = t2.rn - 1
The basis of a method that works in all (ok, I don't know about SQL 6.5) editions is to use the group by clause:
SELECT
WorkFlowID
,datediff(mi, min(EventTime), max(EventTime)) TotalTimeInMins
from MyTable
group by WorkFlowID
This does indeed leave the question of how you got 10 minutes with a start time and (presumably) no end time. As written, this query would list the
WorkFlowID with TotalTimeInMins = 0, which seems accurate enough. The following variant would remove all "start-only" items:
SELECT
WorkFlowID
,datediff(mi, min(EventTime), max(EventTime)) TotalTimeInMins
from MyTable
group by WorkFlowID
having count(*) > 1
(The quick explanation: having is to group by as where is to from)

SQL How to combine Timestamps to show a duration?

I have the following query where the data is the location of an ITEM at any given time in a STORAGE_BOX, where the location of the item can be specified further with a delimiter so you could have STORAGE_BOX/SLOT/TRAY/ROW_ID. This is why I do the SUBSTRING for that column.
What I'm trying to do: Create a view where to show what START_DATE and END_DATE a particular item was in a certain storage box.
select at.primary_key as 'ITEM_ID', f.name as 'INSTITUTION', SUBSTRING(sc.location +'/',0, CHARINDEX('/', sc.location + '/'))
as 'STORAGE_BOX', at.db_timestamp as 'TIMESTAMP' from [dbo].AUDIT_TRAIL at
RIGHT JOIN [dbo].BIOMATERIAL bio on at.primary_key = bio.id
LEFT JOIN [dbo].FACILITY f on bio.at_facility_id = f.id
LEFT JOIN [dbo].STORAGE_CONTAINER sc on at.primary_key = sc.id
where at.table_name = 'Biomaterial' AND sc.location IS NOT NULL
So the following output from the above query
+---------+-------------+-------------+------------+
| ITEM_ID | INSTITUTION | STORAGE_BOX | TIMESTAMP |
+---------+-------------+-------------+------------+
| 1 | Building#1 | STORAGE_0 | 2012-03-25 |
| 1 | Building#1 | STORAGE_0 | 2013-12-25 |
| 1 | Building#1 | STORAGE_1 | 2015-03-25 |
| 2 | Building#2 | STORAGE_3 | 2012-03-25 |
| 2 | Building#2 | STORAGE_4 | 2013-03-25 |
| 2 | Building#2 | STORAGE_5 | 2015-03-25 |
+---------+-------------+-------------+------------+
And change it into the below result where the START_DATE is the first timestamp of the new STORAGE_BOX and the END_DATE is the next timestamp of whatever STORAGE_BOX is next or the current timestamp if it is still there.
I have no idea how to compute these fields in the above query to get it to show
+---------+-------------+-------------+------------+---------------------+
| ITEM_ID | INSTITUTION | STORAGE_BOX | START_DATE | END_DATE |
+---------+-------------+-------------+------------+---------------------+
| 1 | Building#1 | STORAGE_0 | 2012-03-25 | 2015-03-25 |
| 1 | Building#1 | STORAGE_1 | 2015-03-25 | {Current_TimeStamp} |
| 2 | Building#2 | STORAGE_3 | 2012-03-25 | 2013-03-25 |
| 2 | Building#2 | STORAGE_4 | 2013-03-25 | 2015-03-25 |
| 2 | Building#2 | STORAGE_5 | 2015-03-25 | {Current_TimeStamp} |
+---------+-------------+-------------+------------+---------------------+
EDIT
I used the answer provided by Gordon Linoff to create the following query with sql server 2008 limitations
with t as (
select at.transaction_uid,at.primary_key as BIOMATERIAL_ID, f.name as INSTITUTION,
at.new_value as FREEZER,
at.db_timestamp as TIMESTAMP
from [dbo].AUDIT_TRAIL at RIGHT JOIN
[dbo].BIOMATERIAL bio
on at.primary_key = bio.id LEFT JOIN
[dbo].FACILITY f
on bio.at_facility_id = f.id
where at.table_name = 'Biomaterial' AND at.column_name = 'container_id.location' AND at.new_value IS NOT NULL
),
t1 as (
select t.*,
row_number() over (partition by BIOMATERIAL_ID, INSTITUTION, FREEZER
order by timestamp) as seqnum
from t
),
t2 as(
select t1.*,
ROW_NUMBER() over (partition by BIOMATERIAL_ID order by seqnum) as seqnum_b
from t1
where t1.seqnum = 1
)
SELECT a.BIOMATERIAL_ID, a.INSTITUTION, a.FREEZER, a.TIMESTAMP as START_DATE,coalesce(b.TIMESTAMP, getdate()) as END_DATE
FROM t2 a left join t2 b on a.BIOMATERIAL_ID = b.BIOMATERIAL_ID AND a.seqnum_b = (b.seqnum_b + 1) order by a.BIOMATERIAL_ID
You can do this using window functions. First use row_number() to get just the first row for each group:
with t as (
select at.primary_key as ITEM_ID, f.name as INSTITUTION,
SUBSTRING(sc.location +'/',0, CHARINDEX('/', sc.location + '/')) as STORAGE_BOX,
at.db_timestamp as TIMESTAMP
from [dbo].AUDIT_TRAIL at RIGHT JOIN
[dbo].BIOMATERIAL bio
on at.primary_key = bio.id LEFT JOIN
[dbo].FACILITY f
on bio.at_facility_id = f.id LEFT JOIN
[dbo].STORAGE_CONTAINER sc
on at.primary_key = sc.id
where at.table_name = 'Biomaterial' AND sc.location IS NOT NULL
),
t1 as (
select t.*,
row_number() over (partition by ITEM_ID, INSTITUTION, STORAGE_BOX
order by timestamp) as seqnum
from t
),
t2 as (
select t1.*, lead(timestamp) over (partition by item_id, institution order by timestamp) as next_timestamp
from t1
where seqnum = 1
)
select t2.ITEM_ID, t2.INSTITUTION, t2.STORAGE_BOX,
t2.timstamp as START_DATE,
coalesce(t2.next_timestamp, getdate()) as END_DATE
from t2 ;
The first CTE is your query. The second enumerates the rows for each item, institution, and storage box to eliminate duplicates. This appears to be the logic for your query, although if a storage box is used twice for the same item/location somewhat more complicated logic may be necessary.
The third CTE, t2 gets the next timestamp. And the final query applies the logic.
This assumes SQL Server 2012+ (based on your syntax I'm assuming SQL Server). You can do something similar with outer apply in earlier versions.