MYSQL: Find all posts between certain users - mysql

I have two tables:
table message (holds the creator of a message and the message)
id - creatorId - msg
and a table message_viewers (tells who can read the message msgId)
msgId - userId
If I create a message as user 1 and send it to user 2 and user 3, the tables will look like this:
tbl_message:
1 - 1 - 'message'
tbl_message_viewers:
1 - 2
1 - 3
What I want to do is to fetch the messages that are between the users x1...xN (any number of users) AND ONLY the messages between them.
(Example if users are 1, 2, and 3, I want the messages where the creator is 1, 2 or 3, and the users are 2,3 for creator = 1, 1 and 3 for creator = 2 and 1, 2 for creator = 3)
I am not interested by messages between 1 and 2, or 2 and 3, or 1 and 3, but only by messages between the 3 people.
I tried different approaches, such as joining the two tables on message id, selecting the messages where creatorId IN (X,Y) and then taking only the rows where userId IN (X, Y) as well. Maybe something about grouping and counting the rows, but I could not figure out a way of doing this that was working.
EDIT: SQL Fiddle here
http://sqlfiddle.com/#!2/963c0/1

I think this might do what you want:
SELECT m.*
FROM message m
INNER JOIN message_viewers mv ON m.id = mv.msgId
WHERE m.creatorId IN (1, 2, 3)
AND mv.userId IN (1, 2, 3)
AND NOT EXISTS (
SELECT 1
FROM message_viewers mv2
WHERE mv2.msgId = mv.msgId
AND mv2.userId NOT IN (1, 2, 3)
)
AND mv.userId != m.creatorId;
The IN's will give the users that created/can see, and the mv.userId != m.creatorId are for excluding the creator from the message_viewers table (like you showed in your requirements).
Edit:
With the requirement of only sending messages between those 3 id's, i came up with the following:
SELECT m.id,m.creatorId,m.message
FROM message m
INNER JOIN message_viewers mv ON m.id = mv.msgId
WHERE m.creatorId IN (1, 2, 3)
AND mv.userId IN (1, 2, 3)
AND mv.userId != m.creatorId
AND NOT EXISTS (
SELECT 1
FROM message_viewers mv2
WHERE mv2.msgId = mv.msgId
AND mv2.userId NOT IN (1, 2, 3)
)
GROUP BY 1,2,3
HAVING COUNT(*) = 2;
sqlfiddle demo

Try this with join and with IN() clause
SELECT * FROM
tbl_message m
JOIN tbl_message_viewers mv (m.id = mv.msgId )
WHERE m.creatorId IN(1,2,3) AND mv.userId IN(1,2,3)

Sounds like you might want the BETWEEN operator:
SELECT * FROM tablename WHERE fieldname BETWEEN 1 AND 10;
-- returns fieldname 1-10
In this case however, BETWEEN is inclusive, so you'll need to specify != those conditions as well:
SELECT * FROM tablename WHERE fieldname BETWEEN 1 AND 10 AND fieldname NOT IN (1, 10)
-- returns fieldname 2-9
http://www.w3schools.com/sql/sql_between.asp

this worked on oracle
first join gets row count for people we are not interested in
second join gets row count for people we are interested in
the in clauses will need to be generated by some sort of dynamic sql
and number_of_people also needs to be generated somehow.
select msgId, count_1, count_2
from message tm
join ( select ty.msgId as ty_msgId,
count(ty.msgId) as count_1
from message_viewers ty
where ty.userId not in (:a,:b,:c)
group by ty.msgId)
on msgId = ty_msgId
join (select tz.msgId as tz_msgId,
count(tz.msgId) as count_2
from message_viewers tz
where tz.userId in (:a,:b,:c)
group by tz.msgId)
on msgId = tz_msgId
where createrId in(:a,:b,:c)
and count_1 = 0
and count_2 = :number_of_people -1;
my sql prefers this
select msgId, count_1, count_2
from message tm
left join ( select ty.msgId as ty_msgId,
count(ty.msgId) as count_1
from message_viewers ty
where ty.userId not in (:a,:b,:c)
group by ty.msgId) as X
on msgId = ty_msgId
left join (select tz.msgId as tz_msgId,
count(tz.msgId) as count_2
from message_viewers tz
where tz.userId in (:a,:b,:c)
group by tz.msgId) as Y
on msgId = tz_msgId
where createrId in(:a,:b,:c)
and (count_1 = 0 or count_1 is null)
and count_2 = :number_of_people -1;

