I have a table. It has two columns class_id and student. Column student is a list of students. The data type of student column is varchar. I want to write a SQL query that returns rows where student columns is a subset of larger list like ["A", "B", "C", "D", "E", "F", "G"]
class_id student
1 ["A","B"]
2 ["G","K","E"]
3 ["A","B","I"]
For above example, my query should return only one row with class_id 1.
Here is what i have so far
select * from A where student in ("A", "B", "C", "D", "E", "F", "G")
but it doesn't work.
This doesn't answer your question, but it does show how this should have been done. Write your data as:
class student
1 A
1 B
2 G
2 K
2 E
3 A
3 B
3 I
Now, you can answer questions like "which classes is A taking?"
SELECT UNIQUEROW class FROM A WHERE student='A'
and "which classes do A and B have together?"
SELECT class, COUNT(*) AS count FROM A WHERE student IN ('A','B') GROUP BY class HAVING count = 2;
I won't repeat the design suggestions above (but please pay attention to them). We can still process your data as is, given a little effort. Here's an example using tags instead of names:
Working example:
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=84641f571cb8e0bd2cb531f5c4ec586d
The table:
CREATE TABLE pivot (
id int AUTO_INCREMENT PRIMARY KEY
, tags varchar(255)
);
The test data:
INSERT INTO pivot (tags) VALUES
('tag1,tag2,tag3,tag1')
, ('tag2')
, ('tag4,tag5,tag4,tag4')
, ('tag5,tag5,tag4')
, ('tag5,tag5,tag4')
, ('tag1,tag3,tag2,tag1')
;
Here's a way to normalize the data dynamically:
WITH RECURSIVE seq (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM seq WHERE n <= 9
)
SELECT DISTINCT t1.*
, REPLACE(TRIM(LEADING SUBSTRING_INDEX(t1.tags,',',seq.n-1) FROM SUBSTRING_INDEX(t1.tags,',',seq.n)), ',','') AS tag
FROM pivot AS t1
JOIN seq
ON seq.n > 0 AND SUBSTRING_INDEX(t1.tags,',',seq.n-1) <> SUBSTRING_INDEX(t1.tags,',',seq.n)
ORDER BY id, tag
;
Result:
+----+---------------------+------+
| id | tags | tag |
+----+---------------------+------+
| 1 | tag1,tag2,tag3,tag1 | tag1 |
| 1 | tag1,tag2,tag3,tag1 | tag2 |
| 1 | tag1,tag2,tag3,tag1 | tag3 |
| 2 | tag2 | tag2 |
| 3 | tag4,tag5,tag4,tag4 | tag4 |
| 3 | tag4,tag5,tag4,tag4 | tag5 |
| 4 | tag5,tag5,tag4 | tag4 |
| 4 | tag5,tag5,tag4 | tag5 |
| 5 | tag5,tag5,tag4 | tag4 |
| 5 | tag5,tag5,tag4 | tag5 |
| 6 | tag1,tag3,tag2,tag1 | tag1 |
| 6 | tag1,tag3,tag2,tag1 | tag2 |
| 6 | tag1,tag3,tag2,tag1 | tag3 |
+----+---------------------+------+
Given the above normalized list, we can then find if some given set is a subset of a stored set:
WITH RECURSIVE seq (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM seq WHERE n <= 9
)
, norm AS (
SELECT DISTINCT t1.*
, REPLACE(TRIM(LEADING SUBSTRING_INDEX(t1.tags,',',seq.n-1) FROM SUBSTRING_INDEX(t1.tags,',',seq.n)), ',','') AS tag
FROM pivot AS t1
JOIN seq
ON seq.n > 0 AND SUBSTRING_INDEX(t1.tags,',',seq.n-1) <> SUBSTRING_INDEX(t1.tags,',',seq.n)
)
SELECT id
, tags
FROM norm
WHERE tag IN ('tag2', 'tag3')
GROUP BY id
HAVING COUNT(DISTINCT tag) = 2
ORDER BY id, tags
;
Result:
+----+---------------------+
| id | tags |
+----+---------------------+
| 1 | tag1,tag2,tag3,tag1 |
| 6 | tag1,tag3,tag2,tag1 |
+----+---------------------+
2 rows in set (0.003 sec)
Given the above normalized list, we can then find if some given set is a superset (or match) of a stored set (which addresses your specific question):
WITH RECURSIVE seq (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM seq WHERE n <= 9
)
, norm AS (
SELECT DISTINCT t1.*
, REPLACE(TRIM(LEADING SUBSTRING_INDEX(t1.tags,',',seq.n-1) FROM SUBSTRING_INDEX(t1.tags,',',seq.n)), ',','') AS tag
FROM pivot AS t1
JOIN seq
ON seq.n > 0 AND SUBSTRING_INDEX(t1.tags,',',seq.n-1) <> SUBSTRING_INDEX(t1.tags,',',seq.n)
)
SELECT id
, tags
FROM norm
GROUP BY id
HAVING COUNT(DISTINCT CASE WHEN tag IN ('tag2', 'tag3', 'tag8', 'tag9') THEN tag END) = COUNT(DISTINCT tag)
ORDER BY id, tags
;
Result:
+----+---------------------+
| id | tags |
+----+---------------------+
| 2 | tag2 |
+----+---------------------+
1 row in set (0.002 sec)
With your data:
INSERT INTO pivot (tags) VALUES
('A,B')
, ('G,K,E')
, ('A,B,I')
;
Solution:
WITH RECURSIVE seq (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM seq WHERE n <= 9
)
, norm AS (
SELECT DISTINCT t1.*
, REPLACE(TRIM(LEADING SUBSTRING_INDEX(t1.tags,',',seq.n-1) FROM SUBSTRING_INDEX(t1.tags,',',seq.n)), ',','') AS tag
FROM pivot AS t1
JOIN seq
ON seq.n > 0 AND SUBSTRING_INDEX(t1.tags,',',seq.n-1) <> SUBSTRING_INDEX(t1.tags,',',seq.n)
)
SELECT id
, tags
FROM norm
GROUP BY id
HAVING COUNT(DISTINCT CASE WHEN tag IN ('A','B','C','D','E','F','G') THEN tag END) = COUNT(DISTINCT tag)
ORDER BY id, tags
;
Result:
+----+------+
| id | tags |
+----+------+
| 1 | A,B |
+----+------+
1 row in set (0.003 sec)
Your requirement can be achieved if you are using MySQL 8+ / MariabDB 10.6
Using JSON_TABLE functions
JSON_TABLE(expr, path COLUMNS (column_list) [AS] alias)
Extracts data from a JSON document and returns it as a relational table having the specified columns.
Lets create a sample table with data
CREATE TABLE `table1` (
`class_id` int(11) NOT NULL AUTO_INCREMENT,
`student` longtext NOT NULL,
PRIMARY KEY (`class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `table1`(`student`) VALUES ('["A","B"]'), ('["G","K","E"]'), ('["A","B","I"]');
To achieve your requirement select * from A where student in ("A", "B", "C", "D", "E", "F", "G"), we can use JSON_TABLE
SELECT DISTINCT(class_id), student
FROM
table1,
JSON_TABLE(
student,
"$[*]" COLUMNS(
VALUE TEXT
PATH "$"
)
) DATA
WHERE DATA
.Value IN("A", "B", "C", "D", "E", "F", "G")
Result will be
| class_id | student |
|---|---|
| 1 | ["A","B"] |
| 2 | ["G","K","E"] |
| 3 | ["A","B","I"] |
db<>fiddle here