How to do a SQL self join with nulls - sql

I am using MS SQL 2008. My table looks like this:
| Name | Code | Amt |
| ----- | ---- | ---- |
| April | A | 1.23 |
| Barry | A | 2.34 |
| Barry | B | 3.45 |
| Cliff | A | 4.56 |
| Cliff | B | 5.67 |
| Cliff | C | 6.78 |
I need the output to be this:
| Name | Code_A | Code_B | Code_C |
| ----- | ------ | ------ | ------ |
| April | 1.23 | NULL | NULL |
| Barry | 2.34 | 3.45 | NULL |
| Cliff | 4.56 | 5.67 | 6.78 |
The NULLs can be zero.
With a self join I am able to get Cliff, but unable to get Barry and April because i'm using something like this which only outputs if all three conditions are available.
SELECT a.Name, a.Amt Code_A, b.Amt Code_B, c.Amt Code_C
FROM Table_1 as c INNER JOIN
Table_1 AS b ON c.Name = b.Name INNER JOIN
Table_1 AS a ON b.Name = a.Name
WHERE (a.Code = 'A') AND (b.Code = 'B') AND (c.Code = 'C')

Instead of JOINs, I think a PIVOT is more appropriate here:
SELECT
Name,
[A] AS Code_A,
[B] AS Code_B,
[C] AS Code_C
FROM (
SELECT Name, Code, Amount
FROM Table_1
) t
PIVOT (
SUM(Amount)
FOR Code IN ([A], [B], [C])
) AS pvt

A completely sql engine agnostic way is:
select names.Name,
(select sum(a2.Amt) from amounts a2
where a2.Name = names.Name
and a2.Code = 'A') as AmtA,
(select sum(a3.Amt) from amounts a3
where a3.Name = names.Name
and a3.Code = 'B') as AmtB,
(select sum(a4.Amt) from amounts a4
where a4.Name = names.Name
and Code = 'C') as AmtC
from (select distinct Name from amounts) as names
Where you select the unique set of names, and then sum up amounts for each particular code in place. This is more intended to be instructional as to how SQL works.
In practice, I wouldn't actually use this in your case -- PIVOT is going to be much more efficient for any engine that supports it. As shown here: http://sqlfiddle.com/#!3/7cb0a/5

Related

selecting specific fields during runtime from table

How do we select fields dynamically?
I've got a table Table1:
+----------+-------+----+
| Table1Id | x | y |
+==========+=======+====+
| 52 | alex | aa |
+----------+-------+----+
| 43 | liza | aa |
+----------+-------+----+
| 21 | harry | bb |
+----------+-------+----+
| 21 | harry | bb |
+----------+-------+----+
I'd like to join on this Table2:
+----------+----------+--------+------+
| Table2Id | Table1Id | aa | bb |
+==========+==========+========+======+
| 1 | 52 | red | tall |
+----------+----------+--------+------+
| 2 | 43 | blue | thin |
+----------+----------+--------+------+
| 3 | 21 | orange | fat |
+----------+----------+--------+------+
The result I'm looking for is:
+-------+-------+----+----------+
| xyzid | x | y | NewField |
+=======+=======+====+==========+
| 52 | alex | aa | red |
+-------+-------+----+----------+
| 43 | liza | aa | blue |
+-------+-------+----+----------+
| 21 | harry | bb | fat |
+-------+-------+----+----------+
As you can see, Table1 has data in the y column the exact field name to grab from Table2.
How do select specific fields from a table, where those fields are actually stored as data in another table?
Just another option which would be a bit more dynamic
No need for the CASE.
table2 could have any number of columns.
No need to convert columns to string or a common datatype.
BUT ... I suspect a bit less performant than The Impalar's answer.
I should note that this is for 2017+. Oddly enough, 2016 requires a "string literal" for JSON_VALUE or JSON_QUERY.
Example or dbFiddle
Select Distinct
xyzid = A.Table1Id
,A.x
,A.y
,NewField = JSON_VALUE(B.JS,'$.'+A.Y)
From Table1 A
Join ( Select [Table1Id]
,JS=(Select B1.* For JSON Path,Without_Array_Wrapper)
From Table2 B1 ) B
on A.[Table1Id]=B.[Table1Id]
Results
UPDATE - 2016 Version
Select xyzid = A.Table1Id
,A.x
,A.y
,NewField = (select max([Value]) from OpenJSON(B.JS) where [key]=A.Y collate database_default)
From Table1 A
Join ( Select [Table1Id]
,JS=(Select B1.* For JSON Path,Without_Array_Wrapper )
From Table2 B1 ) B
on A.[Table1Id]=B.[Table1Id]
2nd 2016 Approach -- Perhaps slightly more performant dbFiddle
Select xyzid = A.Table1Id
,A.x
,A.y
,NewField = B.Value
From Table1 A
Join (
Select [Table1Id]
,[Key]
,Value
From Table2 B1
Cross Apply OpenJson((Select B1.* For JSON Path,Without_Array_Wrapper ) )
) B
on A.[Table1Id]=B.[Table1Id]
and A.Y = B.[Key] collate database_default
You can select columns dynamically using CASE, as in:
select
a.Table1Id as xyzid,
a.x,
a.y,
case
when a.y = 'aa' then b.aa
when a.y = 'bb' then b.bb
end as NewField
from (select distinct * from table1) a
join table2 b on b.Table1Id = a.Table1Id
Result:
xyzid x y newfield
------ ------ --- --------
52 alex aa red
43 liza aa blue
21 harry bb fat
See running example at DB Fiddle.