Related

MySQL Request with IN clause, with array of couple identifier

I have a user table with a couple as identifier : id and type, like this :
id | type | name
----------------
15 | 1 | AAA
16 | 1 | BBB
15 | 2 | CCC
I would like to get a list, matching both id and type.
I currently use a concat system, which works :
SELECT u.id,
u.type,
u.name
FROM user u
WHERE CONCAT(u.id, '-', u.type) IN ('15-1', '16-1', '17-1', '10-2', '15-2')
But, I have the feeling it could be better, what would be the proper way to do it ?
Thank you !
You may use the following approach in mysql
with dat as
(
select 17 id, 1 type, 'AAA' t
union all
select 16 id, 1 type, 'BBB' t
union all
select 17 id, 2 type, 'CCC' t
)
-- end of testing data
select *
from dat
where (id, type) in (
-- query data
(17, 1), (16, 1)
)
IN can operate on "tuples" of values, like this (a, b) IN ((c,d), (e,f), ....). Using this method is (should be) faster as you are not doing a concat operation on "a" and "b" and then comparing strings; instead you are comparing pairs of values, unprocessed and with an appropriate comparison operation (i.e. not always string compares).
Additionally, if "a" and/or "b" are string values themselves using the concat technique risks ambiguous results. ("1-2","3") and ("1","2-3") pairs concat to the same result "1-2-3"
You can separate them out. Not sure if it's more efficient but at least you would save the concat part :
SELECT u.id,
u.type,
u.name
FROM user u
WHERE (u.id = 15 AND u.type = 1)
OR (u.id = 16 AND u.type = 1)
OR (u.id = 17 AND u.type = 1)
OR (u.id = 10 AND u.type = 2)
OR (u.id = 15 AND u.type = 2)
I think it depends a lot on how you obtain the values for id and type that you use for filtering
If they are results of another computation they can be saved in a temporary table and used in a join
create TEMPORARY TABLE criteria as
select 15 as id, 1 as type
UNION
select 16 as id, 1 as type
UNION
select 17 as id, 1 as type
UNION
select 10 as id, 2 as type
UNION
select 15 as id, 2 as type
SELECT u.id,
u.type,
u.name
FROM user u
inner join criteria c on u.type = c.type and u.id = c.id
The other option is an inner query and then a join or a WITH clause (which is rather late addition to Mysql arsenal of tricks)

Remove double entry from orignial table when joining

I've got 2 tables:
adresses and a log of files (named send) i've sent.
For a given file, I want to get all adresses, and whether they received the file or not.
What I've got so far is this:
SELECT *
, CASE
WHEN send.fileid = 1 THEN 1
ELSE send.fileid = NULL
END as file1
FROM send
RIGHT OUTER JOIN `adress`
ON `send`.adressid = `adress`.`id`
The problem is, when an adress got two diffrent files, they get listed twice. How can I alter the statement to get arround this?
Example Data
*adress*
1 Adrian
2 Christian
3 Max
4 Alex
*file*
1 music
2 video
3 document
*send*
adress:1 file:1
adress:1 file:2 -
adress:3 file:1
adress:4 file:2 -
adress:4 file:3
when i browse the file 2, i want to see:
X Adrian
X Alex
Christian
Max
TLDR: I want all my adresses (once) with either the specific file id or null.
Thanks in advance.
One way of going about is is putting the condition in a subquery and letting the outer join do all the heavy lifting:
SELECT a.*, s.fieldid
FROM address a
LEFT JOIN (SELECT filedid, addressid
FROM send
WHERE fileid = 1) ON s.addressid = a.id
Surely you can do this just by using GROUP BY?
I put together a quick example, then realised this is MySQL. I think the fundamental approach is the same, but the example won't work., as this is SQL Server syntax:
DECLARE #address TABLE (address_id INT, address_name VARCHAR(50));
INSERT INTO #address SELECT 1, 'Adrian';
INSERT INTO #address SELECT 2, 'Christian';
INSERT INTO #address SELECT 3, 'Max';
INSERT INTO #address SELECT 4, 'Alex';
DECLARE #file TABLE ([file_id] INT, [file_name] VARCHAR(50));
INSERT INTO #file SELECT 1, 'music';
INSERT INTO #file SELECT 2, 'video';
INSERT INTO #file SELECT 3, 'document';
DECLARE #send TABLE (address_id INT, [file_id] INT);
INSERT INTO #send SELECT 1, 1;
INSERT INTO #send SELECT 1, 2;
INSERT INTO #send SELECT 3, 1;
INSERT INTO #send SELECT 4, 2;
INSERT INTO #send SELECT 4, 3;
SELECT
a.address_id,
a.address_name,
MAX(CASE WHEN f.[File_id] = 1 THEN 'X' END) AS file_1,
MAX(CASE WHEN f.[File_id] = 2 THEN 'X' END) AS file_2,
MAX(CASE WHEN f.[File_id] = 3 THEN 'X' END) AS file_3
FROM
#address a
LEFT JOIN #send s ON s.address_id = a.address_id
LEFt JOIN #file f ON f.[file_id] = s.[file_id]
GROUP BY
a.address_id,
a.address_name
ORDER BY
a.address_id;
This gives a matrix of address and files, i.e.:
address_id address_name file_1 file_2 file_3
1 Adrian X X NULL
2 Christian NULL NULL NULL
3 Max X NULL NULL
4 Alex NULL X X
SELECT *
FROM adress
LEFT JOIN send ON send.adressid = adress.id
AND send.fileid =1
LIMIT 0 , 30
that seems to be it

