mySQL WHERE IN from JSON Array - mysql

I have a table with JSON data in it, and a statement that pulls out an array of ID's for each row...
SELECT items.data->"$.matrix[*].id" as ids
FROM items
This results in something like..
+------------+
| ids |
+------------+
| [1,2,3] |
+------------+
Next I want to select from another table where the ID of that other table is in the array, similar to the WHERE id IN ('1,2,3') but using the JSON array...
Something along the lines of...
SELECT * FROM other_items
WHERE id IN (
SELECT items.data->"$.matrix[*].id" FROM items
);
but it needs some JSON magic and I cant work it out...

Below is a complete answer. You may want a 'use <db_name>;' statement at the top of the script. The point is to show that JSON_CONTAINS() may be used to achieve the desired join.
DROP TABLE IF EXISTS `tmp_items`;
DROP TABLE IF EXISTS `tmp_other_items`;
CREATE TABLE `tmp_items` (`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT, `data` json NOT NULL);
CREATE TABLE `tmp_other_items` (`id` int NOT NULL, `text` nvarchar(30) NOT NULL);
INSERT INTO `tmp_items` (`data`)
VALUES
('{ "matrix": [ { "id": 11 }, { "id": 12 }, { "id": 13 } ] }')
, ('{ "matrix": [ { "id": 21 }, { "id": 22 }, { "id": 23 }, { "id": 24 } ] }')
, ('{ "matrix": [ { "id": 31 }, { "id": 32 }, { "id": 33 }, { "id": 34 }, { "id": 35 } ] }')
;
INSERT INTO `tmp_other_items` (`id`, `text`)
VALUES
(11, 'text for 11')
, (12, 'text for 12')
, (13, 'text for 13')
, (14, 'text for 14 - never retrieved')
, (21, 'text for 21')
, (22, 'text for 22')
-- etc...
;
-- Show join working:
SELECT
t1.`id` AS json_table_id
, t2.`id` AS joined_table_id
, t2.`text` AS joined_table_text
FROM
(SELECT st1.id, st1.data->'$.matrix[*].id' as ids FROM `tmp_items` st1) t1
INNER JOIN `tmp_other_items` t2 ON JSON_CONTAINS(t1.ids, CAST(t2.`id` as json), '$')
You should see the following results:

Starting from MySQL 8.0.13, there is MEMBER OF operator, which does exactly what you're looking for.
The query should be rewritten in the form of JOIN, though:
SELECT o.* FROM other_items o
JOIN items i ON o.id MEMBER OF(i.data->>'$.id')
If you want your query to have better performance, consider using multi-valued indexes on your JSON column.
Using of MEMBER OF() can be explained more clearly on the following example:
CREATE TABLE items ( data JSON );
INSERT INTO items
SET data = '{"id":[1,2,3]}';
That is how you find out whether the value is present in the JSON array:
SELECT * FROM items
WHERE 3 MEMBER OF(data->>'$.id');
+-------------------+
| data |
+-------------------+
| {"id": [1, 2, 3]} |
+-------------------+
1 row in set (0.00 sec)
Note that type of the value matters in this case, unlike regular comparison. If you pass it in a form of string, there will be no match:
SELECT * FROM items
WHERE "3" MEMBER OF(data->>'$.id');
Empty set (0.00 sec)
Although regular comparison would return 1:
SELECT 3 = "3";
+---------+
| 3 = "3" |
+---------+
| 1 |
+---------+
1 row in set (0.00 sec)

Before JSON being introduced in MySQL, I use this:
Ur original data: [1,2,3]
After replace comma with '][': [1][2][3]
Wrap ur id in '[]'
Then use REVERSE LIKE instead of IN: WHERE '[1][2][3]' LIKE
'%[1]%'
Answer to your question:
SELECT * FROM other_items
WHERE
REPLACE(SELECT items.data->"$.matrix[*].id" FROM items, ',', '][')
LIKE CONCAT('%', CONCAT('[', id, ']'), '%')
Why wrap into '[]'
'[12,23,34]' LIKE '%1%' --> true
'[12,23,34]' LIKE '%12%' --> true
If wrap into '[]'
'[12][23][34]' LIKE '%[1]%' --> false
'[12][23][34]' LIKE '%[12]%' --> true

Take care that the accepted answer won't use index on tmp_other_items leading to slow performances for bigger tables.
In such case, I usually use an integers table, containing integers from 0 to an arbitrary fixed number N (below, about 1 million), and I join on that integers table to get the nth JSON element:
DROP TABLE IF EXISTS `integers`;
DROP TABLE IF EXISTS `tmp_items`;
DROP TABLE IF EXISTS `tmp_other_items`;
CREATE TABLE `integers` (`n` int NOT NULL PRIMARY KEY);
CREATE TABLE `tmp_items` (`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT, `data` json NOT NULL);
CREATE TABLE `tmp_other_items` (`id` int NOT NULL PRIMARY KEY, `text` nvarchar(30) NOT NULL);
INSERT INTO `tmp_items` (`data`)
VALUES
('{ "matrix": [ { "id": 11 }, { "id": 12 }, { "id": 13 } ] }'),
('{ "matrix": [ { "id": 21 }, { "id": 22 }, { "id": 23 }, { "id": 24 } ] }'),
('{ "matrix": [ { "id": 31 }, { "id": 32 }, { "id": 33 }, { "id": 34 }, { "id": 35 } ] }')
;
-- Put a lot of rows in integers (~1M)
INSERT INTO `integers` (`n`)
(
SELECT
a.X
+ (b.X << 1)
+ (c.X << 2)
+ (d.X << 3)
+ (e.X << 4)
+ (f.X << 5)
+ (g.X << 6)
+ (h.X << 7)
+ (i.X << 8)
+ (j.X << 9)
+ (k.X << 10)
+ (l.X << 11)
+ (m.X << 12)
+ (n.X << 13)
+ (o.X << 14)
+ (p.X << 15)
+ (q.X << 16)
+ (r.X << 17)
+ (s.X << 18)
+ (t.X << 19) AS i
FROM (SELECT 0 AS x UNION SELECT 1) AS a
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS b ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS c ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS d ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS e ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS f ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS g ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS h ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS i ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS j ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS k ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS l ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS m ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS n ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS o ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS p ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS q ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS r ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS s ON TRUE
INNER JOIN (SELECT 0 AS x UNION SELECT 1) AS t ON TRUE)
;
-- Insert normal rows (a lot!)
INSERT INTO `tmp_other_items` (`id`, `text`)
(SELECT n, CONCAT('text for ', n) FROM integers);
Now you cna try again the accepted answer's query, which takes about 11seconds to run (but is simple):
-- Show join working (slow)
SELECT
t1.`id` AS json_table_id
, t2.`id` AS joined_table_id
, t2.`text` AS joined_table_text
FROM
(SELECT st1.id, st1.data->'$.matrix[*].id' as ids FROM `tmp_items` st1) t1
INNER JOIN `tmp_other_items` t2 ON JSON_CONTAINS(t1.ids, CAST(t2.`id` as JSON), '$')
;
And compare it to the faster approach of converting the JSON into a (temporary) table of ids, and then doing a JOIN over it (which lead to instant results, 0.000sec according to heidiSQL):
-- Fast
SELECT
i.json_table_id,
t2.id AS joined_table_id,
t2.`text` AS joined_table_text
FROM (
SELECT
j.json_table_id,
-- Don't forget to CAST if needed, so the column type matches the index type
-- Do an "EXPLAIN" and check its warnings if needed
CAST(JSON_EXTRACT(j.ids, CONCAT('$[', i.n - 1, ']')) AS UNSIGNED) AS id
FROM (
SELECT
st1.id AS json_table_id,
st1.data->'$.matrix[*].id' as ids,
JSON_LENGTH(st1.data->'$.matrix[*].id') AS len
FROM `tmp_items` AS st1) AS j
INNER JOIN integers AS i ON i.n BETWEEN 1 AND len) AS i
INNER JOIN tmp_other_items AS t2 ON t2.id = i.id
;
The most inner SELECT retrieves the list of JSON ids, along with their length (for outer join).
The 2nd inner SELECT takes this list of ids, and JOIN on the integers to retrieve the nth id of every JSON list, leading to a table of ids (instead of a table of jsons).
The outer most SELECT now only has to join this table of ids with the table containing the data you wanted.
Below is the same query using WHERE IN, to match the question title:
-- Fast (using WHERE IN)
SELECT t2.*
FROM tmp_other_items AS t2
WHERE t2.id IN (
SELECT
CAST(JSON_EXTRACT(j.ids, CONCAT('$[', i.n - 1, ']')) AS UNSIGNED) AS id
FROM (
SELECT
st1.data->'$.matrix[*].id' as ids,
JSON_LENGTH(st1.data->'$.matrix[*].id') AS len
FROM `tmp_items` AS st1) AS j
INNER JOIN integers AS i ON i.n BETWEEN 1 AND len)
;