Join & Group By - Column is invalid in the select list

I'm running a query to join 2 tables and then group the rows by 2 fields and select the rows with the min ID from these groups and I get an error. The joined table looks like:
+--------+---------+-----------------+-----------+-----------+
| ID | CODE | NAME | VRACHAR01 | VRACHAR02 |
+--------+---------+-----------------+-----------+-----------+
| 290861 | 1110896 | PRODUCT NAME XX | 001 | 706 |
| 290864 | 1110899 | PRODUCT NAME XX | 001 | 706 |
| 290865 | 1110900 | PRODUCT NAME XX | 003 | 721 |
| 290870 | 1110905 | PRODUCT NAME XX | 004 | 743 |
| 290871 | 1110906 | PRODUCT NAME XX | 004 | 743 |
| 290878 | 1110913 | PRODUCT NAME XX | 006 | 806 |
| 290879 | 1110914 | PRODUCT NAME XX | 007 | 807 |
| 290908 | 1110943 | PRODUCT NAME XX | 008 | 815 |
+--------+---------+-----------------+-----------+-----------+
If I run the script below to group the results by the last 2 fields I get an error:
Column 'A.CODE' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
SELECT
min(A.ID),
A.CODE,
A.NAME,
B.VARCHAR01,
B.VARCHAR02
FROM
PRODUCTS A
INNER JOIN
EXTRAS B
ON
A.ID = B.ID
WHERE
A.COMPANY = 1002
AND
A.TYPE = 50
GROUP BY
B.VARCHAR01,
B.VARCHAR02
Any help is appreciated.
If you want the min id, then you don't need other columns:
SELECT MIN(P.ID), E.VARCHAR01, E.VARCHAR02
FROM PRODUCTS P INNER JOIN
EXTRAS E
ON P.ID = E.ID
WHERE P.COMPANY = 1002 AND P.TYPE = 50
GROUP BY E.VARCHAR01, E.VARCHAR02;
Note that this replaces the meaningless table aliases with abbreviations for the table names.
If you want the entire row, then you can use window functions:
SELECT PE.*
FROM (SELECT P.*, E.VARCHAR01, E.VARCHAR02,
ROW_NUMBER() OVER (PARTITION BY E.VARCHAR01, E.VARCHAR02 ORDER BY P.ID ASC) as seqnum
FROM PRODUCTS P INNER JOIN
EXTRAS E
ON P.ID = E.ID
WHERE P.COMPANY = 1002 AND P.TYPE = 50
) PE
WHERE seqnum = 1;

Join three tables based on one key, putting data into same column