MySQL: Select all that only have two rows, with specific values?

I would like to grab all the users that ONLY have two roles, which are 1 and 4.
One user role is stored like this:
user_id role_id
54321 1
54321 4
54322 1
54323 1
How can i make a query, that grabs the user_id 54321, because it Only have two roles and these two are 1 and 4?
I can use WHERE role_id IN (1, 4) but this will also grab users that have other roles.
WHERE role_id IN (1, 4) GROUP BY user_ID HAVING COUNT(DISTINCT role_id) = 2
http://gregorulm.com/relational-division-in-sql-the-easy-way/
This is an example of a set-within-sets query. I like to solve these with group by and having because that is the most general approach:
select user_id
from user_roles ur
group by user_id
having sum(role_id = 1) > 0 and
sum(role_id = 4) > 0 and
sum(role_id not in (1, 4)) = 0;
The having clause has three conditions. The first counts the number of times that role is 1, and the user_id passes if there is at least one such role. The second does the same for 4. The last checks that there are no other values.
I like this structure because it is flexible. If the condition were 1 and 4 and others are allowed, you would just drop the third clause. If the condition were 1 or 4 and no others, then it would look like:
having (sum(role_id = 1) > 0 or
sum(role_id = 4) > 0
) and
sum(role_id not in (1, 4)) = 0;
SELECT u3.user_id
FROM t u1, t u2, t u3
WHERE u1.role_id = 1 AND u2.role_id = 4
AND u3.user_id = u1.user_id
AND u2.user_id = u1.user_id
GROUP BY u3.user_id HAVING COUNT(u3.role_id) = 2;

MySQL Complex Query, user conversation

I have this query to get the last message between two users.
SELECT f.*
FROM
(
SELECT *
FROM messages a
WHERE (LEAST(a.sender, a.receiver), GREATEST(a.sender, a.receiver), a.timestamp)
IN (
SELECT LEAST(b.sender, b.receiver) AS x,
GREATEST(b.sender, b.receiver) AS y,
MAX(b.timestamp) AS msg_time
FROM messages b
GROUP BY x, y
)
) f
WHERE :user_id IN (f.sender, f.receiver)
ORDER BY f.timestamp DESC
I got it here in SO, and it is a bit complex for me.
I need to modify it to get the last message only if :user_id has not deleted the conversation.
The table has 4 important fields: sender, receiver, sender_deleted, receiver_deleted.
When a user want to delete a conversation, i run a query to set the deleted fields to 1.
Example of table where user 4 has deleted the conversation:
sender receiver sender_deleted receiver_deleted message
4 17 1 0 user 4 to 17 message
17 4 0 1 user 17 to 4 message
I need to modify the above query so if :user_id = 4 it return empty, but if :user_id = 17 return the last message sent.
I hope i explained well what i want to do.
This ought to do what you're looking for:
SELECT f.*
FROM
(
SELECT *
FROM messages a
WHERE (LEAST(a.sender, a.receiver), GREATEST(a.sender, a.receiver), a.timestamp)
IN (
SELECT LEAST(b.sender, b.receiver) AS x,
GREATEST(b.sender, b.receiver) AS y,
MAX(b.timestamp) AS msg_time
FROM messages b
GROUP BY x, y
)
) f
WHERE (:user_id = f.sender AND f.sender_deleted != 1)
OR (:user_id = f.receiver AND f.receiver_deleted != 1)
ORDER BY f.timestamp DESC

