0

Requirements: Have all the customers listed from the first table, then pull any classes that match conditions from the second table and third table, with the third table joined only to the second.

What I've tried:

  1. When I use JOIN on the third table I don't get all the Customers (e.g., "Bobby Black").
  2. When I use LEFT JOIN on the third table I get all the Customers, but then all the classes too (e.g., Jane Doe's "PST" class)!

Simplified Example:

DECLARE @T1_Customers TABLE 
(T1_Customer_id INT, 
 T1_FName VARCHAR(50), 
 T1_LName VARCHAR(50))
INSERT INTO @T1_Customers VALUES 
(1,'John','Darwin'),
(2,'Jane','Doe'),
(3,'Bobby','Black')

DECLARE @T2_Classes TABLE 
(T2_Class_id INT, 
 T2_Customer_id INT, 
 T2_ClassType_id INT, 
 T2_ClassName VARCHAR(50), 
 T2_Status VARCHAR(50))
INSERT INTO @T2_Classes VALUES 
(1,1,1,'Emergency Medical Dispatch v1','Pass'),
(2,1,2,'Emergency Medical Dispatch Instructor','Pass'),
(3,2,3,'Public Safety Telecommunicator','Pass'),
(4,2,1,'Emergency Medical Dispatch v1','Pass'),
(5,2,1,'Emergency Medical Dispatch v2','Fail')

DECLARE @T3_ClassTypes TABLE 
(T3_ClassType_id INT, 
 T3_ClassType VARCHAR(50))
INSERT INTO @T3_ClassTypes VALUES 
(1,'EMD'),
(2,'EMD-I'),
(3,'PST')

--SELECT * FROM @T1_Customers SELECT * FROM @T2_Classes SELECT * FROM @T3_ClassTypes

--FIRST ATTEMPT
SELECT * FROM @T1_Customers
  LEFT JOIN @T2_Classes 
    ON T2_Customer_id = T1_Customer_id 
      AND T2_Status != 'Fail'
  JOIN @T3_ClassTypes 
    ON T3_ClassType_id = T2_ClassType_id
      AND T3_ClassType != 'PST'

--SECOND ATTEMPT    
SELECT * FROM @T1_Customers
  LEFT JOIN @T2_Classes 
    ON T2_Customer_id = T1_Customer_id 
      AND T2_Status != 'Fail'
  LEFT JOIN @T3_ClassTypes 
    ON T3_ClassType_id = T2_ClassType_id
      AND T3_ClassType != 'PST'

Results of Attempts & Desired Results: (T2_ClassName is abbreviated)

FIRST ATTEMPT

 T1_Customer_id T1_FName  T1_LName  T2_Class_id  T2_Customer_id  T2_ClassType_id  T2_ClassName  T2_Status  T3_ClassType_id  T3_ClassType 
 -------------- --------- --------- ------------ --------------- ---------------- ------------- ---------- ---------------- ------------ 
 1              John      Darwin    1            1               1                EMD v1        Pass       1                EMD          
 1              John      Darwin    2            1               2                EMDI          Pass       2                EMD-I        
 2              Jane      Doe       4            2               1                EMD v1        Pass       1                EMD          

SECOND ATTEMPT

 T1_Customer_id T1_FName  T1_LName  T2_Class_id  T2_Customer_id  T2_ClassType_id  T2_ClassName  T2_Status  T3_ClassType_id  T3_ClassType 
 -------------- --------- --------- ------------ --------------- ---------------- ------------- ---------- ---------------- ------------ 
 1              John      Darwin    1            1               1                EMD v1...     Pass       1                EMD          
 1              John      Darwin    2            1               2                EMDI...       Pass       2                EMD-I        
 2              Jane      Doe       3            2               3                PST...        Pass       null             null          
 2              Jane      Doe       4            2               1                EMD v1...     Pass       1                EMD          
 3              Bobby     Black     null         null            null             null          null       null             null         

DESIRED RESULTS

 T1_Customer_id T1_FName  T1_LName  T2_Class_id  T2_Customer_id  T2_ClassType_id  T2_ClassName  T2_Status  T3_ClassType_id  T3_ClassType 
 -------------- --------- --------- ------------ --------------- ---------------- ------------- ---------- ---------------- ------------ 
 1              John      Darwin    1            1               1                EMD v1        Pass       1                EMD          
 1              John      Darwin    2            1               2                EMDI          Pass       2                EMD-I        
 2              Jane      Doe       4            2               1                EMD v1        Pass       1                EMD          
 3              Bobby     Black     null         null            null             null          null       null             null         
philipxy
  • 14,867
  • 6
  • 39
  • 83
DanielT
  • 53
  • 9
  • 1
    Please add a few rows of sample data and the expected result. A left join on T2 will not ensure all rows from T1 will be present in the result set. – The Impaler Dec 05 '22 at 21:18
  • Bobby Black doesn't have any matching class. Does he? – The Impaler Dec 05 '22 at 21:39
  • @TheImpaler - Correct, Bobby Black took no classes matching the criteria, but still needs to be listed as a customer. – DanielT Dec 05 '22 at 21:44
  • Please ask 1 specific researched non-duplicate question. Please either ask about 1 bad definition/query/function with the obligatory [mre] & why you think it should return something else at the 1st subexpression that it doesn't give what you expect, justified by reference to authoritative documentation, or ask about your overall goal giving working parts you can do & ideally a [mre]. But please ask about bad code 1st because misconceptions get in the way of your goal. And bad code doesn't tell us what you wish it would do. [ask] [Help] – philipxy Dec 07 '22 at 10:09
  • Re (re)search success: Please before considering posting: Pin down code issues via [mre]. Read the manual/reference & google any error message & many clear, concise & precise phrasings of your question/problem/goal, with & without your particular names/strings/numbers, 'site:stackoverflow.com' & tags; read many answers. Reflect research in posts. SO/SE search is poor & literal & unusual, read the help. Google re googling/searching, including Q&A at [meta] & [meta.se]. [How much research effort is expected of Stack Overflow users?](https://meta.stackoverflow.com/q/261592/3404097) – philipxy Dec 07 '22 at 10:10
  • @philippxy - I read thru your links. Besides the image, I'm pretty sure I followed all the rules. I even referenced an old post that seems to be related, but it had no accepted answer, which is why I felt providing a more readable case for other newbies to follow may be good, including an accepted answer and follow-up comments w/every "solution" and comment)... Again, I think I followed what you shared. If you have something very specific I could have done better, please let me know exactly what that may be from my post, as I value this resource and appreciate everyone's time... Thank you! – DanielT Dec 08 '22 at 14:41
  • I told you some very specific things that you didn't do. PS If you always use a tab to complete an @ then you won't misspell someone's username. – philipxy Dec 09 '22 at 07:21
  • Debug questions require a [mre]--cut & paste & runnable code including initialization; desired & actual output (including verbatim error messages); tags & versions; clear specification & explanation. For SQL include DDL & tabular initialization code. For debug that includes the least code you can give that is code that you show is OK extended by code that you show is not OK. [ask] [Help] When you get a result you don't expect, pause your overall goal, chop to the 1st subexpression with unexpected result & say what you expected & why, justified by documentation. (Debugging fundamental.) – philipxy Dec 09 '22 at 07:22
  • It is not helpful to say "I researched" or to give a link to something else. Say how you researched & explain how/why anything relevant you found was relevant. Put what is relevant to asking your question in your post & say exactly how it is related, don't expect us to read another page & try to figure out what is relevant & why. Make a post self-contained. Please clarify via edits, not comments. Also: Your specification is not clear. Again: Ask 1 specific researched non-duplicate question & if about debug then about 1 case. Don't just dump 1 or more pieces of bad code and/or your goal. – philipxy Dec 09 '22 at 07:34
  • LEFT JOIN returns INNER JOIN rows UNION ALL unmatched left table rows extended by NULLs. Always know what INNER JOIN you want as part of an OUTER JOIN. After a LEFT JOIN a WHERE, INNER JOIN or HAVING that requires a right [sic] table column to be not NULL removes any rows with introduced NULLs, ie leaves only INNER JOIN rows, ie "turns OUTER JOIN into INNER JOIN". [You have that.](https://stackoverflow.com/a/55111083/3404097) This is a faq. But one must pin down via a [mre] & write many clear, concise & precise phrasings of one's question/problem/goal to search reasonably. – philipxy Dec 09 '22 at 08:51
  • @philipxy, I didn't get the auto fill on your name that last comment for some reason... You've said things I feel I've done. This is why I don't get why I'm failing. I created a completely new set of code to match my much larger case as a "minimal reproducible example," specifically using DECLARE to allow anyone to cut & paste & run w/o having to drop a temp table. From "For SQL include DDL & tabular ini..." I'm not following. I'm no expert, I didn't have code that was OK or documentation to share, which is why I'm here. Not sure how to explain what I don't know or be more specific... – DanielT Dec 09 '22 at 14:02
  • @philipxy... I'll be more self contained and forgo linking other posts that were not answered. I didn't find a duplicate question, unless what [https://stackoverflow.com/questions/55094277/is-it-true-that-using-inner-join-after-any-outer-join-will-essentially-invalidat/55111083#55111083](you shared) was it, but that's not easy to recognize if I don't know the answer. I did make edits for clarity. When you say don't just dump code, I didn't. I created the situation, explained each bit of code and what they did not provide to me, with an example, and what I needed with a mock up of my need... – DanielT Dec 09 '22 at 14:13
  • @philipxy... Some of your "specific things" didn't seem to apply to me, which is why I was confused. I didn't have a situation where I had errors. I did read docs on JOINS, but not the invalidation case you shared. I provided the "question/problem/goal" and "ask(ed) about (my) overall goal giving working parts" and "a minimal reproducible example." I didn't have "bad code" but incomplete code to assist w/my goal... I will try to make any extra research talk more helpful and remove unnecessary info per your edit. I've noted what I understood... Btw, thanks for sharing your answer to my OP. – DanielT Dec 09 '22 at 17:20

5 Answers5

3

Join with an INNER join @T2_Classes to @T3_ClassTypes and then do a LEFT join of @T1_Customers to that resultset:

SELECT * 
FROM @T1_Customers t1
LEFT JOIN (
  SELECT *
  FROM @T2_Classes t2 INNER JOIN @T3_ClassTypes t3
  ON t3.T3_ClassType_id = t2.T2_ClassType_id
  WHERE t2.T2_Status <> 'Fail' AND t3.T3_ClassType <> 'PST'
) t 
ON t.T2_Customer_id = t1.T1_Customer_id;

See the demo.

forpas
  • 160,666
  • 10
  • 38
  • 76
  • Thanks... Curious, is there any reason I need all the aliases ("t1","t2","t3") if every table is uniquely named? I tried this without the aliases, keeping only "t" before the last "ON", and it works. I've only used aliases when using the same table name more than once. – DanielT Dec 06 '22 at 15:31
  • @DanielT are you sure that there will be no column name conflicts? I see that in your sample data you name the columns by applying the prefix `T?_` which denotes the table. Though this seems a good naming convention it is not a common practice. Most developers would name the columns without that prefix. So, the ON clause without aliases: ON ClassType_id = ClassType_id would throw an error like "Ambiguous column name 'ClassType_id'". This is why the use of aliases is always a good practice. – forpas Dec 06 '22 at 16:08
  • Thanks... This db has a unique column prefix for each table, with only one exception, which is not relevant to this project... That said, I presume the only required alias is "t" before ON at the end. Even when that alias is removed from the "t.T2Customer" this code works... I guess the answer to this question would simply be "best practice" and not anything specific to the solution, right? – DanielT Dec 06 '22 at 16:36
  • @DanielT you can get some ideas in these threads: https://stackoverflow.com/questions/198196/when-to-use-sql-table-alias and https://stackoverflow.com/questions/3718737/is-it-always-a-good-practice-to-use-aliases-in-sql-joins-or-nested-queries – forpas Dec 06 '22 at 16:59
  • So, it sounds like if your db is prefixed properly (i.e., tables but mainly columns), aliases are only required in certain circumstances. However, in case someone comes across this post and has a db that is not setup like mine (with prefixes) I will mark this as the accepted answer, even though a number of other similar solutions provided did work (i marked useful), including the OUTER APPLY, which may well be better, but I just haven't used APPLY and I'm not sure if it makes that much difference in my case... Thanks! – DanielT Dec 06 '22 at 17:52
1

You are converting it to an implicit inner join by introducing a where there that filters on T2.

SELECT * FROM @T1_Customers
  LEFT JOIN (select * from @T2_Classes
  inner JOIN @T3_ClassTypes 
    ON T3_ClassType_id = T2_ClassType_id
  where T2_Status != 'Fail' AND T3_ClassType != 'PST') tmp
    ON T2_Customer_id = T1_Customer_id ;

DBFiddle demo

Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
1

This looks like a good case for using an outer apply:

select c.*, cl.*
from customers c
outer apply(
  select cl.Class_Id, cl.ClassName, cl.[Status], ct.ClassType
  from Classes cl
    join ClassTypes ct on ct.ClassType_Id = cl.ClassType_Id
  where cl.Customer_Id = c.Customer_Id 
    and cl.[Status] != 'Fail'
    and ct.ClassType != 'PST'
)cl;

Demo Fiddle

Stu
  • 30,392
  • 6
  • 14
  • 33
  • Thanks... Based on your DemoFiddle, it looks like this works, but I went with a solution that I can manage better and more familiar with as I've never used OUTER APPLY. – DanielT Dec 06 '22 at 15:36
  • 1
    @DanielT - apply() is an often overlooked operator which has been in the product since *SQL Server 2005*, I would recommend becoming familiar with it - it's known as a lateral join and can provide significant performance benefits in many siuations. – Stu Dec 06 '22 at 15:44
1

Add

where T3_ClassType is not null or T2_Customer_id is null

to your 2nd attempt

Kevin
  • 7,162
  • 11
  • 46
  • 70
0
SELECT 
t1.T1_Customer_id,t1.T1_FName,t1.T1_LName,
t2.T2_Class_id,t2.T2_Customer_id,t2.T2_ClassType_id,t2.T2_ClassName,t2.T2_Status,
t3.T3_ClassType_id,t3.T3_ClassType
FROM
@T1_Customers t1
LEFT JOIN @T2_Classes t2
    INNER JOIN @T3_ClassTypes t3 ON t3.T3_ClassType_id = t2.T2_ClassType_id AND t3.T3_ClassType != 'PST'
ON t2.T2_Customer_id = t1.T1_Customer_id AND  t2.T2_Status != 'Fail'

Note the above to nest the INNER inside of the LEFT. The sample includes all columns, but adjust the SELECT as necessary.

Sean
  • 91
  • 4