Left join not pulling through nulls where expected - sql

I have a query that looks something like this (I've changed the table names):
select #user_id
, isnull(ur.rule_value, isnull(manr.rule_value, def.rule_value)) [rule_value]
, isnull(urt.name, isnull(manrt.name, def.name)) [rule_type]
from (select #user_id [user_id]
, rule.rule_value
, rule_type.name
from rule
join rule_type on rule_type.rule_type_id = rule.rule_type_id
where rule.user_id = 1) def
join user on user.user_id = def.user_id
join manager man on man.manager_id = user.manager_id
left join rule ur on ur.user_id = user.user_id
left join rule_type urt on urt.rule_type_id = ur.rule_type_id
left join rule manr on manr.manager_id = man.manager_id
left join rule_type manrt on manrt.rule_type_id = manr.rule_type_id
What I'm expecting is that when there isn't a rule for the user or the user's manager, then the default rule should be used. However, I'm only getting a result if the user has a rule.
I've tried left joining everything but to no avail, and the select statement for the def table brings back all the default rules.
What am I doing wrong?
#user_id is a variable.
Update
Example of schema
rule
rule_id user_id manager_id rule_value
1 1 1 27
2 1 1 24
3 1 1 25
4 1 1 44
5 1 1 88
1 2 4 2
2 2 4 23
3 2 4 18
3 NULL 4 19
4 NULL 4 20
5 NULL 4 21
rule_type
rule_id name
1 'Craziness'
2 'Number of legs'
3 'Hair ranking'
4 'Banana preference'
5 'Rule 5'
user
user_id manager_id ... other columns
1 1
2 4
3 4
manager
manager_id ... other columns
1
2
3
4
5
6
So if #user_id is 2 then I would expect the output
2, 2, 'Craziness'
2, 23, 'Number of legs'
2, 18, 'Hair ranking'
2, 20, 'Banana preference'
2, 21, 'Rule 5'
But if #user_id is 3 then I would expect the output
3, 27, 'Craziness'
3, 24, 'Number of legs'
3, 19, 'Hair ranking'
3, 20, 'Banana preference'
3, 21, 'Rule 5'

A regular join is actually a left inner join. What you actually want is an outer join, which fetches the result, even if it cannot join it with the other table.

To answer the question in the title, a LEFT JOIN does NOT act like a REGULAR JOIN. The LEFT JOIN is the same as the LEFT OUTER JOIN, meaning will also return the records in the LEFT table that don't match the ON criteria.

Get rid of this :
left join rule ur on ur.user_id = user.user_id
left join rule_type urt on urt.rule_type_id = ur.rule_type_id
left join rule manr on manr.manager_id = man.manager_id
left join rule_type manrt on manrt.rule_type_id = manr.rule_type_id
and replace it with this :
left join rule ur on ur.user_id = user.user_id or ur.user_id = man.manager_id
left join rule_type urt on urt.rule_type_id = ur.rule_type_id
then change your opening SELECT to :
select #user_id
, isnull(ur.rule_value, def.rule_value) [rule_value]
, isnull(urt.name, def.name) [rule_type]
Essentially what you're doing wrong is that you have one entity (user-manager-default) and you're trying to link it to a different entity (rule) via two different joins. If one join doesn't work but the other does, there's no matching NULL returned for your ISNULL to find. It's a little counter-intuitive.

I've split the query into two parts.
I think because the table structures are more complicated than I put into the original question, this means that CodeByMoonlight's answer isn't quite enough - it was giving me duplicate rows that I couldn't account for.
My solution is as follows (with respect to the details I gave in the question):
create table #user_rules
(
user_id int,
rule_value int,
rule_type varchar(255),
rule_type_id,
)
--Insert default values
insert into #user_rules
select #user_id [user_id]
, rule.rule_value
, rule_type.name
, rule_type.rule_type_id
from rule
join rule_type on rule_type.rule_type_id = rule.rule_type_id
where rule.user_id = 1
--Update table with any available values
update #user_rules
set rule_value = ur.rule_value
from user u
join manager m on u.manager_id = m.manager_id
join rule ur on ur.user_id = u.user_id or (ur.manager_id = man.manager_id and ur.user_id is null)
join rule_type urt on urt.rule_type_id = ur.rule_type_id
where urt.rule_type_id = #urse_rules.rule_type_id
and u.user_id = #user_id

Related

Is there any way to show the records using same query?

I want to list out notices which are not sent. So I tried the query like below. But its showing wrong result. Is there any way to show notices which are not sent using the following query.
SELECT
vtn.*,
vn.id as notice_id,
vn.vnotice_datetime as sent_notice_time
FROM
vtemplates vt
LEFT JOIN vtemplate_notices vtn ON( vtn.vtemplate_id = vt.id)
LEFT JOIN vnotices vn ON(vn.vtemplate_notice_id = vtn.id AND vn.vnotice_datetime IS nULL)
LEFT JOIN violations v ON ( v.vtemplate_id = vt.id)
WHERE
v.id = 1
Records in a violation_notices table are as follows:
--------------------------------------------------------------
id vtemplate_notice_id desc vnotice_datetime created_on
---------------------------------------------------------------
1 1 test1 22/12/2018 05:30 22/12/2018
Expected Result:
id vtemplate_id created_on notice_id sent_notice_time
---------------------------------------------------------------
2 1 23/12/2018 NULL NULL
3 1 24/12/2018 NULL NULL
4 1 24/12/2018 NULL NULL
Actual Result:
id vtemplate_id created_on notice_id sent_notice_time
---------------------------------------------------------------
1 1 22/12/2018 NULL NULL
2 1 23/12/2018 NULL NULL
3 1 24/12/2018 NULL NULL
4 1 24/12/2018 NULL NULL
In actual result, it shows first record (which should not come) for which vnotice_datetime is NOT NULL but still it's showing.
Well, left joins don't remove non matching rows. Shifting the IS NULL check from the ON to the WHERE clause might work.
SELECT vtn.*,
vn.id notice_id,
vn.vnotice_datetime sent_notice_time
FROM vtemplates vt
LEFT JOIN vtemplate_notices vtn
ON vtn.vtemplate_id = vt.id
LEFT JOIN vnotices vn
ON vn.vtemplate_notice_id = vtn.id
LEFT JOIN violations v
ON v.vtemplate_id = vt.id
WHERE v.id = 1
AND vn.vnotice_datetime IS NULL;
You can use NOT EXISTS or test the joined column IS NULL in the WHERE clause
https://dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html
e.g
SELECT * FROM violations
WHERE NOT EXISTS(
SELECT * FROM notifications
WHERE violation_id = violations.id
)
SELECT v.*, n.* FROM violations v
LEFT JOIN notifications n
ON n.violation_id = v.id
WHERE n.violation_id IS NULL

Select Total and If Read

I have the following tables:
Book
Id Title
1 Test
BookPage (BookId corresponds to Id from Book table)
Id BookId Page
1 1 1
1 1 2
BookUserPage (BookPageId corresponds to Id from BookPage table)
UserId BookPageId
1 1
2 2
3 1
My select query is as follows:
SELECT B.[Id], B.[Title], BP.[Id], BP.[BookId], BP.[Page], COUNT(BUP.[BookpageId]) AS Total
FROM [Book] B
LEFT OUTER JOIN [BookPage] BP ON BP.[BookId] = B.[Id]
LEFT OUTER JOIN [BookUserPage] BUP ON BUP.[BookPageId] = BP.[Id]
WHERE B.[Id] = 1
GROUP BY B.[Id], B.[Title], BP.[Id], BP.[BookId], BP.[Page]
The result I get is the following:
Id, Title, Id, BookId, Page, Total
1 Test 1, 1, 1, 2
1 Test 2, 1, 1, 1
I'm trying to modify the query so that it will also tell me which of the 2 pages were read by the user.
I have tried the following:
SELECT B.[Id], B.[Title], BP.[Id], BP.[BookId], BP.[Page], COUNT(BUP.[BookpageId]) AS Total,
CASE WHEN EXISTS (
SELECT BUP2.[UserId]
FROM [PollUserAnswer] BUP2
WHERE BUP2.[UserId] = '98ad813b-cd0e-4a63-b40a-e09ee84f4b96')
THEN 1
ELSE 0
END AS Voted
FROM [Book] B
LEFT OUTER JOIN [BookPage] BP ON BP.[BookId] = B.[Id]
LEFT OUTER JOIN [BookUserPage] BUP ON BUP.[BookPageId] = BP.[Id]
WHERE B.[Id] = 1
GROUP BY B.[Id], B.[Title], BP.[Id], BP.[BookId], BP.[Page]
But the above puts a 1 on both rows of my result. I have also tried adding in the Case statement a condition:
AND BUP2.[BookPageId] = BUP.[PageId]
But that can't work due to group by and i can't list it in the group by as its a subquery.
My Desired Output for user 1 and 3 is this:
Id, Title, Id, BookId, Page, Total, Read
1 Test 1, 1, 1, 2, 1
1 Test 2, 1, 1, 1, 0
My Desired Output for user 2 is this:
Id, Title, Id, BookId, Page, Total, Read
1 Test 1, 1, 1, 2, 0
1 Test 2, 1, 1, 1, 1
Note: Please ignore the fact that there are 2 Id columns in the query output.
I would join to separate subquery to find the total number of users who read a given page. Then, you only need to tag on another left join to BookUserPage to generate the Read column:
SELECT b.Id, b.Title, bp.Id, bp.BookId, bp.Page, bup1.total,
CASE WHEN bup2.UserId IS NULL THEN 0 ELSE 1 END AS [Read]
FROM Book b
LEFT JOIN BookPage bp
ON bp.BookId = b.Id
LEFT JOIN
(
SELECT BookPageId, COUNT(*) AS total
FROM BookUserPage
GROUP BY BookPageId
) bup1
ON bup1.BookPageId = bp.Id
LEFT JOIN BookUserPage bup2
ON bup2.BookPageId = bp.Id AND bup2.UserId = 1
WHERE b.Id = 1;
Demo
This is not generating the results you expect, but it seems logically correct given your actual sample data and the join logic.

record doesn't show up if there's no corresponding data

I have a few tables I'm trying to join however, I'm missing some rows if there's no data, but would like to have them be displayed
tblCountry
sID sCountry
1 Algeria
2 Armenia
3 Belgium
tblRefData
RefID IDnum sID
1 7 1
2 8 2
3 9 3
tblMData
IDnum IDa
7 123
8 123
Here's what my query looks like:
Select tblCountry.sCountry, count(tblMData.Ida) as CountIDa
From tblRefData
inner join tblMData on tblRefData.IDnum = tblMData.IDnum
inner join tblCountry on tblRefData.sID = tblCountry.sID
GroupBy tblCountry.sCountry
What my desired end result is:
sCountry CountIDa
Algeria 1
Armenia 1
Belgium 0
What I'm currently getting is
sCountry CountIDa
Algeria 1
Armenia 1
So if country does not have corresponding data in tblMData, that country does not show up in my result. Any ideas?
You should use left join if the on condition columns values don't match
Select
tblCountry.sCountry,
case
when (count(tblMData.Ida) is null) then 0 end as CountIDa
From
tblRefData
left join
tblMData on tblRefData.IDnum = tblMData.IDnum
left join
tblCountry on tblRefData.sID = tblCountry.sID
Group By
tblCountry.sCountry
You could change the 'INNER JOIN' to a 'LEFT JOIN' to bring back data when there is no corresponding data. Then you could you the ISNULL() function to set the NULL value to a zero.
You want a left join, but have to rearrange the from clause:
Select c.sCountry, count(m.Ida) as CountIDa
From tblCountry c left join
tblRefData r
on r.sID = c.sID left join
tblMData m
on r.IDnum = m.IDnum
GroupBy c.sCountry;
Notice that the table aliases make the query easier to write and to read.

CASE for joining sql tables

I need a help on sql database side. And i have
table 1 : ENTITY_TYPE
entity_type_id entity_name
1 Task
2 Page
3 Project
4 Message
5 User
and table 2 : MESSAGE , that contains message from each entity values like
message_id entity_type owner_tableid message
1 1 12 A message on task level
2 3 14 A message on project level
and I want select these message according to each entity type and details from its owner table using 'owner_tableid' ie a query like....
select * from MESSAGE JOIN
case entity_type when 1 then taskTable
when 2 then pageTable
when 3 then projectTable
when 4 then MessageTable
when 5 then UserTable
Which is best method to solve this issue on single procedure. Any idea ?? Now I am using IF clause for each entity...
You can't parameterise the tables involved in a query (so you can't put a table name in a variable and expect that to be used either).
One way to do it is as a chain of left joins:
select
* /* TODO - Pick columns */
from
MESSAGE m
left join
taskTable tt
on
m.entity_type = 1 and
m.owner_entity_id = tt.id
left join
pageTable pt
on
m.entity_type = 2 and
m.owner_entity_id = pt.id
left join
projectTable prt
on
m.entity_type = 3 and
m.owner_entity_id = prt.id
left join
MessageTable mt
on
m.entity_type = 4 and
m.owner_entity_id = mt.id
left join
UserTable ut
on
m.entity_type = 5 and
m.owner_entity_id = ut.id
If you want values from these tables to appear in a single column in the result, use a COALESCE across all of the values, e.g.
COALESCE(tt.Value,pt.Value,prt.Value,mt.Value,ut.Value) as Value
Use Union Clause with your individual entity_type
SELECT * FROM Message
JOIN pageTable ON ....
WHERE entity_type = 1
UNION ALL
..........
entity_type = 2
UNION ALL
..........
entity_type = 3
Select ...
From Message
Join (
Select 1 As entity_type, id
From taskTable
Union All
Select 2, id
From pageTable
Union All
Select 3, id
From projectTable
Union All
Select 4, id
From messageTable
Union All
Select 5, id
From userTable
) As Z
On Z.entity_type = Message.entity_type
And Z.id = Message.owner_tableid
If you need to return several entity_types details in one query, than UNION might help:
SELECT interesting_columns FROM Message
JOIN pageTable ON (joinPredicate)
WHERE entity_type = 1
UNION ALL
SELECT interesting_columns FROM Message
JOIN pageTable ON (joinPredicate)
WHERE entity_type = 2
-- ...
But if you only need details of certain entity_type than you original solution with IF would be much better.

