MySQL join two tables without NOT IN - mysql

Two tables users, relationships in my db.
CREATE TABLE users(
id int primary key auto_increment,
nickname varchar(20),
is_active TINYINT
)
CREATE TABLE relationships(
id int primary key auto_increment,
user_id int,
target_user_id int,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(target_user_id) REFERENCES users(id)
)
mysql> select * from users;
+----+----------+-----------+
| id | nickname | is_active |
+----+----------+-----------+
| 1 | hide | 1 |
| 2 | john | 1 |
| 3 | ben | 0 |
| 4 | test | 1 |
| 5 | gogo | 1 |
+----+----------+-----------+
mysql> select * from relationships;
+----+---------+----------------+
| id | user_id | target_user_id |
+----+---------+----------------+
| 1 | 1 | 2 |
| 2 | 1 | 4 |
+----+---------+----------------+
I have to extract users.id with certain condition.
I will explain in case of users.id = 1
users.is_active=1
user who does not have relationships via relationships table. you know that in current relationships table, user_id = 1 has 2 rows that target_user_id = 2 and 4. So query result does not contain user_id = 2 and 4.
Using NOT IN, it is pretty simple.
SELECT id FROM users WHERE is_active=1 AND id NOT IN(SELECT target_user_id FROM relationships WHERE user_id=1)
RESULT : 1, 5
Note that there is huge rows in users and relationships.
If I using NOT IN with subquery, it will occur performance issue.
So I think I have to join with foreign key but I don't know how to make query exactly.
Any suggestion, appreciate.
Thanks.

TRY THIS: I am sure LEFT JOIN with IS NULL approach will definitely work for you
SELECT u.id
FROM users u
LEFT JOIN relationships r ON r.target_user_id = u.id
AND r.user_id = 1
WHERE u.is_active=1 AND r.target_user_id IS NULL

Nothing wrong with your query. MySQL should be able to use your index.
However, you can also use left join:
SELECT
users.id
FROM
users
LEFT JOIN relationships ON (
users.id = relationships.target_user_id
/*
-- un-comment if filtering by user is needed
AND relationships.user_id = 1
*/
)
WHERE
users.is_active=1
AND relationships.target_user_id IS NULL
-- addition filtering can be here
UPDATE:
If you filtering by user, you can try to add composite index (user_id, target_user_id) to relationships table (columns should be in this order)

Related

Hibernate query to get existing conversations between 2 users

