SELECTing rows where no other rows match - mysql

This seemed pretty simple to start with, but it's getting awkward.
Suppose we have a table containing...
+---------+-----------+
| chat_id | friend_id |
+---------+-----------+
| A | 1 |
| A | 2 |
| A | 3 |
| B | 1 |
| B | 2 |
| C | 1 |
| C | 2 |
| C | 3 |
| D | 1 |
| D | 2 |
| D | 3 |
| D | 4 |
| D | 5 |
| E | 0 |
| E | 1 |
| E | 2 |
| E | 3 |
| E | 4 |
| E | 5 |
| E | 6 |
| E | 7 |
| F | 0 |
| F | 1 |
| G | 1 |
| G | 2 |
+---------+-----------+
And I wish to select only those chat_id that have friend_ids 1 and 2 and no other friend_id, what would the SQL be to get B and G returned?
So far, the best I've come up with is:
SELECT DISTINCT a.chat_id, COUNT(*)
FROM tt2 a
LEFT JOIN tt2 b
ON a.chat_id = b.chat_id
AND b.friend_id NOT IN (1,2)
WHERE a.friend_id in (1,2)
and b.chat_id IS NULL GROUP BY a.chat_id HAVING COUNT(*) = 2;
+---------+----------+
| chat_id | count(*) |
+---------+----------+
| B | 2 |
| G | 2 |
+---------+----------+
2 rows in set (0.00 sec)
And just in case I was looking for chat_id where only 1,2,3 exist...
SELECT DISTINCT a.chat_id, COUNT(*)
FROM tt2 a
LEFT JOIN tt2 b
ON a.chat_id = b.chat_id
AND b.friend_id not in (1,2,3)
WHERE a.friend_id IN (1,2,3)
AND b.chat_id IS NULL
GROUP BY a.chat_id
HAVING COUNT (*) = 3;
+---------+----------+
| chat_id | count(*) |
+---------+----------+
| A | 3 |
| C | 3 |
+---------+----------+
But this table could get massive and I need the SQL to be swift, does anyone know a better way?
To try and clarify... I get given a bunch of friend_id's and I want to get chat_id where only those friend_id exist for that chat_id.... with the SQL being quick (on sqlite)
Many thanks in advance!

Here's an option that should be able to limit the amount of data needed
SELECT
d.chat_id,
COUNT(DISTINCT s.friend_id) AS matchedFriends,
COUNT(DISTINCT d.friend_id) AS totalFriends
FROM tt2 AS d
INNER JOIN tt2 AS s
ON s.chat_id = d.chat_id
AND s.friend_id IN (1,2)
GROUP BY d.chat_id
HAVING matchedFriends = 2
AND totalFriends = matchedFriends
The INNER JOIN s makes sure that it only hits rows that have got at least one of the requested friends in. The matchedFriends count checks how many of the requested friends are found.
The totalFriends count then checks how many friends in total are on that chat.
Finally the HAVING first makes sure there are 2 matched friends, and then checks the number of friends in total equals the number of matched friends.
This will require you to supply both a list of friends, and a number of friends you are looking for, but should be efficient.
For increased efficiency, have an index on (chat_id,friend_id) (if you don't already, assuming it's a 2-part PK at time of writing)

Try this:
SELECT chat_id, GROUP_CONCAT(DISTINCT friend_id ORDER BY friend_id) AS friends
FROM table_1
GROUP BY chat_id
HAVING friends = '1,2'
Note: This works in mysql but I doubt that it will work on sqlite.

Related

MYSQL Limit 1 Record per in Joined Table