Related

Best way to update/translate the values of a field mysql

So, I need to update/translate a field called status_id on my mysql table, basically following this:
WHERE | SHOULD BE
0 | 0
1 | 1
2 | 4
3 | 8
4 | 9
5 | 10
6 | 6
7 | 2
8 | 11
I've though of a few methods, but i'm not sure which one would be the best
The first one:
Use some transition elements, in this case, the desired final value+100
UPDATE myTable
SET status_id = 100
WHERE status_id = 0;
-- ...
UPDATE myTable
SET status_id = 111
WHERE status_id = 8;
-- ...
UPDATE myTable
SET status_id = 0
WHERE status_id = 100;
-- ...
UPDATE myTable
SET status_id = 11
WHERE status_id = 111;
The second one:
use CASE
UPDATE myTable SET status_id =
CASE
WHEN status_id = 0 THEN 0
-- ...
WHEN status_id = 8 THEN 11
ELSE status_id
END
This will only be done once, i just want to make sure i do not mess this up (I have backups, but it's always good to not need them)
For MySql 8.0+ you can create a CTE that returns each pair of current and new status_id and join it to the table in the UPDATE statement:
WITH cte(status_id, new_status_id) AS (VALUES
ROW(0, 0),
ROW(1, 1),
ROW(2, 4),
ROW(3, 8),
ROW(4, 9),
ROW(5, 10),
ROW(6, 6),
ROW(7, 2),
ROW(8, 11)
)
UPDATE myTable t
INNER JOIN cte c ON c.status_id = t.status_id
SET t.status_id = c.new_status_id
See the demo.
For prior versions join a subquery to the table:
UPDATE myTable t
INNER JOIN (
SELECT 0 status_id, 0 new_status_id UNION ALL
SELECT 1, 1 UNION ALL
SELECT 2, 4 UNION ALL
SELECT 3, 8 UNION ALL
SELECT 4, 9 UNION ALL
SELECT 5, 10 UNION ALL
SELECT 6, 6 UNION ALL
SELECT 7, 2 UNION ALL
SELECT 8, 11
) c ON c.status_id = t.status_id
SET t.status_id = c.new_status_id
See the demo.
Note that I don't see any reason to keep where both values are equal, but MySql will not perform update in such a case.
If you want to have some extra fun, you can use MySQL's JSON capabilities:
UPDATE myTable SET status_id = JSON_EXTRACT(
CAST('
{
"0": 0,
"1": 1,
"2": 4,
"3": 8,
"4": 9,
"5": 10,
"6": 6,
"7": 2,
"8": 11
}'
AS JSON
),
CONCAT('$."', status_id, '"')
)
It can actually be more convenient to write and maintain (and in some cases also cleaner and simpler to construct automatically in code).
You can put your values in a "holder table".
CREATE TEMPORARY TABLE my_temp_table (before_value int, after_value int) ENGINE=MEMORY;
/* populate your my_temp_table with your cross-walk values */
INSERT INTO my_temp_table (before_value, after_value)
SELECT 101, 201 FROM DUAL
UNION ALL SELECT 102, 202 FROM DUAL;
(etc, etc, i'm giving example values, put in your own real values.)
Update myTable
Set status_id = my_temp_table.after_value
from myTable , my_temp_table
Where
myTable.status_id = my_temp_table.before_value
That means you have "matches" that are not ambiguous.
The above is (one) "set based" approach.
If you have to do it sequentially (aka, you can't do the second update until the first update has been completed), you have to update one by one (also known as Row by Agonizing Row) RBAR.
REFERENCE:
https://dev.mysql.com/doc/refman/8.0/en/create-temporary-table.html
UPDATE myTable
SET status_id = FIND_IN_SET(status_id, '1,7,x,2,x,6,x,3,4,5,8')
WHERE status_id BETWEEN 0 AND 8;
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=5dd617361fcf6a3d2d669625dc809db1
PS. This is a special case which allows such special solution.

How to chain a series of numbers together in a MySQL DB with two columns defining the series connections?

I have a MySQL DB that defines series of numbers within sets as such:
set item1 item2
1 1 2
1 2 3
1 3 4
1 4 5
1 5 6
I want to write a query (or queries) that returns to me the fact that set 1 is a series of numbers that spans from 1 to 6. Is this possible?
Please note that the real DB I'm dealing with contains hundreds of sets and that each set can contain a series of items that can be somewhat long as well (up to 50 items per set, I'm guessing). Also, I'm not totally sure, but the DB might also have cases where the series of numbers split. Using the example above, there may be instances like the following:
set item1 item2
1 1 2
1 2 3
1 3 4
1 4 5
1 5 6
1 3 7
1 7 8
1 8 9
In which case, I'd want to know that set 1 has two series of numbers: [1, 2, 3, 4, 5, 6] and [1, 2, 3, 7, 8, 9]. Is this possible with hopefully one query (or if necessary, multiple queries)?
Edit: Please note that I used the numbers 1-9 in sequential order to make the question easier to understand. The real data is much more mixed up and not that orderly.
As you are aware, MySQL cannot handle recursion 'out-of-the-box', so options include:
writing a stored procedure
switching from an adjacency list to an alternative model (e.g. nested set)
joining the table to itself as often as could be required
handling the recursion in application level code (e.g. a bit of PHP)
Here is an example using option 3, but it could be easily adapted to suit option 4...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(
family_id INT NOT NULL,
item_id INT NOT NULL,
parent_id INT NULL,
PRIMARY KEY(family_id, item_id)
);
INSERT INTO my_table
VALUES (101, 1, null), (101, 2, 1), (101, 3, 2), (101, 4, 3),
(101, 5, 4), (101, 6, 5), (101, 7, 3), (101, 8, 7), (101, 9, 8);
SELECT CONCAT_WS(','
, a.item_id
, b.item_id
, c.item_id
, d.item_id
, e.item_id
, f.item_id
, g.item_id
, h.item_id
, i.item_id
) series
FROM
my_table a
LEFT JOIN
my_table b ON b.parent_id = a.item_id AND b.family_id = a.family_id
LEFT JOIN
my_table c ON c.parent_id = b.item_id AND c.family_id = b.family_id
LEFT JOIN
my_table d ON d.parent_id = c.item_id AND d.family_id = c.family_id
LEFT JOIN
my_table e ON e.parent_id = d.item_id AND e.family_id = d.family_id
LEFT JOIN
my_table f ON f.parent_id = e.item_id AND f.family_id = e.family_id
LEFT JOIN
my_table g ON g.parent_id = f.item_id AND g.family_id = f.family_id
LEFT JOIN
my_table h ON h.parent_id = g.item_id AND h.family_id = g.family_id
LEFT JOIN
my_table i ON i.parent_id = h.item_id AND i.family_id = h.family_id
WHERE
a.parent_id IS NULL;
+-------------+
| series |
+-------------+
| 1,2,3,4,5,6 |
| 1,2,3,7,8,9 |
+-------------+
I can solve the first problem.
Note that "set" is a keyword, so I renamed the first column to "sset"
You can see the result in http://sqlfiddle.com/#!9/ef6360/5
Create table and insert data:
create table test
(
sset int not null
, item1 int not null
, item2 int not null
) engine=InnoDB;
insert into test
values
(1, 1, 2)
, (1, 2, 3)
, (1, 3, 4)
, (1, 4, 5)
, (1, 5, 6)
Run the query:
select
sset
, group_concat(distinct item1or2 order by item1or2 asc)
from
(
select
sset
, item1 as item1or2
from test
union all
select
sset
, item2 as item1or2
from test
) u;
The output is:
1,2,3,4,5,6

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

select records that add up to a number

I have a table that looks like this
Table - mylist
-----
id | max_p
----------
1 | 4
2 | 2
3 | 2
4 | 6
5 | 2
6 | 2
I'd like to run a query which will find the minimum number of rows where the sum of max_p=10. so in this instance it would select records 1 and 4
if I wanted to run the same query to find 12 then it would select records 1,4 and 5
if I wanted to find records which equalled 2 it would just select record number 5 as that is the correct number and therefore no more than one record needs to be selected?
ideally this would select just one record if the desired amount was the same as any one row, then if this wasn't possible it would select two records, then three etc. If the desired number wasn't possible then it would return an empty result
fiddle here : http://ideone.com/3ECaT2
CREATE TABLE `my_list` (
`id` int(2) ,
`max_p` int(2),
PRIMARY KEY (`id`)
) ;
INSERT INTO `my_list` (`id`, `max_p`) VALUES
(1, 4),
(2, 2),
(3, 2),
(4, 6),
(5, 2),
(6, 2);
Any help greatly appreciated
To really solve this problem in SQL, you would need recursive subqueries. MySQL does not offer this functionality. What you can do is look for such a combination with up to a given number of elements. The following query implements this for four combinations:
select ml1.max_p as p1, ml2.max_p as p2, ml3.max_p as p3, ml4.max_p as p4
from my_list ml1 left outer join
my_list ml2
on ml1.id < ml2.id left outer join
my_list ml3
on ml2.id < ml3.id left outer join
my_list ml4
on ml3.id < ml4.id
where coalesce(ml1.max_p, 0) + coalesce(ml2.max_p, 0) + coalesce(ml3.max_p, 0) + coalesce(ml4.max_p, 0)
To get the shortest, count the number of elements and use limit:
select ml1.max_p as p1, ml2.max_p as p2, ml3.max_p as p3, ml4.max_p as p4
from my_list ml1 left outer join
my_list ml2
on ml1.id < ml2.id left outer join
my_list ml3
on ml2.id < ml3.id left outer join
my_list ml4
on ml3.id < ml4.id
where coalesce(ml1.max_p, 0) + coalesce(ml2.max_p, 0) + coalesce(ml3.max_p, 0) + coalesce(ml4.max_p, 0)
order by ((ml1.map_p is null) +
(ml2.map_p is null) +
(ml3.map_p is null) +
(ml4.map_p is null)
) desc
limit 1;

MYSQL: Find all posts between certain users

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;