高效的MySQL多对多标签查询

高效的MySQL多对多标签查询,mysql,sql,Mysql,Sql,我很难找到一种有效的方法来根据标记选择数据库中的一行,并返回与该行关联的所有其他标记。 当我使用不返回行的所有标记的查询时,大约需要0.001秒。我最初的方案更加规范化,并且有另一个用于标记标签的表,但最终完成一个查询需要几秒钟的时间,因此我结束了删除该表并减少了它的规范化,但即使是这个解决方案也似乎相当慢 SELECT c.* FROM collections c, tags t WHERE t.collection_id=c.id AND (t.name IN ("foo",

我很难找到一种有效的方法来根据标记选择数据库中的一行,并返回与该行关联的所有其他标记。 当我使用不返回行的所有标记的查询时,大约需要0.001秒。我最初的方案更加规范化,并且有另一个用于标记标签的表,但最终完成一个查询需要几秒钟的时间,因此我结束了删除该表并减少了它的规范化,但即使是这个解决方案也似乎相当慢

SELECT c.*
FROM collections c,
     tags t
WHERE t.collection_id=c.id
  AND (t.name IN ("foo",
                  "bar"))
GROUP BY c.id HAVING COUNT(t.id)=2 LIMIT 10
现在,我无法找到一种有效的方法来同时获取该元素的所有其他标记,而不会减慢速度。我目前的解决方案大约慢了10倍,需要0.01秒才能完成,而且我感觉它的伸缩性不好(我发现它非常难看)


有没有一种有效或至少更有效的方法来实现这一点?我真的很感谢你对这件事的任何建议或暗示

好的。考虑下面……/P>
DROP TABLE IF EXISTS ingredients;

CREATE TABLE ingredients 
(ingredient_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,ingredient VARCHAR(30) NOT NULL UNIQUE
);

INSERT INTO ingredients (ingredient_id, ingredient) VALUES
(1, 'Macaroni'),
(2, 'Cheese'),
(3, 'Beans'),
(4, 'Toast'),
(5, 'Jam'),
(6, 'Jacket Potato'),
(7, 'Peanut Butter');


DROP TABLE IF EXISTS recipes;

CREATE TABLE recipes 
(recipe_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,recipe VARCHAR(50) NOT NULL UNIQUE
);

INSERT INTO recipes (recipe_id, recipe) VALUES
(1, 'Macaroni & Cheese'),
(2, 'Cheese on Toast'),
(3, 'Beans on Toast'),
(4, 'Cheese & Beans on Toast'),
(5, 'Toast & Jam'),
(6, 'Beans & Macaroni'),
(9, 'Beans on Jacket Potato'),
(10, 'Cheese & Beans on Jacket Potato'),
(12, 'Peanut Butter on Toast');

DROP TABLE IF EXISTS recipe_ingredient;

CREATE TABLE recipe_ingredient 
(recipe_id INT NOT NULL
,ingredient_id INT NOT NULL
,PRIMARY KEY (recipe_id,ingredient_id)
);

INSERT INTO recipe_ingredient (recipe_id, ingredient_id) VALUES
(1, 1),
(1, 2),
(2, 2),
(2, 4),
(3, 3),
(3, 4),
(4, 2),
(4, 3),
(4, 4),
(5, 4),
(5, 5),
(6, 1),
(6, 3),
(9, 3),
(9, 6),
(10, 2),
(10, 3),
(10, 6),
(12, 4),
(12, 7);

SELECT r.*
      , GROUP_CONCAT(CASE WHEN i.ingredient IN ('Cheese','Beans') THEN i.ingredient END) i
      , GROUP_CONCAT(CASE WHEN i.ingredient NOT IN('Cheese','Beans') THEN i.ingredient END) o 
   FROM recipes r 
   LEFT 
   JOIN recipe_ingredient ri 
     ON ri.recipe_id = r.recipe_id 
   LEFT 
   JOIN ingredients i 
     ON i.ingredient_id = ri.ingredient_id 
  GROUP 
     BY recipe_id;

+-----------+---------------------------------+--------------+---------------------+
| recipe_id | recipe                          | i            | o                   |
+-----------+---------------------------------+--------------+---------------------+
|         1 | Macaroni & Cheese               | Cheese       | Macaroni            |
|         2 | Cheese on Toast                 | Cheese       | Toast               |
|         3 | Beans on Toast                  | Beans        | Toast               |
|         4 | Cheese & Beans on Toast         | Cheese,Beans | Toast               |
|         5 | Toast & Jam                     | NULL         | Toast,Jam           |
|         6 | Beans & Macaroni                | Beans        | Macaroni            |
|         9 | Beans on Jacket Potato          | Beans        | Jacket Potato       |
|        10 | Cheese & Beans on Jacket Potato | Cheese,Beans | Jacket Potato       |
|        12 | Peanut Butter on Toast          | NULL         | Toast,Peanut Butter |
+-----------+---------------------------------+--------------+---------------------+

一模一样:

使用显式连接语法(这不应该对性能造成影响,因为MySQL应该设法优化它)

可能值得为正在检查的每个标记执行单独的内部联接,这样就不需要:-

SELECT c.*,
       GROUP_CONCAT(t1.name) AS tags
FROM collections c
INNER JOIN tags t ON t.collection_id = c.id AND t.name = "foo"
INNER JOIN tags t0 ON t.collection_id = c.id AND t0.name = "bar"
INNER JOIN tags t1 ON t1.collection_id = c.id
GROUP BY c.id 
LIMIT 10

但是,您的原始查询看起来并不糟糕,因此可能是索引问题。

您定义了哪些索引?执行计划是什么(
EXPLAIN-SELECT…
)?我不知道这是否仍然是真的(或者曾经是…),但是如果您将连接条件置于
中,mysql可能会更容易连接。atm您在语义上要求将
集合
中的每一行与
标记
中的每一行连接起来,然后过滤掉其中的一部分。@eggyal,所有表都将自己的id设置为primery索引。@evee,听起来值得一试。我要试试这个。谢谢使用显式连接语法而不是逗号。否则你和我们都很难弄清楚到底发生了什么。此外,必须有配方/成分的例子,处理正是这种东西。
SELECT c.*,
       GROUP_CONCAT(t1.name) AS tags
FROM collections c
INNER JOIN tags t ON t.collection_id = c.id
INNER JOIN tags t1 ON t1.collection_id = c.id
WHERE t.name IN ("foo", "bar")
GROUP BY c.id 
HAVING COUNT(t.id) = 2 
LIMIT 10
SELECT c.*,
       GROUP_CONCAT(t1.name) AS tags
FROM collections c
INNER JOIN tags t ON t.collection_id = c.id AND t.name = "foo"
INNER JOIN tags t0 ON t.collection_id = c.id AND t0.name = "bar"
INNER JOIN tags t1 ON t1.collection_id = c.id
GROUP BY c.id 
LIMIT 10