I have 2 tables to denote a conversation, I think it is a pretty standard format.
CREATE TABLE conversation (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(40) DEFAULT '',
last_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(id)
) ENGINE = InnoDB;
CREATE TABLE conversation_user (
user_id INT NOT NULL,
conversation_id INT NOT NULL,
conversation_is_visible BOOLEAN NOT NULL DEFAULT TRUE,
unread BOOLEAN NOT NULL DEFAULT TRUE,
PRIMARY KEY (user_id, conversation_id),
CONSTRAINT fk_conversation_user_user_id FOREIGN KEY (user_id) REFERENCES user (id),
CONSTRAINT fk_conversation_user_conversation_id FOREIGN KEY (conversation_id) REFERENCES conversation (id)
) ENGINE = InnoDB;
I want to be able to check if a conversation exists between 2 users (I don't care about starting a new conversation for more than 2 users if it already exists).
I can't seem to be able to write a query to select the conversation with just the 2 given users in it, and no more.
I then want to convert the query into a hibernate query.
For example if i had the following data:
select * from conversation;
+----+------------------------+---------------------+
| id | name | last_modified |
+----+------------------------+---------------------+
| 1 | test conversation 1 | 2017-02-25 15:39:01 |
| 2 | test conversation 2 | 2017-02-26 16:29:14 |
+----+------------------------+---------------------+
select * from conversation_user;
+---------+-----------------+-------------------------+--------+
| user_id | conversation_id | conversation_is_visible | unread |
+---------+-----------------+-------------------------+--------+
| 1 | 1 | 1 | 0 |
| 1 | 2 | 1 | 0 |
| 2 | 1 | 1 | 1 |
| 2 | 2 | 1 | 0 |
| 3 | 2 | 1 | 0 |
+---------+-----------------+-------------------------+--------+
The query should when searching for a conversation between user 1 and user 2 only have conversation 1 show up, as conversation 2 has user 3 in it aswell.
I've tried the following query, but that shows up both conversations, so not sure how to restrict it to conversations just containing those users...
SELECT * FROM conversation
LEFT JOIN conversation_user ON conversation.id = conversation_user.conversation_id
WHERE user_id IN (1,2);
I could use this query in hibernate and then check the set of conversation users in the conversation to make sure the conversation only contains the 2 given users, but was wondering if there was a more efficient approach
Try this
http://sqlfiddle.com/#!9/587e4/5
SELECT t1.user_id,
t1.conversation_id,
t1.conversation_is_visible,
t1.unread
FROM conversation_user t1
LEFT OUTER JOIN
(SELECT user_id,conversation_id
FROM conversation_user WHERE user_id IN (1,2)
) t2 ON t1.user_id=t2.user_id
AND t1.conversation_id=t2.conversation_id
WHERE t2.conversation_id IS NOT NULL;
If you just want conversation_id then use distinct t1.conversation_id in select clause.
Based on Utsav's answer, the following query got the results i was after.
SELECT t1.user_id, t1.conversation_id, t1.conversation_is_visible, t1.unread
FROM conversation_user t1
LEFT OUTER JOIN
(SELECT *
FROM conversation_user
GROUP BY conversation_id HAVING COUNT(*) = 2
) t2
ON t1.user_id = t2.user_id AND t1.conversation_id=t2.conversation_id
WHERE t1.user_id IN (1,2) AND t2.conversation_id IS NOT NULL;
still not sure how to convert this to a query in hibernate but its a start

How can I efficiently store a 2-way "like" system similar to Tinder?

On Tinder, when 2 members like each other, they are a "match" and are able to communicate. If only one member likes another, then it's not a match.
I'm trying to store this "Like" system in MySQL but can't figure out the best way to do it that's efficient. This is my setup right now.
mysql> desc likes_likes;
+--------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+----------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| from_user_id | int(11) | NO | MUL | NULL | |
| to_user_id | int(11) | NO | MUL | NULL | |
| value | int(11) | NO | | NULL | |
| created_at | datetime | NO | | NULL | |
| updated_at | datetime | YES | | NULL | |
+--------------+----------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
To find my matches, I would query something like...
SELECT to_user_id FROM likes_likes WHERE from_user_id = my_id AND value = 1 AND .... I don't know how to join the same table from here.
How do I perform the query on this table? If it's not efficient, what's a better structure to store this model?
1 is like, 0 is not like. Those are the only 2 values.
SELECT A.from_user_id AS userA, B.from_user_id AS userB
FROM likes_likes A
JOIN likes_likes B
ON A.from_user_id = B.to_user_id
AND A.to_user_id = B.from_user_id
AND A.id <> B.id
WHERE A.value = 1
AND B.value = 1
To find matches you can use a regular join with alias:
SELECT l1.from_user_id user1, l2.from_user_id user2
FROM likes_likes l1
INNER JOIN likes_likes l2 ON
l2.from_user_id = l1.to_user_id AND
l1.to_user_id = l2.from_user_id AND
l1.value = 1 AND l2.value = 1
The first condition checks whether the person user1 has liked or not liked user2 and that user2 has liked at least one other person.
The second condition completes the check so that we now have two persons who have expressed an opinion about each other.
The last two checks make sure that they both like each other :)
Here's a way using group by least(),greatest() to get each unique pair of users into a group and then checking if there are 2 rows per group
select least(from_user_id,to_user_id), greatest(from_user_id,to_user_id)
from likes_likes
where value = 1
-- and my_id in (from_user_id,to_user_id)
group by least(from_user_id,to_user_id), greatest(from_user_id,to_user_id)
having count(*) = 2
If it's possible to have multiple likes from the same user to another user (i.e. user 'A' likes user 'B' twice) then use having count(distinct from_user_id) = 2
Do you actually need value? If there is no row there is no like. From this query you should get 1 for a match and 0 for no mutual match.
SELECT
COUNT(*)
FROM
likes_like i_like_you
JOIN likes_like you_like_me ON i_like_you.to_user_id = you_like_me.from_user_id
WHERE
i_like_you.from_user_id = #my_id
AND you_like_me.from_user_id = #your_id
Is there any reason for id? It seems like the pair (from_user_id, to_user_id) should be UNIQUE, hence could be the 'natural' PRIMARY KEY.
I have yet to see any good argument for needing value.
So the table has shrunk to
CREATE TABLE likes_likes (
from_user_id ...,
to_user_id ...,
created_at ...,
updated_at ...,
PRIMARY KEY(from_user_id, to_user_id), -- serves as the necessary INDEX.
) ENGINE=InnoDB;
SELECT A.from_user_id AS userA,
B.from_user_id AS userB
FROM likes_likes A
JOIN likes_likes B
ON A.from_user_id = B.to_user_id
AND A.to_user_id = B.from_user_id
(I'm assuming you disallow a person liking himself.)

Multi join one to many

Trades
id |Trade_name |
1 | trade1 |
2 | trade2 |
3 | trade3 |
4 | trade4 |
Users
Name | Primary_id(FK to trade.id) | secondary_id (FK to trade.id) |
John | 1 | 2 |
Alex | 3 | 4 |
This is my current SQL which joins trades.t1 to primary & secondary.id:
select
`users`.`name` ,
`t1`.`trade_name` AS `Primary_trade`,
`t2`.`trade_name` AS `Secondary_trade`,
FROM `users`
right JOIN `trades` `t1` On (`t1`.`trade_id` = `users`.`primary_trade_id`)
right JOIN `trades` `t2` on (`t2`.`trade_id` = `users`.`secondary_trade_id`)
My question is, how do I identify which trades are not used for users both as primary or secondary. I want to see record where a trade does not exist in both primary or secondary column so I can perform housekeeping.
Thanking you all in advance for your help.
If you need only the trades rows
SELECT t.*
FROM trades t
WHERE NOT EXISTS ( SELECT 'u'
FROM Users u
WHERE u.Primary_id = t.id
OR u.Secondary_id = t.id
)
I think this should work for you:
SELECT * FROM trades WHERE id NOT IN (SELECT Primary_id FROM Users) AND id NOT IN (SELECT Secondary_id FROM Users)
It selects the rows which are not in either primary_id nor secondary_id

Update MySQL table with another table's data

I read this and this, but I need to make a GROUP BY query to set another table.
logintable is like this:
id | name | login_date
------------------------
1 | michael | 2013-01-04
2 | michael | 2013-01-08
3 | mary | 2013-01-11
4 | john | 2013-01-15
5 | michael | 2013-01-19
6 | mary | 2013-01-22
7 | john | 2013-01-26
I make a query like this:
SELECT * FROM logintable GROUP BY name ORDER BY id ASC
This gives me first login date of the users:
1 | michael | 2013-01-04
3 | mary | 2013-01-11
4 | john | 2013-01-15
I have another table called userstable like this:
id | name | last_seen_date | first_login_date
------------------------------------------------
1 | michael | 2013-02-02 |
2 | john | 2013-02-04 |
3 | mary | 2013-02-16 |
I need to update userstable's first_login_date column, with the first result. How can I do this ?
(I have 75k records in logintable and 10k records in userstable)
UPDATE userstable a
INNER JOIN
(
SELECT name, min(login_date) min_date
FROM logintable
GROUP BY name
) b ON a.name = b.Name
SET a.first_login_table = b.min_date
for faster performance, you need to add an INDEX on column Name for both tables. This will prevent from performing full table scan which is very slow in large databases.
Adding INDEX on table userstable:
ALTER TABLE usersTable ADD INDEX (Name);
for referential integrity, make table logintable dependent on table userstable by defining FOREIGN KEY constraint:
ALTER TABLE loginTable ADD CONSTRAINT tb_fk
FOREIGN KEY (Name) REFRENCES usersTable (Name)
UPDATE userstable AS u
INNER JOIN
(
SELECT MIN(login_date) AS MinLoginDate, Name
FROM logintable
GROUP BY name
) AS l ON u.name = l.name
SET u.first_login_date = l.MinLoginDate
I don't know MySql...but in SQL Server you would write something like this:
UPDATE userstable set first_login_date = (SELECT MIN(login_date) FROM logintable where name = userstable.name)
UPDATE userstable a
INNER JOIN
(
SELECT name, min(login_date) min_date
FROM logintable
GROUP BY name
) b ON a.name = b.Name
SET a.first_login_table = b.min_date
This will definitely show your output.

How to select the lowest value from selection

I'm using MySQL 5.1
table users:
id | exp
---------
5 | 5
4 | 9
table levels:
id|min_exp
----------
1 | 0
2 | 5
3 | 8
How to select user with his levels.id?
I need to join table users and levels.
This must be valid:
users.exp >= levels.min_exp
and min_exp should be lowest as it can be.
expected output:
users.id|users.exp|levels.id|levels.min_exp
5 | 5 | 2 | 5
4 | 9 | 3 | 8
If there are no gaps in level.id, you could get better performance by joining levels twice and avoiding aggregates:
select users.id, users.exp, current_level.id, current_level.min_exp
from levels current_level
join levels next_level on next_level.id = current_level.id + 1
join users on users.exp >= current_level.min_exp
and users.exp < next_level.min_exp
If you need the min/max experience bracket frequently, I'd recommend making the current_level/next_level join a view.
Edit: It just occurred to me that this fails for users at maximum level. Depending on how you want to handle that (do users stop gaining XP at max or continue gaining it without increasing level?), you could add a dummy record above max level in levels or make the next_level join an outer join.
You want the min level that the user can see:
select
u.id as UserId,
MIN(l.id) as LevelId
from
Users u
inner join Levels l on
u.exp >= l.min_exp
group by
u.Id
If you want it just for a specific user:
select
u.id as UserId,
MIN(l.id) as LevelId
from
Users u
inner join Levels l on
u.exp >= l.min_exp
where
u.id = 5
group by
u.Id
The question is phrased unclearly, but from the context, it looks like you are looking for the highest level that a person has achieved given their current exp and the min_exp.
Since you know the id of the user you are looking for, the query can be
select users.id as uid,users.exp,levels.id as levelid from users,levels where users.id = 4 and levels.min_exp <= users.exp order by levelid desc limit 1
if you make sure you have proper indexes on both tables
alter table users add primary key (id)
alter table levels add primary key (id)
alter table levels add unique key(min_exp);
(non primary key indexes would be fine too)
Then the query is extremely efficient:
explain select users.id as uid,users.exp,levels.id as levelid from users,levels where users.id = {{uid}} and levels.min_exp <= users.exp order by levelid desc limit 1;
+----+-------------+--------+-------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+---------------+---------+---------+-------+------+-------------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
| 1 | SIMPLE | levels | index | min_exp | PRIMARY | 4 | NULL | 1 | Using where |
----+-------------+--------+-------+---------------+---------+---------+-------+------+-------------+`