Link to home
Start Free TrialLog in
Avatar of Jim Horn
Jim HornFlag for United States of America

asked on

To Double Cursor, or Not to Double Cursor...

... that is the question.

I'm working with some flight data at a SQL Server 2008R2 client, and specifically I have a requirement to come up with the ultimate origin (OrigApt) and destination (DestApt) for a given flight.  

But some flights have one or more connections, such as in the below image, where my requirement is to come up with one row where OriginApt = YKM and DestApt=SLC, ignoring all of the connecting flights (i.e. SEA and BOI), which I'll define for now as any leg where the GapInMinutes (i.e. time between landing and next flight taking off) is less than 240 minutes.

User generated image
So .. Does anyone have any ideas on how to pull this off other than a double cursor, with the outer cursor being the PK for the overall flight (not shown in the image), the inner cursor being each leg in the image, and incrimenting a number whenever there is a GapInMinutes > 240?

Thanks in advance.
Jimbo
Avatar of zephyr_hex (Megan)
zephyr_hex (Megan)
Flag of United States of America image

The relationship between the records you've shown is not clear (is there a flightId or something that can be used to relate the legs?).  Also, it's not clear how the final destination is determined (I would have guessed it was the destination of the leg with the largest number, but you've stated it's SLC).

In all my years with SQL, I have NEVER had to use a cursor, so I suspect there is likely a way to get your results without one.  My first guess would be a CTE, but the relationships I've mentioned above need to be explained before I can determine if that's a possibility.  CTE's are ideal for recursive relationships.
SOLUTION
Avatar of slightwv (䄆 Netminder)
slightwv (䄆 Netminder)

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
1. Is every flight round trip like this?
2. Is there ever more than 1 stop > 240 minutes?
Avatar of Jim Horn

ASKER

>The relationship between the records you've shown is not clear
I hid the PK.  The image is a single customer/booking, sorted by OriginDt, which I used to UPDATE LegNumber and GapInMinutes.
Yakima, WA to Salt Lake City, UT had one layover, going back had two.

>Is there a flight number that changes once the plane leaves the destination?
Yes but flight number is not relevant here as in the above example each of the five legs had a different flight number.

>1. Is every flight round trip like this?
No.  I'm dealing with multiple scenarios where I have almost all of them covered, this is just a complex one.
  • Single flight - Good
  • Two legs, there and back - Good
  • Two legs, three different airport codes - Good
  • Only one layover for any given trip - Good, BUT
  • More than one layover for any given trip - This question, not handled yet.

>2. Is there ever more than 1 stop > 240 minutes?
Probably, but management has decided for now that that's the definition to use.
This is probably laughable, but the only way I know of to do it :P

DECLARE @origin1 varchar(50), @dest1 varchar(50), @origin2 varchar(50), @dest2 varchar(50), @leg int
SET @origin1 = (SELECT TOP 1 OriginApt FROM flights ORDER BY OriginDt Asc)
SET @dest1 = (SELECT TOP 1 DestApt FROM flights ORDER BY GapInMinutes Desc, DestDt Asc)
SET @leg = (SELECT TOP 1 LegNumber FROM flights ORDER BY GapInMinutes Desc, DestDt Asc)
SET @origin2 = (SELECT OriginApt FROM flights WHERE LegNumber = @leg + 1)
SET @dest2 = (SELECT TOP 1 DestApt FROM flights ORDER BY LegNumber Desc)
SELECT @origin1 As "Out Origin", @dest1 As "Out Destination", @origin2 AS "Return Origin", @dest2 AS "Return Destination"

Open in new window

SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
All - I have a near-working solution based on zepher_hex's comment in #a41735249, so everyone hang tight while I nail this down and then I'll post the complete answer.  

Also this client is on 2008R2, so I don't have the ability to use LEAD, LAG, and FIRST_VALUE.

TIA  
-Jim
My solution only uses cross apply, but it even gets simpler only making use of the gap>240 rule;

Select f1.bookingid,f1.Origin, f2.Destination
From @flights f1
left join @flights f2 on f2.bookingId = f1.bookingid And f2.Gap>240
Where f1.LegNumber = 1

Open in new window


It has another good side if there are two destinations with a longer stay both are listed as destinations.

Bye, Olaf.
Were you able to come up with a solution?  Or is there something further we can assist with?
An accept is inbound on #a41735249, but I have to put a few fires out before I can dress up my code and post it in this question.  Sorry for the delay.  -Jim
Avatar of slightwv (䄆 Netminder)
slightwv (䄆 Netminder)

I forgot to come back to this.

If #a41735249 produces the correct results with the provided data, take a look at this (Again, Oracle specific but should port to SQL Server).