MySQL - loop through rows

I have the following code
select count(*)
from (select Annotations.user_id
from Annotations, Users
where Users.gender = 'Female'
and Users.user_id = Annotations.user_id
and image_id = 1
group by Annotations.user_id
having sum(case when stem = 'taxi' then 1 else 0 end) > 0 and
sum(case when stem = 'zebra crossing' then 1 else 0 end) > 0
) Annotations
It produces a count of how many females who have given the stem 'taxi' and 'zebra crossing' for image 1.
Sample data
user id, image id, stem
1 1 image
1 1 taxi
1 1 zebra crossing
2 1 person
2 1 zebra crossing
2 1 taxi
3 1 person
3 1 zebra crossing
Expected result (or similar)
stem1, stem2, count
taxi , zebra crossing 2
person, zebra crossing 2
However, as there are over 2000 stems, I cannot specify them all.
How would I go around looping through the stem rows with the image_id = 1 and gender = female as opposed to specifying the stem string?
Thank you
As per my understanding, you need to fetch female users that have 2 or more stems
Update: It seems you need to display the user's that have a stem that is used by another user too, I have updated the query for the same
SELECT
distinct a.user_id,
group_concat(DISTINCT a.stem ORDER BY a.stem)
FROM
Annotations a
JOIN Users u ON ( a.user_id = u.user_id AND u.gender = 'Female' )
JOIN
(
SELECT
b.user_id,
b.stem
FROM
Annotations b
) AS b ON ( a.user_id <> b.user_id AND b.stem = a.stem )
WHERE
a.image_id = 1
GROUP BY
a.user_id
UPDATE: As I understand it, you want to select all combinations of 2 stems, and get a count of how many users have that combination of stems. Here is my solution:
SELECT stem1, stem2, count(*) as count FROM
(
SELECT a.user_id,a.image_id,a.stem as stem1,b.stem as stem2
FROM Annotations a JOIN Annotations b
ON a.user_id=b.user_id && b.image_id=a.image_id && a.stem!=b.stem
JOIN Users ON Users.user_id = a.user_id
WHERE Users.gender = "Female"
) as stems GROUP BY stem1, stem2 having count > 1 WHERE image_id=1;
The caveat here is that it will return 2 rows for each combinations of stems. (The second occurrence will have the stems in reverse order).
Here's my attempt to solve your problem:
SELECT COUNT(*) AS Count, a1.stem AS Stem1, a2.Stem AS Stem2
FROM Annotations AS a1
INNER JOIN Annotations AS a2 ON a1.user_id = a2.user_id AND a1.image_id = a2.image_id
AND a1.stem < a2.stem
WHERE a1.image_id = 1
GROUP BY a1.stem, a2.Stem
HAVING COUNT(*) > 1;
I did not include image_id logic.
Please see my SQL Fiddle here: http://sqlfiddle.com/#!2/4ee69/33
Based on the following data (copied from yours) I get the result posted underneath it.
CREATE TABLE Annotations
(`user_id` int, `image_id` int, `stem` varchar(14))
;
INSERT INTO Annotations
(`user_id`, `image_id`, `stem`)
VALUES
(1, 1, 'image'),
(1, 1, 'taxi'),
(1, 1, 'zebra crossing'),
(2, 1, 'person'),
(2, 1, 'zebra crossing'),
(2, 1, 'taxi'),
(3, 1, 'person'),
(3, 1, 'zebra crossing')
;
COUNT STEM1 STEM2
2 person zebra crossing
2 taxi zebra crossing