0

I have a table, like this:

id:int | name:String | categories:String

example rows:

1 | "Lorem1" | "A, B, C" 
2 | "Lorem2" | "A, B" 
3 | "Lorem3" | "A, C" 
4 | "Lorem4" | "B" 

I also have a form, where you can check the categories which u are intrested in. This should be the guide for the order of the select.

First you get back the rows, that has all the selected categories, then you get which has, less match. (If the row has none of the categories, it won't show up)

If someone for example checks:

  • A and B, they should get back the rows in this order: Lorem1, Lorem2, Lorem3, Lorem 4

  • A and C, they should get back the rows in this order: Lorem1, Lorem3, Lorem2

This is what I'm trying to make. I am quite new to programing, and this problem showed up.

I also know, maybe I should make a new table for the connections between the categories, and the objects.

Radagast
  • 5,102
  • 3
  • 12
  • 27

3 Answers3

0

You can use find_in_set() to check for a string being in the comma separated list. But you have to replace() the spaces first. Do so for each category selected by the user. Then check if the result of find_in_set() is larger than 0, as 0 means it didn't find anything, otherwise it returns the position in the list, which is larger than 0. Add the results of these comparisons. Since Boolean operations that are true are 1 in numeric context and otherwise 0, you can then order by that sum descending. I.e. the more matches a row has, the earlier it is out putted in the result.

Example for categories 'A' and 'C':

SELECT *
       FROM elbat
       ORDER BY (find_in_set('A', replace(categories, ' ', '')) > 0)
                +
                (find_in_set('C', replace(categories, ' ', '')) > 0)
                DESC;

You can also use this to exclude rows without any match. The sum will be 0 then.

SELECT *
       FROM elbat
       WHERE (find_in_set('A', replace(categories, ' ', '')) > 0)
             +
             (find_in_set('C', replace(categories, ' ', '')) > 0)
             > 0
       ORDER BY (find_in_set('A', replace(categories, ' ', '')) > 0)
                +
                (find_in_set('C', replace(categories, ' ', '')) > 0)
                DESC;

But comma separated lists are a pain. You should consider revising the schema and have another table, that links the items to categories.

sticky bit
  • 36,626
  • 12
  • 31
  • 42
0

Instead of storing your categories as a string you should define a ManyToManyfield in your user table. So, a user can be a part of one or many categories and vice versa. The categories table can store the different categories with their respective IDs.

jaimish11
  • 536
  • 4
  • 15
0

A normalized version of you data could be:

create table items (
  id int,
  name varchar(50),
  primary key (id),
  index (name)
);

create table categories (
  id int,
  name varchar(50),
  primary key (id),
  index (name)
);

create table items_categories (
  item_id int,
  category_id int,
  primary key (item_id, category_id),
  index (category_id, item_id),
  foreign key (item_id) references items(id),
  foreign key (category_id) references categories(id)
);

insert into items (id, name) values
  (1, 'Lorem1'),
  (2, 'Lorem2'),
  (3, 'Lorem3'),
  (4, 'Lorem4');

insert into categories (id, name) values
  (1, 'A'),
  (2, 'B'),
  (3, 'C'),
  (4, 'D');

insert into items_categories (item_id, category_id) values
  (1, 1),
  (1, 2),
  (1, 3),
  (2, 1),
  (2, 2),
  (3, 1),
  (3, 3),
  (4, 2);

Now - When you search for items in categories 'A' and 'B', the SELECT query would be:

select i.*, count(*) as matches
from items i
join items_categories ic on ic.item_id = i.id
join categories c on c.id = ic.category_id
where c.name in ('A', 'B')
group by i.id
order by matches desc, i.name;

Result:

| id  | name   | matches |
| --- | ------ | ------- |
| 1   | Lorem1 | 2       |
| 2   | Lorem2 | 2       |
| 3   | Lorem3 | 1       |
| 4   | Lorem4 | 1       |

If you want to search in categories 'A' and 'C', change the WHERE clause to

where c.name in ('A', 'C')

The result would be:

| id  | name   | matches |
| --- | ------ | ------- |
| 1   | Lorem1 | 2       |
| 3   | Lorem3 | 2       |
| 2   | Lorem2 | 1       |

View on DB Fiddle

You can even "emulate" your original schema with

select i.*, group_concat(c.name separator ', ') as categories
from items i
join items_categories ic on ic.item_id = i.id
join categories c on c.id = ic.category_id
group by i.id

Result:

| id  | name   | categories |
| --- | ------ | ---------- |
| 1   | Lorem1 | A, B, C    |
| 2   | Lorem2 | A, B       |
| 3   | Lorem3 | A, C       |
| 4   | Lorem4 | B          |

It would be much harder to do it the other way round. That is (for me) a major reason to use a normalized schema.

A good read: Is storing a delimited list in a database column really that bad?

Paul Spiegel
  • 30,925
  • 5
  • 44
  • 53