How can I read a string column as a list in MySQL?

Viewed 162

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.

3 Answers

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

Related