Attempting to join two tables on user_id. The users table has unique id for each user. The user_codes table can have multiple rows with the same user_id. I only want to return 1 row from the joined user_codes table, where code_count is the largest.
users Table
| id | email |
| -------- | --------------- |
| 1 | user1#gmail.com |
| 2 | user2#gmail.com |
| 3 | user3#gmail.com |
user_code TABLE
| user_id | invite_code | count |
| -------- | ----------- | ------|
| 1 | X49MCL1 | 40 |
| 1 | K59CLT9 | 1000 |
| 2 | X5BC924 | 15 |
| 2 | 38DF80L | 8 |
| 3 | 641020T | 22 |
EXPECTED RESULT
| id | email | invite_code | count |
| --- | --------------- | ----------- | ------|
| 1 | user1#gmail.com | K59CLT9 | 1000 |
| 2 | user2#gmail.com | X5BC924 | 15 |
| 3 | user3#gmail.com | 641020T | 22 |
The query result only includes a single instance of each user found in the user_codes table with the highest count.
Here is the closest query I could get, but it only returns the invite_code and count for the first user.
SELECT a.id, a.email, b.invite_code, b.count
FROM users a
LEFT JOIN user_codes b
ON b.user_id = a.id
AND b.count = (SELECT MAX(count) FROM user_codes GROUP BY b.user_id)
The above query returns the result:
| id | email | invite_code | count |
| --- | --------------- | ----------- | ------ |
| 1 | user1#gmail.com | K59CLT9 | 1000 |
| 2 | user2#gmail.com | `NULL` | `NULL` |
| 3 | user3#gmail.com | `NULL` | `NULL` |
I can't seem to figure out how/why the records after the first one don't include the invite_code and the count.
Thanks for help!
On MySQL 8+, I suggest using the RANK() window function:
WITH cte AS (
SELECT u.id, u.email, uc.invite_code, uc.count,
RANK() OVER (PARTITION BY u.id ORDER BY uc.count DESC) rnk
FROM users u
INNER JOIN user_code uc
ON uc.user_id = u.id
)
SELECT id, email, invite_code, count
FROM cte
WHERE rnk = 1;
The RANK() function will also match multiple records per user tied for the highest count.
You might be able to salvage your current attempt by correlating the user inside the subquery to the outer query:
SELECT a.id, a.email, b.invite_code, b.count
FROM users a
LEFT JOIN user_codes b
ON b.user_id = a.id AND
b.count = (SELECT MAX(uc.count) FROM user_codes uc WHERE uc.user_id = a.id);

Join records with lowest value