/*
drop table tab1 purge;
create table tab1(flightid number, legnumber number, originApt char(3), destApt char(3), gapInMinutes number);

insert into tab1 values(47,1,'A','C',80);
insert into tab1 values(47,2,'C','F',2518);
insert into tab1 values(47,3,'F','I',105);
insert into tab1 values(47,4,'I','J',230);
insert into tab1 values(47,5,'J','A',null);

insert into tab1 values(74,1,'A','C',60);
insert into tab1 values(74,2,'C','F',80);
insert into tab1 values(74,3,'F','I',105);
insert into tab1 values(74,4,'I','J',1447);
insert into tab1 values(74,5,'J','A',null);
commit;
*/

select flightid, max(origin) origin, max(dest) dest from (
select flightid, case when legnumber=1 then originapt end origin, case when gapInMinutes > 240 then destapt end dest
from tab1
)
group by flightid
/

Open in new window

SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
I'd propose to accept zephry & netminder's posts, but St. Jimbo is very active on EE so perhaps he can post the finalized code or make the accept decision on this.
The solution is below, which is longer then desired but it works for flights with two 'hops' and ignoring connecting flights.  As soon as we upgrade beyond 2008R2 I hope to revisit this code and rewrite using LEAD and LAG to remove the cursors.

If I remember correctly the existence of multiple 'legs' made CROSS APPLY not useful, but don't quote me on that.

Thanks to all that participated.  Splitting points..

DECLARE @KeyField1_parent varchar(17)
DECLARE @KeyField1 varchar(17) 
Declare @LegNumber int, @LegGroupNumber int, @GapInMinutes int
Declare @i int = 1, @NextParentLeg bit = 0
 
DECLARE cFlight CURSOR FOR   
SELECT DISTINCT l.KeyField1 
FROM temp.leg l
	LEFT JOIN temp.flight f ON l.KeyField1 = f.KeyField1
WHERE f.KeyField1 IS NULL		-- Has not already been accounted for in MatchingRule's 1-3
ORDER BY KeyField1
  
OPEN cFlight  
  
FETCH NEXT FROM cFlight INTO @KeyField1_parent
  
WHILE @@FETCH_STATUS = 0  
BEGIN  

    --SELECT @KeyField1_parent
    
    DECLARE cLeg CURSOR FOR   
    SELECT KeyField1, LegNumber, LegGroupNumber, GapInMinutes 
    FROM temp.leg
    WHERE KeyField1 = @KeyField1_parent
    ORDER BY LegNumber
  
    --SELECT 'Outer' as label, KeyField1, LegNumber, LegGroupNumber, GapInMinutes 
    --FROM temp.leg
    --WHERE KeyField1 = @KeyField1_parent
    --ORDER BY LegNumber
    
    OPEN cLeg  
    FETCH NEXT FROM cLeg INTO @KeyField1, @LegNumber, @LegGroupNumber, @GapInMinutes  
  
	    WHILE (@@FETCH_STATUS = 0)
			begin
	   
			-- SELECT 'Inner' as label, @KeyField1, @LegNumber, @LegGroupNumber, @GapInMinutes  
	   
			IF @NextParentLeg = 1
				begin
				SET @i = @i + 1
				SET @NextParentLeg = 0
				end
				
			IF @GapInMinutes >= 240
				begin 
				SET @NextParentLeg = 1
				end
				
			UPDATE temp.leg 
			SET LegGroupNumber = @i 
			WHERE CURRENT OF cLeg
			
			FETCH NEXT FROM cLeg INTO @KeyField1, @LegNumber, @LegGroupNumber, @GapInMinutes
		  
			END
		
    CLOSE cLeg  
    DEALLOCATE cLeg  
    
    SELECT @i = 1, @NextParentLeg = 0
    
    -- Get the next flight. 
    FETCH NEXT FROM cFlight   
    INTO @KeyField1_parent
    
END   
CLOSE cFlight 
DEALLOCATE cFlight  

--SELECT * 
--FROM temp.leg 
--WHERE KeyField1 = @KeyField1_parent
--ORDEr BY KeyField1, LegNumber

