joining tables with not exists query [duplicate] - mysql

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Mysql: Perform of NOT EXISTS. Is it possible to improve permofance?
Is there a better/optimal way to do it. Should I use exists instead of join? Or two separate queries? And what about temporary tables, as I was reading about those but uncertain.
Getting members email from a group. Checking that they have not received a item yet.
SELECT m.email,g.id
FROM group g
LEFT JOIN members m
ON g.mid = m.id
AND g.gid='1'
WHERE NOT EXISTS
( SELECT id
FROM items AS i
WHERE i.mid=m.id
AND i.item_id='5'
)

Here's the same thing written as a JOIN:
SELECT m.email, g.id
From members m
JOIN group g ON g.mid = m.id AND g.gid = '1'
LEFT JOIN items i ON i.mid = m.id AND i.item_id = '5'
WHERE i.id IS NULL
Use the following compound indexes:
group (mid, gid)
items (mid, item_id)
I reversed the LEFT JOIN on members and group because it seems like you're returning members, not groups, and I changed the LEFT JOIN into an INNER JOIN since you only want members from that group.
I think this one might read better:
SELECT m.email, g.id
From members m
JOIN group g ON g.mid = m.id
LEFT JOIN items i ON i.mid = m.id AND i.item_id = 5
WHERE g.gid = 1
AND i.id IS NULL
You might be wondering if we can move the i.item_id = 5 part to the WHERE clause also. You can't because there are no rows where i.id IS NULL and i.item_id = 5. You must do the join first and then eliminate the NULL rows in the WHERE clause.

I don't believe a temporary table is necessary. We'd really only go that route if we can't get acceptable performance.
From your query, we gather your schema looks like this:
group (id INT PK, gid INT, mid INT)
items (id INT PK, item_id INT, mid INT)
members (id INT PK, email VARCHAR)
It looks like your group table is really a "membership" table, which resolves/implements a many-to-many relationship between a group and a person. (That is, a person can be a member of zero, one or more groups; a group can have zero, or or more persons as members.)
You are using a LEFT JOIN between group and members. This will return a row for group (returning group.id) when there are no matching members, with a NULL for members.email (which may be what you want). But if you only want to return email addresses, then this can be changed to an INNER JOIN.
The NOT EXISTS predicate can be replaced with an OUTER JOIN and a test for a NULL value returned from the JOINED table. If the group.gid and/or items.item_id columns are numeric datatype, then you can remove the quotes from around the integer literals in the predicates.
Here is an alternative which will return an equivalent resultset, and may perform better:
SELECT m.email
, g.id
FROM members m
JOIN group g ON g.mid = m.id AND g.gid = 1
LEFT
JOIN items i ON i.mid = m.id AND i.item_id = 5
WHERE i.id IS NULL
ADDENDUM:
TEST CASE (provided in comment on selected answer) demonstrates difference in result set between queries with the predicate items.item_id = 5 in the ON clause and in the WHERE clause. (Moving this predicate to the WHERE clause messes with the anti-join.)
CREATE TABLE `group` (`id` INT PRIMARY KEY, `gid` INT, `mid` INT);
CREATE TABLE `items` (`id` INT PRIMARY KEY, `item_id` INT, `mid` INT);
CREATE TABLE `members` (`id` INT PRIMARY KEY, `email` VARCHAR(40));
INSERT INTO `group` VALUES (1,1,1), (2,1,2);
INSERT INTO `items` VALUES (1,5,1);
INSERT INTO `members` VALUES (1,'one#m.com'),(2,'two#m.com');

Related

What Am I Missing? MySQL Left Join Most Newest Entry From 2nd Table