I have two tables, users and survey. I want query the table user and to join the table survey in a way that only the survey record with the lowest value is returned for each record in user table.
I want to avoid subqueries and temporary tables.
table users:
--------------
| uid | name |
--------------
| 1 | mike |
| 2 | john |
| 3 | bill |
--------------
table survey:
----------------------
| id | uid | value |
----------------------
| 1 | 3 | 9 |
| 2 | 3 | 5 |
| 3 | 1 | 3 |
| 4 | 1 | 7 |
| 5 | 1 | 2 |
| 6 | 2 | 4 |
| 7 | 2 | 9 |
| 8 | 1 | 0 |
| 9 | 2 | 5 |
---------------------
expected output:
---------------------
| id | name | value |
---------------------
| 8 | mike | 0 |
| 2 | bill | 5 |
| 6 | john | 4 |
---------------------
What kind of JOIn should I do, or how should I write the query?
The following query gets all rows with minimum value (doesn't exist another survey with value under the selected value)
Try this:
SELECT u.*, s.value
FROM survey s
JOIN users u
ON s.uid = u.uid
WHERE NOT EXISTS(
SELECT 'maximum'
FROM survey s2
WHERE s2.uid = s.uid
AND s2.value < s.value)
You could use something like this:
select s.id, u.name, y.min_value
from
(
select uid, min(value) as min_value
from survey
group by uid
) y
join survey s
on s.value = y.min_value
and s.uid = y.uid
join user u
on u.uid = y.uid
I think this will help you
SELECT * FROM SURVEY S
INNER JOIN USERS U
ON S.UID=U.UID
QUALIFY ROW_NUMBER() OVER (PARTITION BY S.UID ORDER BY S.VALUE1 ASC )=1;

Mysql how to combine GROUP BY and COUNT here

This must be quite easy, but I cannot find a good solution myself.
I have two tables:
file
+----+--------+
| id | system |
+----+--------+
| 1 | AA |
| 2 | AA |
| 3 | BB |
| 4 | AA |
+----+--------+
feature
+----+---------+------+
| id | file_id | name |
+----+---------+------+
| 1 | 1 | A |
| 1 | 2 | A |
| 1 | 2 | B |
| 1 | 3 | B |
| 1 | 3 | C |
| 1 | 4 | A |
| 1 | 4 | B |
| 1 | 4 | C |
+----+---------+------+
and I want to count how many times a feature was added to files with a specific system. For that, I have the following query:
SELECT f.name, COUNT(*) AS nr
FROM dossier d
JOIN feature f
ON f.file_id = d.id
WHERE d.system = 'AA'
AND d.id NOT IN (3157,3168,3192)
GROUP BY f.name
which gives the desired output:
+------+----+
| name | nr |
+------+----+
| A | 3 |
| B | 2 |
| C | 1 |
+------+----+
Now I also want to know the total amount of files with the same specific system. A simple separate query would be:
SELECT COUNT(*) FROM file WHERE system = 'AA' AND id NOT IN (3157,3168,3192)
I've added the extra AND id NOT IN (which is irrelevant for this example) just to show that the actual query is much more complex. If I use a separate query to get the total I would have to duplicate that complexity, so I want to avoid that by returning the total from the same query.
So how can I count the number of files in the first query?
Desired output:
+------+----+-------+
| name | nr | total |
+------+----+-------+
| A | 3 | 3 |
| B | 2 | 3 |
| C | 1 | 3 |
+------+----+-------+
Here is one way using Sub-query
SELECT f.NAME,
Count(*) AS nr,
(SELECT Count(*)
FROM FILE
WHERE system = 'AA'
AND id NOT IN ( 3157, 3168, 3192 )) as Total
FROM dossier d
JOIN feature f
ON f.file_id = d.id
WHERE d.system = 'AA'
AND d.id NOT IN ( 3157, 3168, 3192 )
GROUP BY f.NAME
Or Use CROSS JOIN
SELECT *
FROM (SELECT f.NAME,
Count(*) AS nr,
FROM dossier d
JOIN feature f
ON f.file_id = d.id
WHERE d.system = 'AA'
AND d.id NOT IN ( 3157, 3168, 3192 )
GROUP BY f.NAME) A
CROSS JOIN (SELECT Count(*) AS Total
FROM FILE
WHERE system = 'AA'
AND id NOT IN ( 3157, 3168, 3192 )) B

Counts from 3 tables with 2 left joins and 1 composite primary key

I have 3 tables like this
SecretAgents
| id | name |
|----|------|
| 1 | A |
| 2 | B |
Victims
| id | name | agent_id |
|----|------|----------|
| 1 | Z | 1 |
| 2 | Y | 1 |
| 3 | X | 2 |
Data
| id | keys | values | victim_id | form_id |
|----|------|--------|-----------|---------|
| 1 | a1 | x | 1 | 1 |
| 2 | a2 | xx | 1 | 2 |
| 3 | a3 | xxx | 2 | 1 |
| 4 | a5 | xxx | 1 | 1 |
I have to get the count of forms(here victim_id and form_id are composite primary keys) and the count of victims for each agent.
I have tried this for any 2 tables with left joins and group by but I am not able to achieve the same together. If anyone can be generous enough to offer a pointer/solution, that would be super awesome..
EDIT 1: The query
This is definitely not the right query but anyways
SELECT count(DISTINCT v.id) as victimcount, `sa`.`username`, `sa`.`id`, count(DISTINCT d.form_id) as submissions
FROM `SecretAgents` as `sa`
LEFT JOIN `Victims` as `v` ON `v`.`agent_id`=`sa`.`id`
LEFT JOIN `Data` as `d` ON `d`.`victim_id`=`v`.`id`
GROUP BY `v`.`agent_id`
ORDER BY `sa`.`id` ASC
The victimcount is correct but the submissions count becomes wrong. Tried lots of other things too but this is the most relevant...
Thanks
I believe you can count the forms-per-agent like so:
SELECT COUNT(*) as form_count, a.id as id, a.name as agent
FROM Data d
LEFT JOIN Victims v ON v.id = d.victim_id
LEFT JOIN SecretAgents a on v.agent_id = a.id
GROUP BY a.id;
To count the victims, just leave off the Data table.

How do I select last message for each conversation?

Here is how my database looks like:
table: conversations
+----+--------+--------+
| id | user_1 | user_2 |
+----+--------+--------+
| 1 | 1 | 2 |
| 2 | 2 | 3 |
| 3 | 1 | 3 |
+----+--------+--------+
table: messages
+----+--------------+------+
| id | conversation | text |
+----+--------------+------+
| 1 | 1 | hej |
| 2 | 1 | test |
| 3 | 2 | doh |
| 4 | 2 | hi |
| 5 | 3 | :) |
| 6 | 3 | :D |
+----+--------------+------+
Then when I run the followin query:
SELECT
*
FROM `messages`
INNER JOIN `conversations`
ON `conversations`.`id` = `messages`.`convesations`
GROUP BY `conversations`.`id`
ORDER BY `messages`.`id` DESC
Then I get those out from messages:
+----+--------------+------+
| id | conversation | text |
+----+--------------+------+
| 1 | 1 | hej |
| 3 | 2 | doh |
| 5 | 3 | :) |
+----+--------------+------+
But, is it somehow possible to do so that I will get the messages with the highest messages.id from that group, instead of the lowest?
EDIT: Here is the output I want from messages:
+----+--------------+------+
| id | conversation | text |
+----+--------------+------+
| 2 | 1 | test |
| 4 | 2 | hi |
| 6 | 3 | :D |
+----+--------------+------+
As those are the messages in same conversation with the highest id.
SELECT *
FROM conversations c
JOIN messages m
ON m.id =
(
SELECT id
FROM messages mi
WHERE mi.conversation = c.id
ORDER BY
mi.conversation DESC, mi.id DESC
LIMIT 1
)
Create an index on messages (conversation, id) for this to work fast.
You simply need to use nested query like this:
SELECT * FROM Messages
WHERE ID IN(
SELECT Max(m.ID) FROM Messages m
INNER JOIN conversations c
ON c.id = m.conversation
GROUP BY m.conversation
);
Output:
| ID | CONVERSATION | TEXT |
----------------------------
| 2 | 1 | test |
| 4 | 2 | hi |
| 6 | 3 | :D |
If you want data from both tables try this:
SELECT * FROM Messages m
JOIN conversations c
ON c.id = m.conversation
WHERE m.ID IN (
SELECT MAX(ID) FROM Messages
GROUP BY conversation
)
GROUP BY m.conversation;
Output:
| ID | CONVERSATION | TEXT | USER_1 | USER_2 |
----------------------------------------------
| 2 | 1 | test | 1 | 2 |
| 4 | 2 | hi | 2 | 3 |
| 6 | 3 | :D | 1 | 3 |
See this SQLFiddle
You are making your Join on the wrong column. 'Id' in Conversation cannot be equal to 'Id' in messages.
I thin, 'Conversation' in table messsages is 'id_conversation' right?
So, if I understood well :
SELECT *
FROM messages
INNER JOIN conversations
ON conversations.id = messages.conversation
GROUP BY conversations.id
ORDER BY messages.id DESC
I think you just have an incorrect table join:
SELECT *
FROM `messages`
INNER JOIN `conversations`
ON `conversations`.`id` = `messages`.`conversation`
GROUP BY `conversations`.`id`
ORDER BY `messages`.`id` DESC
Edit
You can try this:
SELECT *
FROM `messages`
WHERE `messages`.`id` IN (
SELECT MAX(id)
FROM messages
GROUP BY conversation
)
A couple of different approaches:
This approach relies on known but undocumented behaviour within MySQL, where the unaggregated, ungrouped values returned in a grouped query are the first in the sort order - it's fast, but should not be viewed as reliable:
SELECT * FROM
(SELECT * FROM messages
ORDER BY conversation, id desc) a
GROUP BY conversation
Alternatively, an approach that should always be reliable:
SELECT m.*, c.user_1, c.user_2 FROM messages m
JOIN (select conversation, max(id) max_id from messages group by conversation) l
ON m.id = l.max_id
JOIN conversations c
ON c.id = m.conversation
GROUP BY conversation
SQLFiddle here.