Query where foreign key column can be NULL - sql

I want to get data if orgid = 2 or if there is no row at all for the uid. orgid is an integer. The closest thing I could think of is to do IS NULL but I'm not getting data for the uid that doesn't have an orgid row. Any idea?
select u.uid,u.fname,u.lname from u
inner join u_org on u.uid = u_org.uid
inner join login on u.uid = login.uid
where u_org.orgid=2 or u_org.orgid is NULL
and login.access != 4;
Basically the OR is if u_org.orgid row doesn't exist.

If there is "no row at all for the uid", and you JOIN like you do, you get no row as result. Use LEFT [OUTER] JOIN instead:
SELECT u.uid, u.fname, u.lname
FROM u
LEFT JOIN u_org o ON u.uid = o.uid
LEFT JOIN login l ON u.uid = l.uid
WHERE (o.orgid = 2 OR o.orgid IS NULL)
AND l.access IS DISTINCT FROM 4;
Also, you need the parenthesis I added because of operator precedence. (AND binds before OR).
I use IS DISTINCT FROM instead of != in the last WHERE condition because, again, login.access might be NULL, which would not qualify.
However, since you only seem to be interested in columns from table u to begin with, this alternative query would be more elegant:
SELECT u.uid, u.fname, u.lname
FROM u
WHERE (u.uid IS NULL OR EXISTS (
SELECT 1
FROM u_org o
WHERE o.uid = u.uid
AND o.orgid = 2
))
AND NOT EXISTS (
SELECT 1
FROM login l
WHERE l.uid = u.uid
AND l.access = 4
);
This alternative has the additional advantage, that you always get one row from u, even if there are multiple rows in u_org or login.

Related

SQL question, regarding removing records in a new table from an existing table

