【问题标题】:Find rows without many-to-many children meeting a certain condition查找没有满足特定条件的多对多孩子的行
【发布时间】:2026-02-08 12:55:02
【问题描述】:

这是我正在尝试做的通用版本:

recipes 具有字段idname。表 ingredients 包含字段 idnamesweetness,以 1-10 的等级描述该成分的甜度。食谱有许多成分,成分在许多食谱中,因此两者在ingredients_recipes 表中相关,字段为ingredient_idrecipe_id

很容易找到包含甜度为 10 的成分的食谱。

SELECT DISTINCT recipes.* FROM recipes
INNER JOIN recipes_ingredients ri ON ri.recipe_id = recipes.id
INNER JOIN ingredients ON ingredients.id = ri.ingredient_id
WHERE ingredients.sweetness = 10

但是,我在否定该查询以查找具有 no 甜度 10 成分的食谱时遇到了麻烦。我的第一个想法是:

SELECT DISTINCT recipes.* FROM recipes
INNER JOIN recipes_ingredients ri ON ri.recipe_id = recipes.id
INNER JOIN ingredients ON ingredients.id = ri.ingredient_id
WHERE ingredients.sweetness != 10

但是,它会找到包含 任何 non-sweetness-10 成分的食谱。

我的下一次尝试如下,似乎可行:

SELECT * FROM recipes WHERE
(
  SELECT count(*) FROM ingredients INNER JOIN recipes_ingredients ri ON
  ri.ingredient_id = ingredients.id WHERE ingredients.sweetness = 10 AND
  ri.recipe_id = recipes.id
) = 0

但是,我的一般经验是,与等效的、精心设计的 JOIN 相比,依赖子查询运行缓慢。我玩过加入、分组等,但不能完全理解它,特别是因为,虽然看起来 LEFT JOINIS NULL 是合适的工具,但有两个加入已经让事情变得很糟糕。很棒的 SQL 向导,我可以运行什么查询来获得最佳结果?谢谢!

【问题讨论】:

    标签: mysql optimization join subquery


    【解决方案1】:

    试试这个:

    SELECT DISTINCT recipes.* 
    FROM recipes r LEFT JOIN
    (SELECT ri.recipe_id
    FROM recipes_ingredients ri 
    INNER JOIN ingredients ON ingredients.id = ri.ingredient_id
    WHERE ingredients.sweetness = 10) i on i.recipe_id=r.recipe_id
    WHERE i.recipe_id is null
    

    【讨论】:

      【解决方案2】:

      试试:

      select
        r.*
      from
        recipes r
      where
        not exists (
          select
            1
          from
            recipe_ingredients ri
            join ingredients i on ri.ingredient_id = ri.ingredient_id
          where
            ri.recipie_id = r.recipe_id
            and i.sweetness = 10
        )
      

      它仍然是一个相关子查询,但 existsnot exists 进行了一些优化,使其性能优于原始查询。

      对于直接加入解决方案,这应该工作:

      select distinct
        r.*
      from
        recipes r
        join recipe_ingredients ri on ri.recipe_id = r.recipe_id
        left join ingredents i on i.ingredient_id = ri.ingredient_id and i.sweetness = 10
      where
        i.ingredient_id is null
      

      根据索引,not exists 解决方案可能会更快,因为not exists 在确定是否有任何行满足给定条件后立即返回,而无需查看表中的任何内容。例如,如果发现单行甜度为 10,则停止查看该表并返回 false。

      【讨论】:

        【解决方案3】:

        我尝试了这里给我的答案(我已经投了赞成票),并且从他们的灵感中提出了一个查询,它似乎以惊人的出色表现完成了这项工作:

        SELECT r.* FROM recipes r
        LEFT JOIN recipes_ingredients ri ON ri.parent_id = r.id
        LEFT JOIN ingredients i ON i.id = ri.ingredient_id AND i.sweetness = 10
        GROUP BY r.id HAVING MAX(i.id) IS NULL
        

        与内部条件的连接(受@Donnie 启发)带来配方成分组合,如果成分不是甜度 10,则为 NULL 行。然后我们按配方 ID 分组,并选择“最大”成分 ID。 (MAX 函数将返回 null 当且仅当没有可供选择的实际 ID 时,即绝对没有与此食谱关联的非甜度 10 项可供选择。)如果该“最大”成分 ID 为null,则 MAX 函数没有 sweetness-10 项可供选择,因此,行 HAVING 和 null MAX(i.id) 被选择。

        在禁用查询缓存器的情况下,我多次运行NOT EXISTS 版本的查询和上述版本的查询。针对大约 400 个配方,NOT EXISTS 查询始终需要大约 1.0 秒才能完成,而此查询的运行时间通常约为 0.1 秒。针对大约 5000 个食谱,NOT EXISTS 查询大约需要 30 秒,而上述查询通常仍然需要 0.1 秒,并且几乎总是低于 1.0。

        值得注意的是,检查每个 EXPLAIN,这里列出的查询几乎可以完全在我给这些表的索引上运行,这可能解释了为什么它能够在不击球的情况下进行各种连接和分组眼睛。另一方面,NOT EXISTS 查询必须执行相关子查询。如果没有这些索引,这两者的性能可能会更加平等,但是当有机会使用原始连接时,查询优化器似乎非常强大。

        故事的寓意:格式良好的 JOIN 非常强大 :) 谢谢大家!

        【讨论】:

          最近更新 更多