I need a fresh pair of eyes on this. I have two tables, one of which has users and the second which contains login records, multiple records for each user. What I'm trying to do is select all entries from the first table, and the most recent record from the second table, e.g., a list of all users but only show the most recent activity. Both tables have auto increment in the ID column.
My code currently is thus:
SELECT u.user_id, u.name, u.email, r.rid, r.user_id
FROM users AS u
LEFT JOIN login_records AS r ON r.user_id = u.user_id
WHERE
r.rid = (
SELECT MAX( rid )
FROM login_records
WHERE user_id = u.user_id
)
I've scoured answers to similar questions on SO and tried all of them, but results have been either returning nothing or only getting odd results (not necessarily the newest one). ID in both tables is auto-increment, so I thought it should be a relatively simple matter to get the only or highest ID for a particular user, but it either returns nothing or a completely different selection each time.
It's my first time using JOIN - do I have the wrong JOIN? Do I need to ORDER or GROUP things differently?
Thanks for your help. It's got to be something simple, since Danny Coulombe's answer appearing here seems to work for other users.
You will need a subquery I believe:
https://www.db-fiddle.com/f/2wudMDVxReYJz4FEyG19Va/0
CREATE TABLE users (
user_id INT UNSIGNED NOT NULL
AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE users_logins (
user_login_id INT UNSIGNED NOT NULL
AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL
);
INSERT INTO users SELECT 1;
INSERT INTO users SELECT 2;
INSERT INTO users_logins SELECT 1,1;
INSERT INTO users_logins SELECT 2,1;
INSERT INTO users_logins SELECT 3,1;
INSERT INTO users_logins SELECT 4,1;
INSERT INTO users_logins SELECT 5,2;
INSERT INTO users_logins SELECT 6,2;
And the query:
SELECT
u.user_id, ul.latest_login_id
FROM users u
LEFT JOIN
(
SELECT user_id, MAX(user_login_id) latest_login_id
FROM users_logins
GROUP BY user_id
) ul ON u.user_id = ul.user_id
You have to ORDER BY with what column you want to display by desc, for example ORDER BY last_login DESC.
Change the last_login column with the column you want to order, but you must first declare the last_login column after SELECT.
How about replacing all rid in where clause and corrolated subquery by record_id?
SELECT u.user_id, u.name, u.email, r.rid, r.record_id, r.user_id
FROM test_users AS u
LEFT JOIN test_login_records AS r ON r.user_id = u.user_id
WHERE
(r.record_id = (
SELECT MAX(record_id)
FROM test_login_records
WHERE user_id = u.user_id
) OR r.record_id is null);
Test here

MySQL join on whichever column is not null

I have a table with a bunch of columns, but we only need to look at two of them. I'm trying to join another table on this table, but all we know about these two columns is that one will be null and the other won't:
client_id | note_id
The main table wants to join client_id (if not null) on clients.id OR note_id on notes.id if clients.id is null.
This will work for you. This is very basic query I wrote. Make changes if required.
SELECT * FROM YOUR_TABLE t
LEFT OUTER JOIN clients c ON t.client_id = c.id
LEFT OUTER JOIN notes n ON t.note_id = n.id
WHERE c.id IS NOT NULL OR n.id IS NOT NULL
Assuming there are 3 tables involved (the main table that contains client_id and note_id columns, clients table, and notes table), you can use a query such as this:
(select *
from mainTable inner join clients on mainTable.client_id = clients.id)
union
(select *
from mainTable inner join notes on mainTable.note_id = notes.id
where mainTable.client_id is NULL);
The above query contains 2 queries where each query will output rows where the joining column is not null. The results are then combined using union.
You can use coalesce in the join on clause. See demo here:
http://sqlfiddle.com/#!9/99911/2. If client id is null then use note id to join table1 and table2.
Select t1.client_id, t1.note_id,t2.client_id, t2.note_id
From table1 t1
Join table2 t2
on coalesce(t1.client_id, t1.note_id) =coalesce(t2.client_id, t2.note_id)

How do I query multiple tables using inner join?

Given the table creation SQL and insertion SQL, How will I generate a query to display the information in a a particular way?
CREATE TABLE cities (
id serial NOT NULL UNIQUE PRIMARY KEY,
iname varchar(100) NOT NULL UNIQUE
)
CREATE TABLE suburbs (
id serial NOT NULL UNIQUE PRIMARY KEY,
icity integer REFERENCES cities (id),
iname varchar(100) NOT NULL UNIQUE
)
CREATE TABLE type (
id serial NOT NULL UNIQUE PRIMARY KEY,
iname varchar(100) NOT NULL UNIQUE
)
CREATE TABLE sale (
id serial NOT NULL UNIQUE PRIMARY KEY,
iname varchar(100) NOT NULL UNIQUE
)
CREATE TABLE estate (
id serial NOT NULL UNIQUE PRIMARY KEY,
icity integer REFERENCES cities (id),
isuburb integer REFERENCES suburbs (id),
itype integer REFERENCES type (id),
isale integer REFERENCES sale (id),
idescription text,
itimestamp timestamp DEFAULT CURRENT_TIMESTAMP
)
INSERT INTO cities (iname) VALUES ('Johannesburg');
INSERT INTO suburbs (icity, iname) VALUES (1, 'Westbury');
INSERT INTO type (iname) VALUES ('Room');
INSERT INTO sale (iname) VALUES ('Rent');
INSERT INTO estate (icity, isuburb, itype, isale, idescription) VALUES (1, 1, 1, 1, 'A Nice place in a real bad neighbor hood');
Now I want the numerical values that are in the table estate to be displayed by the string values that they represent.
E.G
1 Johannesburg, Westbury, Room, Rent, Description
What will the SQL Query be for this, I am more concerned in using postgreSQL.
you cany try like
select * from table1
inner join table2 on tabl1.pk = table2.FK
inner join table3 on tabl1.pk = table.FK
Final
select table2.iname,table3.iname,table4.iname,table1.idescription
from estate as table1
inner join sale as table2 on table1.isale = table2.id
inner join type as table3 on table1.itype = table3.id
inner join suburbs as table3 on table1.isuburb = table3.id
inner join cities as table4 on table1.icity = table4.id
If you want to get info about joins have look to below image
Ans at : How do I decide when to use right joins/left joins or inner joins Or how to determine which table is on which side?
First, they must have some sort of common field. Let's assume the common field between them is called <tablename>_ID; the way you do it is as follows:
select A.colx, A.coly, A.colz, B.colx, B.colw, c.cold
from A inner join B on A.ID=B.A_ID
inner join C on C.A_ID=A.ID
select suburbs.icity
, cities.iname
, suburbs.iname
, type.iname
, sale.iname
, estate.idescription
from estate
inner join suburbs on suburbs.id = estate.isuburb
inner join cities on cities.id = estate.icity
inner join type on type.id = estate.itype
inner join sale on sale.id = estate.isale
In general, you are looking for a join..
select ct.iname as CityName,sb.name as SuburbName,et.*
from estate et
join cities ct on ct.id=et.icity
join suburbs sb on sb.id=et.isuburb
etc...
You could do it this way :
SELECT *
FROM A
INNER JOIN B
ON B.id = A.b
INNER JOIN C
ON C.id = A.c
Check this example as per ur ques.
SELECT A.*,B.*,C.*
FROM A , B, C
WHERE C.c = B.b
AND B.b = A.a
-> select * from estate
-> inner join cities on estate.icity=cities.id
-> inner join suburbs on estate.isuburb=suburbs.id
-> inner join type on estate.itype=type.id
-> inner join sale on estate.isale=sale.id;

MYSQL JOIN with unique result rows

Say I have 2 tables, one called categories and one called cat_pages.
The categories table has columns ID, title and timestamp. For example:
CREATE TABLE categories (
id INT UNSIGNED PRIMARY KEY,
title VARCHAR(32),
`timestamp` TIMESTAMP,
INDEX (title)
) Engine=InnoDB;
The cat_pages has 2 columns, cat_id and page_id:
CREATE TABLE cat_pages (
cat_id INT UNSIGNED
REFERENCES categories (id)
ON DELETE CASCADE ON UPDATE CASCADE,
page_id INT UNSIGNED
REFERENCES pages (id)
ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE INDEX (cat_id, page_id),
INDEX (page_id, cat_id),
) Engine=InnoDB;
I'm trying to join the categories table with the cat_pages table on the ID, such that
Only categories with id's in the category_pages table are retrieved and
Each category is only displayed once in the resultset
The query:
SELECT * FROM categories as c
LEFT JOIN cat_pages as p ON c.id = p.cat_id
produces a result set that has the categories repeated multiple times (as there are multiple matches in the cat_pages table. What do I need so that each category is only shown once, and not at all if there are no matches in the cat_pages table?
If you don't want categories that aren't in cat_pages, don't use a left join; use an inner join. A left join includes every row from the left table, even if there isn't a matching row in the right table (the missing fields are given NULL values). A right join is similar, but includes all rows from the right table. An outer join includes all rows from the left and right tables, joining rows that have matches and joining rows without matches with NULL values. An inner join, by contrast, only includes matching rows. To put it another way, the intersection of left and right joins is an inner join; their union is an outer join. Jeff Atwood posted some nice Venn diagrams describing joins, though it should be noted that the sets in the circles aren't properly the left and right tables, but rather the results of the left and right joins of the left and right tables.
To get distinct rows, you can use a DISTINCT modifier:
SELECT DISTINCT c.*
FROM categories AS c
INNER JOIN cat_pages AS cp ON c.id = cp.cat_id
As for SELECT * ..., see "What is the reason not to use select *?"
Another approach to getting distinct rows would be to use an EXISTS clause or IN operator, but the join is likely more performant (though only an EXPLAIN would tell you for certain). Just make sure you have appropriate indices set.
Why don't you use an Inner Join?
SELECT * FROM categories as c INNER JOIN cat_pages as p ON c.id = p.cat_id
Or
SELECT * FROM categories as c LEFT JOIN cat_pages as p ON c.id = p.cat_id WHERE p.cat_id IS NOT NULL
Left Join selects all on the left table and the matches on the right table.

Select in a many-to-many relationship in MySQL

I have two tables in a MySQL database, Locations and Tags, and a third table LocationsTagsAssoc which associates the two tables and treats them as a many-to-many relationship.
Table structure is as follows:
Locations
---------
ID int (Primary Key)
Name varchar(128)
LocationsTagsAssoc
------------------
ID int (Primary Key)
LocationID int (Foreign Key)
TagID int (Foreign Key)
Tags
----
ID int (Primary Key)
Name varchar(128)
So each location can be tagged with multiple tagwords, and each tagword can be tagged to multiple locations.
What I want to do is select only Locations which are tagged with all of the tag names supplied. For example:
I want all locations which are tagged with both "trees" and "swings". Location "Park" should be selected, but location "Forest" should not.
Any insight would be appreciated. Thanks!
There are two ways to do this. I prefer the first way, which is to self-join for each tag:
SELECT l.*
FROM Locations l
JOIN LocationsTagsAssoc a1 ON a1.LocationID = l.ID
JOIN Tags t1 ON a1.TagID = t1.ID AND t1.Name = ?
JOIN LocationsTagsAssoc a2 ON a2.LocationID = l.ID
JOIN Tags t2 ON a2.TagID = t2.ID AND t2.Name = ?
JOIN LocationsTagsAssoc a3 ON a3.LocationID = l.ID
JOIN Tags t3 ON a3.TagID = t3.ID AND t3.Name = ?;
The other way also works, but using GROUP BY in MySQL tends to incur a temporary table and performance is slow:
SELECT l.*
FROM Locations l
JOIN LocationsTagsAssoc a ON a.LocationID = l.ID
JOIN Tags t ON a.TagID = t.ID
WHERE t.Name IN (?, ?, ?)
GROUP BY l.ID
HAVING COUNT(*) = 3;
Re comment from #Erikoenig:
If you want to make sure there are no extra tags, you can do it this way:
SELECT l.*
FROM Locations l
JOIN LocationsTagsAssoc a ON a.LocationID = l.ID
JOIN Tags t ON a.TagID = t.ID
GROUP BY l.ID
HAVING COUNT(*) = 3 AND SUM(t.Name IN (?, ?, ?)) = 3;
Taking out the WHERE clause allows other tags to be counted, if there are any. So the COUNT() may be greater than 3.
Or if the count is exactly three tags, but some of these three are not the correct tags, then the SUM() condition in the HAVING clause makes sure that all three tags you want are present in the group.
You need locations where there doesn't exist a given tag that doesn't appear in the LocationsTagsAssoc table with the location.
You can specify the given tags with IN () as in the following, or by joining onto another table containing them.
I.e.
SELECT l.*
FROM Locations AS l
WHERE NOT EXISTS (
SELECT NULL FROM Tags AS t
WHERE NOT EXISTS (
SELECT NULL FROM LocationsTagsAssoc AS lt
WHERE lt.LocationId = l.ID
AND lt.TagID = t.ID
)
AND t.ID IN (1, 2, 3,...)
)