I have three tables that I am trying to join together to check that the proper data matches. I have table A which is a list of all accounts that a commission was paid on and what that commission amount was. I have Table B and Table C which are two tables that have commission calculations in it. The goal is to compare Table A to Table and to Table C and pulling back the amounts from both tables to ensure a match. The part I am struggling with is, Table A has all the accounts that are the base population. Table B has some and Table C as some. An account will be in either Table B or C, but never in both. I want to pull the payment from Table A, and then verify to the payment in Table B or C(whichever it occurs) and the same with commission. I then am doing a case when that compares the two fields and tells me if it matches are not.
+---------+---------+-----+------+
| Table A | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 12345 | ABCD | 100 | 10 |
| 23456 | OLPOL | 25 | 2 |
| 45678 | LKJHG | 200 | 15 |
| 96385 | LKJ67 | 250 | 26 |
+---------+---------+-----+------+
+---------+---------+-----+------+
| Table B | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 12345 | ABCD | 100 | 8 |
| 45678 | LKJHG | 200 | 15 |
+---------+---------+-----+------+
+---------+---------+-----+------+
| Table C | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 23456 | OLPOL | 25 | 2 |
| 96385 | LKJ67 | 250 | 32 |
+---------+---------+-----+------+
I am trying to get my results to show up in a columns called pay_ver and comm_verf, and it would populate with the data from either Table B or C based on which it matched with. I am hoping to have to output look like so....
+---------+---------+-----+----------+------+-----------+---------+
| Output | | | | | | |
+---------+---------+-----+----------+------+-----------+---------+
| Account | Uniq_ID | Pay | Pay_verf | comm | comm_Verf | Matched |
| 12345 | ABCD | 100 | 100 | 10 | 8 | No |
| 23456 | OLPOL | 25 | 25 | 2 | 2 | Yes |
| 45678 | LKJHG | 200 | 200 | 15 | 15 | Yes |
| 96385 | LKJ67 | 250 | 250 | 26 | 32 | No |
+---------+---------+-----+----------+------+-----------+---------+
This is the code I have used to join Table A to B, and Table A to C but I have done this in two separate queries giving me two outputs. I would like to be able to do this in one, so I only have one output.
select a.account, a.uniq_id, a.pay, b.pay as pay_verf, a.comm, b.comm as comm_verf,
CASE WHEN a.comm = b.comm THEN 'MATCHED'
ELSE 'UNMATCHED'
END as Matched
from tblA a
left join tblB b
on a.account = b.account
and a.uniq_id = b.uniq_id;
I can not just figure out how to also get it to join to Table C without adding an extra column.
You can do:
select
account, uniq_id, pay,
pay_total as pay_verf,
comm,
comm - comm_total as comm_verf,
case when comm = comm_total then 'Yes' else 'No' end as matched
from (
select
a.account, a.uniq_id, a.pay, a.comm,
coalesce(b.pay, 0) + coalesce(c.pay, 0) as pay_total,
coalesce(b.comm, 0) + coalesce(c.comm, 0) as comm_total
from table_a a
left join table_b b on a.account = b.account
left join table_c c on a.account = c.account
) x
You are very close. Just need to add one more join and an addition WHEN to your case statement. This should act like an if elseif else logic. So it checks if a.comm = b.comm and then checks a.comm = c.comm. If neither match if will set to unmatched. This works well because you stated the ID can't be in both B and C.
select a.account, a.uniq_id, a.pay, b.pay as pay_verf, a.comm, b.comm as comm_verf,
CASE WHEN a.comm = b.comm THEN 'MATCHED'
WHEN a.comm = c.comm THEN 'MATCHED'
ELSE 'UNMATCHED'
END as Matched
from tblA a
left join tblB b
on a.account = b.account
and a.uniq_id = b.uniq_id;
left join tblB c
on a.account = c.account
and a.uniq_id = c.uniq_id;
Yet another option could be something like
SELECT a.account, a.uniq_id, a.pay, bc.pay as pay_verf, a.comm, bc.comm as comm_verf
FROM a left join (
SELECT * from b
UNION ALL
SELECT * from c
) bc on (a.account = bc.account and a.uniq_id = bc.uniq_id)

Access Queries comparing two tables

