I have a query that currently looks like:
SELECT [column a], [column b], [column c], [column d]
FROM [table]
WHERE FIND_IN_SET(2, column d)
ORDER BY [colu
Alternative: properly normalize the schema.
FIND_IN_SET is not Sargable and the index cannot be used.
One possible optimization is to define [column d]
as type SET
. As documentation says:
MySQL stores SET values numerically, with the low-order bit of the stored value corresponding to the first set member. If you retrieve a SET value in a numeric context, the value retrieved has bits set corresponding to the set members that make up the column value.
Here's a quick and simple example:
CREATE TABLE `tbl_name` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`set_col` set('a','b','c','d') NOT NULL,
PRIMARY KEY (`id`),
KEY `set_col_idx` (`set_col`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `tbl_name` (`set_col`) VALUES
('a'),
('b'),
('c'),
('d'),
('a,b'),
('a,c'),
('a,d'),
('b,c'),
('b,d'),
('c,d'),
('a,b,c'),
('a,b,d'),
('a,c,d'),
('b,c,d'),
('a,b,c,d');
A column set_col
specified as SET('a','b','c','d')
have members with following decimal and binary values:
╔════════════╦═══════════════╦══════════════╗
║ SET Member ║ Decimal Value ║ Binary Value ║
╠════════════╬═══════════════╬══════════════╣
║ 'a' ║ 1 ║ 0001 ║
╠════════════╬═══════════════╬══════════════╣
║ 'b' ║ 2 ║ 0010 ║
╠════════════╬═══════════════╬══════════════╣
║ 'c' ║ 4 ║ 0100 ║
╠════════════╬═══════════════╬══════════════╣
║ 'd' ║ 8 ║ 1000 ║
╚════════════╩═══════════════╩══════════════╝
So, if you need to retrive records with value a,c,d
, it's first, third and fourth member, which is 1 + 4 + 8
, which is 13
.
If you run query:
EXPLAIN SELECT * FROM `tbl_name` WHERE `tbl_name`.`set_col` = 13;
You'll get:
╔════╦═════════════╦══════════╦══════╦═══════════════╦═════════════╦═════════╦═══════╦══════╦═════════════╗
║ id ║ select_type ║ table ║ type ║ possible_keys ║ key ║ key_len ║ ref ║ rows ║ Extra ║
╠════╬═════════════╬══════════╬══════╬═══════════════╬═════════════╬═════════╬═══════╬══════╬═════════════╣
║ 1 ║ SIMPLE ║ tbl_name ║ ref ║ set_col_idx ║ set_col_idx ║ 1 ║ const ║ 1 ║ Using index ║
╚════╩═════════════╩══════════╩══════╩═══════════════╩═════════════╩═════════╩═══════╩══════╩═════════════╝
You don't need to manualy get know decimal values of SET
options – you can use SUM
, e.g.:
SELECT *
FROM `tbl_name`
WHERE `set_col` = (SELECT SUM(`set_col`)
FROM `tbl_name`
WHERE `set_col` IN ('a', 'c', 'd')
);
╔════╦═════════════╦══════════╦═══════╦═══════════════╦═════════════╦═════════╦═══════╦══════╦══════════════════════════╗
║ id ║ select_type ║ table ║ type ║ possible_keys ║ key ║ key_len ║ ref ║ rows ║ Extra ║
╠════╬═════════════╬══════════╬═══════╬═══════════════╬═════════════╬═════════╬═══════╬══════╬══════════════════════════╣
║ 1 ║ PRIMARY ║ tbl_name ║ index ║ set_col_idx ║ set_col_idx ║ 1 ║ NULL ║ 15 ║ Using where; Using index ║
╠════╬═════════════╬══════════╬═══════╬═══════════════╬═════════════╬═════════╬═══════╬══════╬══════════════════════════╣
║ 1 ║ SUBQUERY ║ tbl_name ║ range ║ set_col_idx ║ set_col_idx ║ 1 ║ NULL ║ 3 ║ Using where; Using index ║
╚════╩═════════════╩══════════╩═══════╩═══════════════╩═════════════╩═════════╩═══════╩══════╩══════════════════════════╝
1- fulltext index is not a good idea in this case because: length of the string you searching for is small (1) in this case, and this won't be found (It is configurable though, but not a good idea)
2- If this query is frequent, I suggest changing the tables' structure as follows:
In this one-to-many relationship, you can either do a join, or get the PK from table2 where condition is met, and select that ID's row from table1 Example:
Select * from table1 t1 inner join table2 t2 on t1.col_a=t2.col_a WHERE t2.value_of_sub_d=2