SELECT Data from multiple tables?

I have 3 tables, with 3 fields all the same. I basically want to select information from each table
For example:
userid = 1
I want to select data from all 3 tables, where userid = 1
I am currently using:
SELECT r.*,
p.*,
l.*
FROM random r
LEFT JOIN pandom p ON r.userid = p.userid
LEFT JOIN landom l ON l.userid = r.userid
WHERE r.userid = '1'
LIMIT 0, 30
But it doesn't seem to work.
with 3 fields all the same
So you mean that you want the same 3 fields from all 3 tables?
SELECT r.col1, r.col2, r.col3
FROM random r
WHERE r.userid = '1'
LIMIT 0, 30
UNION ALL
SELECT p.pcol1, p.pcol_2, p.p3
FROM pandom p
WHERE p.userid = '1'
LIMIT 0, 30
UNION ALL
SELECT l.l1, l.l2, l.l3
FROM landom l
WHERE l.userid = '1'
LIMIT 0, 30
The fields don't have to be named the same, but the same types need to line up in position 1, 2 and 3.
The way the limits work is:
it will attempt to get 30 from random.
If it has 30 already, it won't even look at the other 2 tables
if it has less than 30 from random, it will try to fill up to 30 from pandom and only finally landom
SELECT t1.*, t2.*, t3.*
FROM `random` as t1, `pandom` as t2, `landom` as t3
WHERE t1.`userid`='1' AND t2.`userid`='1' AND t3.`userid`='1'
SELECT * FROM `random`
JOIN `pandom` USING (`userid`)
JOIN `landom` USING (`userid`)
WHERE `userid`='1'