I have two tables in Access, Table A and Table B:
Table MasterLockInsNew:
+----+-------+----------+
| ID | Value | Date |
+----+-------+----------+
| 1 | 123 | 12/02/13 |
| 2 | 1231 | 11/02/13 |
| 4 | 1265 | 16/02/13 |
+----+-------+----------+
Table InitialPolData:
+----+-------+----------+---+
| ID | Value | Date |Type
+----+-------+----------+---+
| 1 | 123 | 12/02/13 | x |
| 2 | 1231 | 11/02/13 | x |
| 3 | 1238 | 10/02/13 | y |
| 4 | 1265 | 16/02/13 | a |
| 7 | 7649 | 18/02/13 | z |
+----+-------+----------+---+
All I want are the rows from table B for IDs not contained in A. My current code looks like this:
SELECT Distinct InitialPolData.*
FROM InitialPolData
WHERE InitialPolData.ID NOT IN (SELECT Distinct InitialPolData.ID
from InitialPolData INNER JOIN
MasterLockInsNew
ON InitialPolData.ID=MasterLockInsNew.ID);
But whenever I run this in Access it crashes!! The tables are fairly large but I don't think this is the reason.
Can anyone help?
Thanks
or try a left outer join:
SELECT b.*
FROM InitialPolData b left outer join
MasterLockInsNew a on
b.id = a.id
where
a.id is null
Simple subquery will do.
select * from InitialPolData
where id not in (
select id from MasterLockInsNew
);
Try using NOT EXISTS:
SELECT Distinct i.*
FROM InitialPolData AS i
WHERE NOT EXISTS (SELECT 1
FROM MasterLockInsNew AS m
WHERE m.ID = i.ID)

Using SWITCH() to split data from a column into distinct columns, with associated data in reach row

I'm not quite sure how to properly phrase the question, but I am basically trying to develop an SQL query that SELECTs information from this table:
-------------------
| id | Val | Date |
|----|-----|------|
| 1 | A | 10/9 |
| 1 | B | 3/14 |
| 2 | A | 1/6 |
| 3 | A | 4/4 |
| 4 | B | 7/12 |
| 5 | A | 8/6 |
-------------------
And produces a table that looks like this:
------------------------------------------------
| id | Val_1 | Val_1_Date | Val_2 | Val_2_Date |
|----|-------|------------|-------|-------------
| 1 | A | 10/9 | B | 3/14 |
| 2 | A | 1/6 | | |
| 3 | A | 4/4 | | |
| 4 | | | B | 7/12 |
| 5 | A | 8/6 | | |
------------------------------------------------
I have already begun and developed the query to pull out the values in the Val fields into distinct columns:
SELECT * FROM
(
SELECT id, MAX(SWITCH( val='A', 'A')) as Val_1,
MAX(SWITCH( val='B', 'B')) as Val_2
FROM table1 GROUP BY id
)a
WHERE Val_1 IS NULL OR Val_2 IS NULL;
How would I expand on this to pull out their associated dates?
(I am using SWITCH() instead of CASE WHEN because I am using a driver similar to that of MS Access.)
Thanks!
I think following should work:
select id, SWITCH( val='A', 'A') as Val_1, SWITCH( val='A', Date) as Val_1_Date, SWITCH( val='B', 'B') as Val_2, SWITCH( val='B', Date) as Val_2_Date FROM table1 GROUP BY id
I do not prefer switches, so here is a query that does what you want without switches. This also answers your previous question.
Select distinct table1.ID, tableA.Val as Val_1, tableA.Date as Val_1_Date,
tableB.Val as Val_2, tableB.Date as Val_2_Date
FROM table1 left outer join
table1 as tableA on table1.id = tableA.id and tableA.Val = 'A' left outer join
table1 as tableB on table1.id = tableB.id and tableB.Val = 'B'
You can use ISNULL if that is preferred. This works because the first tables selects a distinct column of ID's, and the two joins get the A and B values. When creating selects using this method, make sure that you use tableA.Val = 'A' in the join conditions, and not in the where clause. Having tableA.Val = 'A' in the where clause will filter out all NULL's.