-- 5 - More than two overall legs.  Per Thomas chat pick the first Origin and the Destination with the largest GapInMinutes.
INSERT INTO temp.flight (KeyField1, City1, City2, l.OriginDt, MatchingRule) 
SELECT f.KeyField1, minleg.OriginApt, maxleg.DestApt, maxleg.OriginDt, 5
FROM (SELECT DISTINCT KeyField1 FROM temp.leg WHERE LegGroupNumber > 2) f
	LEFT JOIN (
		SELECT KeyField1, OriginApt
		FROM temp.leg
		WHERE LegNumber = 1) minleg ON f.KeyField1 = minleg.KeyField1
	LEFT JOIN (
		SELECT l.KeyField1, l.DestApt, OriginDt
		FROM temp.leg l
			JOIN (SELECT KeyField1, MAX(GapInMinutes) as MaxGap FROM temp.leg GROUP BY KeyField1) maxgap 
				ON l.KeyField1 = maxgap.KeyField1 AND l.GapInMinutes = maxgap.MaxGap) maxleg ON f.KeyField1 = maxleg.KeyField1


INSERT INTO temp.flight_leg (KeyField1, City1, City2, l.OriginDt, MatchingRule) 
SELECT l.KeyField1, l.OriginApt, l.DestApt, l.OriginDt, f.MatchingRule
FROM temp.leg l 
	JOIN temp.flight f ON f.KeyField1 = l.KeyField1
WHERE f.MatchingRule = 5

-- 6 and 7:  Two overall legs, one or more layovers
IF OBJECT_ID('tempdb..temp.flight_with_layover') IS NOT NULL
	DROP TABLE temp.flight_with_layover

CREATE TABLE temp.flight_with_layover ( 
	KeyField1 varchar(17), 
	OriginApt1 char(3), 
	DestApt1 char(3),
	OriginApt2 char(3), 
	DestApt2 char(3),
	OriginDt1 datetime, 
	OriginDt2 datetime) 

-- When SQL Server us upgraded beyond 2008R2 recode this query using LEAD and LAG. 
;with scope as (
	SELECT DISTINCT l.KeyField1
	FROM temp.leg l
		LEFT JOIN temp.flight f ON l.KeyField1 = f.KeyField1
	WHERE f.KeyField1 IS NULL AND l.LegGroupNumber = 2),
MinMaxLegNumberOne as ( 
 	SELECT l.KeyField1, MIN(LegNumber) as MinLegNumberOne, MAX(LegNumber) as MaxLegNumberOne
	FROM temp.leg l
		JOIN scope s ON l.KeyField1 = s.KeyField1
	WHERE LegGroupNumber = 1
	GROUP BY l.KeyField1), 
MinMaxLegNumberTwo as ( 
 	SELECT l.KeyField1, MIN(LegNumber) as MinLegNumberTwo, MAX(LegNumber) as MaxLegNumberTwo
	FROM temp.leg l
		JOIN scope s ON l.KeyField1 = s.KeyField1
	WHERE LegGroupNumber = 2
	GROUP BY l.KeyField1), 
OriginAptOne as ( 
	SELECT l.KeyField1, l.OriginApt, l.OriginDt as OriginDt1 
	FROM temp.leg l 
		JOIN MinMaxLegNumberOne one ON l.KeyField1 = one.KeyField1 AND l.LegNumber = one.MinLegNumberOne), 
DestAptOne as ( 
	SELECT l.KeyField1, l.DestApt 
	FROM temp.leg l 
		JOIN MinMaxLegNumberOne one ON l.KeyField1 = one.KeyField1 AND l.LegNumber = one.MaxLegNumberOne), 
OriginAptTwo as ( 
	SELECT l.KeyField1, l.OriginApt, l.OriginDt as OriginDt2
	FROM temp.leg l 
		JOIN MinMaxLegNumberTwo one ON l.KeyField1 = one.KeyField1 AND l.LegNumber = one.MinLegNumberTwo), 
DestAptTwo as ( 
	SELECT l.KeyField1, l.DestApt 
	FROM temp.leg l 
		JOIN MinMaxLegNumberTwo one ON l.KeyField1 = one.KeyField1 AND l.LegNumber = one.MaxLegNumberTwo) 
INSERT INTO temp.flight_with_layover (KeyField1, OriginApt1, DestApt1, OriginApt2, DestApt2, OriginDt1, OriginDt2) 
SELECT DISTINCT 
	s.KeyField1,
	o1.OriginApt as OriginApt1, d1.DestApt as DestApt1, o2.OriginApt as OriginApt2, d2.DestApt as DestApt2, 
	o1.OriginDt1, o2.OriginDt2
FROM temp.leg l
	JOIN scope s ON l.KeyField1 = s.KeyField1
	JOIN OriginAptOne o1 ON l.KeyField1 = o1.KeyField1
	JOIN DestAptOne d1 ON l.KeyField1 = d1.KeyField1
	JOIN OriginAptTwo o2 ON l.KeyField1 = o2.KeyField1
	JOIN DestAptTwo d2 ON l.KeyField1 = d2.KeyField1

Open in new window