My problem: I have a master (large) table 'Contacts'. I have created a new (smaller) table 'Unsubs'. My goal is to remove all rows in 'Unsubs' from 'Contacts', and have it update the existing 'Contacts' table (so not create a new table). 'user_id' is the common attribute.
I have tried this code below and some similar variations, however the query continues to time out. I attempted a Left join as well but I don't think it meets my requirements.
SELECT *
FROM contacts c
WHERE user_id NOT IN (SELECT user_id
FROM Unsubs u
WHERE u.user_ID = c.user_id)
Expectation - if 'Contacts' is 200k, and 'Unsubs' is 1k, 'Contacts' would result to be 199k (if all user_id's found a match).
You mentioned UPDATE & DELETE but show a SELECT-Query. Here is an option for all of them (yes, LEFT JOIN should work):
SELECT * FROM contacts c
LEFT JOIN Unsubs u ON c.user_id = u.user_id
WHERE u.user_id IS NOT NULL
UPDATE c.myField = u.myField FROM contacts c
LEFT JOIN Unsubs u ON c.user_id = u.user_id
WHERE u.user_id IS NOT NULL
DELETE FROM contacts c
LEFT JOIN Unsubs u ON c.user_id = u.user_id
WHERE u.user_id IS NOT NULL
i prefer not exists
SELECT c.* FROM contacts c
WHERE NOT exists
(SELECT 1 FROM Unsubs u WHERE u.user_ID = c.user_id
)
You seem to want something like this:
update contacts
set is_active = 0
where not exists (select 1 from unsubs u where u.user_id = c.user_id);
Then when you query contacts, you would use something like this:
select c.*
from contacts c
where is_active = 1;

Using same column multiple times in WHERE clause

I have a following table structure.
USERS
PROPERTY_VALUE
PROPERTY_NAME
USER_PROPERTY_MAP
I am trying to retrieve user/s from the users table who have matching properties in property_value table.
A single user can have multiple properties. The example data here has 2 properties for user '1', but there can be more than 2. I want to use all those user properties in the WHERE clause.
This query works if user has a single property but it fails for more than 1 properties:
SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.id IN (SELECT id FROM property_value WHERE value like '101')
AND pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like '102')) and u.user_name = 'user1' and u.city = 'city1'
I understand since the query has pn.id = 1 AND pn.id = 2 it will always fail because pn.id can be either 1 or 2 but not both at the same time. So how can I re-write it to make it work for n number of properties?
In above example data there is only one user with id = 1 that has both matching properties used in the WHERE clause. The query should return a single record with all columns of the USERS table.
To clarify my requirements
I am working on an application that has a users list page on the UI listing all users in the system. This list has information like user id, user name, city etc. - all columns of the in USERS table. Users can have properties as detailed in the database model above.
The users list page also provides functionality to search users based on these properties. When searching for users with 2 properties, 'property1' and 'property2', the page should fetch and display only matching rows. Based on the test data above, only user '1' fits the bill.
A user with 4 properties including 'property1' and 'property2' qualifies. But a user with only one property 'property1' would be excluded due to the missing 'property2'.
This is a case of relational-division. I added the tag.
Indexes
Assuming a PK or UNIQUE constraint on USER_PROPERTY_MAP(property_value_id, user_id) - columns in this order to make my queries fast. Related:
Is a composite index also good for queries on the first field?
You should also have an index on PROPERTY_VALUE(value, property_name_id, id). Again, columns in this order. Add the the last column id only if you get index-only scans out of it.
For a given number of properties
There are many ways to solve it. This should be one of the simplest and fastest for exactly two properties:
SELECT u.*
FROM users u
JOIN user_property_map up1 ON up1.user_id = u.id
JOIN user_property_map up2 USING (user_id)
WHERE up1.property_value_id =
(SELECT id FROM property_value WHERE property_name_id = 1 AND value = '101')
AND up2.property_value_id =
(SELECT id FROM property_value WHERE property_name_id = 2 AND value = '102')
-- AND u.user_name = 'user1' -- more filters?
-- AND u.city = 'city1'
Not visiting table PROPERTY_NAME, since you seem to have resolved property names to IDs already, according to your example query. Else you could add a join to PROPERTY_NAME in each subquery.
We have assembled an arsenal of techniques under this related question:
How to filter SQL results in a has-many-through relation
For an unknown number of properties
#Mike and #Valera have very useful queries in their respective answers. To make this even more dynamic:
WITH input(property_name_id, value) AS (
VALUES -- provide n rows with input parameters here
(1, '101')
, (2, '102')
-- more?
)
SELECT *
FROM users u
JOIN (
SELECT up.user_id AS id
FROM input
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
GROUP BY 1
HAVING count(*) = (SELECT count(*) FROM input)
) sub USING (id);
Only add / remove rows from the VALUES expression. Or remove the WITH clause and the JOIN for no property filters at all.
The problem with this class of queries (counting all partial matches) is performance. My first query is less dynamic, but typically considerably faster. (Just test with EXPLAIN ANALYZE.) Especially for bigger tables and a growing number of properties.
Best of both worlds?
This solution with a recursive CTE should be a good compromise: fast and dynamic:
WITH RECURSIVE input AS (
SELECT count(*) OVER () AS ct
, row_number() OVER () AS rn
, *
FROM (
VALUES -- provide n rows with input parameters here
(1, '101')
, (2, '102')
-- more?
) i (property_name_id, value)
)
, rcte AS (
SELECT i.ct, i.rn, up.user_id AS id
FROM input i
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
WHERE i.rn = 1
UNION ALL
SELECT i.ct, i.rn, up.user_id
FROM rcte r
JOIN input i ON i.rn = r.rn + 1
JOIN property_value pv USING (property_name_id, value)
JOIN user_property_map up ON up.property_value_id = pv.id
AND up.user_id = r.id
)
SELECT u.*
FROM rcte r
JOIN users u USING (id)
WHERE r.ct = r.rn; -- has all matches
dbfiddle here
The manual about recursive CTEs.
The added complexity does not pay for small tables where the additional overhead outweighs any benefit or the difference is negligible to begin with. But it scales much better and is increasingly superior to "counting" techniques with growing tables and a growing number of property filters.
Counting techniques have to visit all rows in user_property_map for all given property filters, while this query (as well as the 1st query) can eliminate irrelevant users early.
Optimizing performance
With current table statistics (reasonable settings, autovacuum running), Postgres has knowledge about "most common values" in each column and will reorder joins in the 1st query to evaluate the most selective property filters first (or at least not the least selective ones). Up to a certain limit: join_collapse_limit. Related:
Postgresql join_collapse_limit and time for query planning
Why does a slight change in the search term slow down the query so much?
This "deus-ex-machina" intervention is not possible with the 3rd query (recursive CTE). To help performance (possibly a lot) you have to place more selective filters first yourself. But even with the worst-case ordering it will still outperform counting queries.
Related:
Check statistics targets in PostgreSQL
Much more gory details:
PostgreSQL partial index unused when created on a table with existing data
More explanation in the manual:
Statistics Used by the Planner
SELECT *
FROM users u
WHERE u.id IN(
select m.user_id
from property_value v
join USER_PROPERTY_MAP m
on v.id=m.property_value_id
where (v.property_name_id, v.value) in( (1, '101'), (2, '102') )
group by m.user_id
having count(*)=2
)
OR
SELECT u.id
FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
WHERE (pv.property_name_id=1 and pv.value='101')
OR (pv.property_name_id=2 and pv.value='102')
GROUP BY u.id
HAVING count(*)=2
No property_name table needed in query if propery_name_id are kown.
If you want just to filter:
SELECT users.*
FROM users
where (
select count(*)
from user_property_map
left join property_value on user_property_map.property_value_id = property_value.id
left join property_name on property_value.property_name_id = property_name.id
where user_property_map.user_id = users.id -- join with users table
and (property_name.name, property_value.value) in (
values ('property1', '101'), ('property2', '102') -- filter properties by name and value
)
) = 2 -- number of properties you filter by
Or, if you need users ordered descending by number of matches, you could do:
select * from (
SELECT users.*, (
select count(*) as property_matches
from user_property_map
left join property_value on user_property_map.property_value_id = property_value.id
left join property_name on property_value.property_name_id = property_name.id
where user_property_map.user_id = users.id -- join with users table
and (property_name.name, property_value.value) in (
values ('property1', '101'), ('property2', '102') -- filter properties by name and value
)
)
FROM users
) t
order by property_matches desc
SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.id IN (SELECT id FROM property_value WHERE value
like '101') )
OR ( pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like
'102'))
OR (...)
OR (...)
You can't do AND because there is no such a case where id is 1 and 2 for the SAME ROW, you specify the where condition for each row!
If you run a simple test, like
SELECT * FROM users where id=1 and id=2
you will get 0 results. To achieve that use
id in (1,2)
or
id=1 or id=2
That query can be optimised more but this is a good start I hope.
you are using AND operator between two pn.id=1 and pn.id=2. then how you getting the answer is between that:
(SELECT id FROM property_value WHERE value like '101') and
(SELECT id FROM property_value WHERE value like '102')
So like above comments , Use or operator.
Update 1:
SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE pn.id in (1,2) AND pv.id IN (SELECT id FROM property_value WHERE value like '101' or value like '102');
If you just want the distinct columns in U, it is:
SELECT DISTINCT u.*
FROM Users u INNER JOIN USER_PROPERTY_MAP upm ON u.id = upm.[user_id]
INNER JOIN PROPERTY_VALUE pv ON upm.property_value_id = pv.id
INNER JOIN PROPERTY_NAME pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.[value] = '101')
OR (pn.id = 2 AND pv.[value] = '102')
Notice I used pv.[value] = instead of the subquery to reacquire id... this is simplification.
If I understand your question correctly I would do it like this.
SELECT u.id, u.user_name, u.city FROM users u
WHERE (SELECT count(*) FROM property_value v, user_property_map m
WHERE m.user_id = u.id AND m.property_value_id = v.id AND v.value IN ('101', '102')) = 2
This should return a list of users that have all the properties listed in the IN clause. The 2 represents the number of properties searched for.
Assuming you want to select all the fields in the USERS table
SELECT u.*
FROM USERS u
INNER JOIN
(
SELECT USERS.id as user_id, COUNT(*) as matching_property_count
FROM USERS
INNER JOIN (
SELECT m.user_id, n.name as property_name, v.value
FROM PROPERTY_NAME n
INNER JOIN PROPERTY_VALUE v ON n.id = v.property_name_id
INNER JOIN USER_PROPERTY_MAP m ON m.property_value_id = v.property_value_id
WHERE (n.id = #property_id_1 AND v.value = #property_value_1) -- Property Condition 1
OR (n.id = #property_id_2 AND v.value = #property_value_2) -- Property Condition 2
OR (n.id = #property_id_3 AND v.value = #property_value_3) -- Property Condition 3
OR (n.id = #property_id_N AND v.value = #property_value_N) -- Property Condition N
) USER_PROPERTIES ON USER_PROPERTIES.user_id = USERS.id
GROUP BY USERS.id
HAVING COUNT(*) = N --N = the number of Property Condition in the WHERE clause
-- Note :
-- Use HAVING COUNT(*) = N if property matches will be "MUST MATCH ALL"
-- Use HAVING COUNT(*) > 0 if property matches will be "MUST MATCH AT LEAST ONE"
) USER_MATCHING_PROPERTY_COUNT ON u.id = USER_MATCHING_PROPERTY_COUNT.user_id

Conditional Statement in SQL defining variables

I have a query like this:
SELECT c.review, u.username, c.date, c.good, c.rate_id, r.rating
FROM comments AS c
LEFT JOIN users AS u
ON c.user_id = u.user_id
LEFT JOIN items as i
ON i.items_id = c.item_id
LEFT JOIN master_cat AS m
ON m.cat_id = i.cat_id
LEFT JOIN ratings as r
ON r.item_id = c.item_id
WHERE i.item = '{$item}' AND c.user_id = r.user_id
ORDER by {$sort};
But this is limiting. Because if that WHERE statement (c.user_id = r.user_id) matches, I JSON print out c.rating. If not, where it prints out in my application and it will skip data that I need.
For example, if it matches a comment to a rating from the same user, it will pair the data. But some comments don't have ratings (you can rate from 1 - 5) associated with them yet. So, instead of skipping the data with a inflexible WHERE clause, I want to make it conditional and print out 0 where later on I can say IF 0, print out "not rated". (this will not be done in SQL but JAVA. In SQL, I just need to store 0 in the MySQL DB.)
I am assuming I will need to drop the WHERE and make part of the SELECT?
LEFT JOIN ratings as r
ON r.item_id = c.item_id AND c.user_id = r.user_id
WHERE i.item = '{$item}'
That condition when placed in the where clause transforms the left join into an inner join

Left Outer Join with subqueries?

----------
User
----------
user_ID(pk)
UserEmail
----------
Project_Account
----------
actno
actname
projno
projname
ProjEmpID
ProjEmpMGRID
Where ProjEmpID,ProjEmpMGRID is the user_id and ProjEmpMGRID can be null.
I need to look up the useremail and display the table project_account. I need to query with actNo which has duplicate values.
My query goes like this:
select projno,projname,actno,actname,
(select u.user_email as project_manager from project_account c left outer join users u
on u.user_id = c.ProjEmpID where actno='some no')as project_manager,
(select u.user_email as program_manager from project_account c left outer join users u
on u.user_id = c.ProjEmpMGRID where actno='someno') as program_manager
from project_account where actno='someno'
The error message I get in Oracle:
ora-01427 single row subquery returns
more than one row
As my subquery returns more than one email id, I get this error. As I said, act no is not unique. I could understand the error, but I couldn't figure out the solution. I am doing a left outer join in a subquery because there might be nulls in prog manager id.
Any help would be appreciated.
The error you are getting is that one of your subqueries (either for project_manager or program_manager) is giving you back more than one ID based on your conditions. This kind of makes sense, since multiple project accounts could have the same "actno" since you haven't specified that as a Primarky Key (pk)
furhter, rather than using subqueries, just join directly to the user tables to find the IDs
select projno,projname,actno,actname,
project_user.user_email as project_manager,
program_user.user_email as program_manager
from project_account
left join User as project_user
on project_account.ProjEmpID = project_user.user_id
left join User as program_user
on project_account.ProjEmpMGRID = program_user.user_id
where actno='someno'
What about something like:
select c.projno, c.projname, c.actno, c.actname, u.user_email as project_manager, us.user_email as program_manager
from project_account c
left outer join users u
on u.user_id = c.ProjEmpID
left outer join users us
on us.user_id = c.ProjEmpMGRID
WHERE actno = 'someno'
This way you aren't running subqueries and returning multiple results and trying to store them as one value.
Why don't you simply use this?
select projno, projname, actno, actname, (select user_email from users where user_id = pa.projempid), (select user_email from users where user_id = pa.projempmgrid)
from project_account pa

joining one table multiple times to other tables

I have three tables:
Table User( userid username)
Table Key( userid keyid)
Table Laptop( userid laptopid)
i want all users who have either a key or a laptop, or both. How do i write the query so that it uses a join between table User and table Key, as well as a join between table User and table Laptop?
The main problem is that in the actual scenario, there are twelve or so table joins, sth like:
" select .. From a left join b on (...), c join d on (..),e,f,g where ...",
and i see that a could be joined to b, and a could also be joined to f. So assuming i can't make the tables a,b, and f appear side-by-side, how do i write the sql query?
You can use multiple joins to combine multiple tables:
select *
from user u
left join key k on u.userid = k.userid
left join laptop l on l.userid = u.userid
A "left join" also finds users which do not have a key or a laptop. If you replace both with "inner join", it would find only users with a laptop and a key.
When a "left join" does not find a row, it will return NULL in its fields. So you can select all users that have either a laptop or a key like this:
select *
from user u
left join key k on u.userid = k.userid
left join laptop l on l.userid = u.userid
where k.userid is not null or l.userid is not null
NULL is special, in that you compare it like "field is not null" instead of "field <> null".
Added after your comment: say you have a table Mouse, that is related to Laptop, but not to User. You can join that like:
select *
from user u
left join laptop l on l.userid = u.userid
left join mouse m on m.laptopid = l.laptopid
If this does not answer your question, you gotta clarify it some more.
select distinct u.userid, u.username
from User u
left outer join Key /* k on u.userid = k.userid */
left outer join Laptop /* l on u.userid = l.userid */
where k.userid is not null or l.userid is not null
EDIT
"The main problem is that in the actual scenario, there are twelve or so table joins, sth like:
" select .. From a left join b on (...), c join d on (..),e,f,g where ...",
and i see that a could be joined to b, and a could also be joined to f. So assuming i can't make the tables a,b, and f appear side-by-side, how do i write the sql query?"
You can have as many left outer joins as required. Join the table with the primary key to the rest of the tables or on any other field where field values of one table should match field values of other table.
eg will explain better than words
select *
from a
left outer join b on a.pk = b.fk -- a pk should match b fk
left outer join c on a.pk = c.fk -- a pk should match c fk
left outer join d on c.pk = d.fk -- c pk should match d fk
and so on
-- // Assuming that a user can have at max 1 items of each type
SELECT u.*
-- // Assuming that a user can have more then 1 items of each type, just add DISTINCT:
-- // SELECT DISTINCT u.*
FROM "User" u
LEFT JOIN "Key" u1 ON u.UserID = u1.UserID
LEFT JOIN "Laptop" u2 ON u.UserID = u2.UserID
LEFT JOIN "Server" u3 ON u.UserID = u3.UserID
-- // ...
WHERE COALESCE(u1.UserID, u2.UserID, u3.UserID /*,...*/) IS NOT NULL
As you described the case, you only wanted to know if someone has a laptop or a key. I would write the query with a subquery rather than a join:
select *
from user
where userid in (select userid from key union select userid from laptop)
The reason for this is that by a join a person with multiple laptops or multiple keys will be listed several times (unless you use distinct). And even you use distinct you end up with a less efficient query (at least on Oracle the query optimizer doesn't appear to be able to create an efficient plan).
[Edited to correct what Rashmi Pandit pointed out.]
Solution one:
SELECT * FROM [User] u
INNER JOIN [Key] k
ON u.userid = k.userid
UNION
SELECT * FROM [User] u
INNER JOIN Laptop l
ON u.userid = l.userid
[...]
Solution two:
SELECT * FROM [User] u
LEFT JOIN [Key] k
ON u.userid = k.userid
LEFT JOIN Laptop l
ON u.userid = l.userid
LEFT JOIN [...]
WHERE k.userid IS NOT NULL
OR l.userid IS NOT NULL
OR [...]
Just a guess, also you could check the execution plan for theses two to see if the UNION one is heavier or vice versa.
SELECT *
FROM User
LEFT JOIN Key ON User.id = Key.user_id
LEFT JOIN Laptop ON User.id = Laptop.user_id
WHERE Key.id IS NOT NULL OR Laptop.id IS NOT NULL