在多个表上选择最大值,而不计算两次

Posted

技术标签:

【中文标题】在多个表上选择最大值,而不计算两次【英文标题】:Select max value on multiple tables, without counting them twice 【发布时间】:2018-05-30 16:09:21 【问题描述】:

我正在查询允许我按分数订购食谱。

表结构

结构是传单包含一个或多个flyer_items,其中可以包含一个或多个ingredients_to_flyer_item(此表将成分链接到传单项目)。另一张表ingredient_to_recipe 将相同的成分链接到一个或多个食谱。最后包含指向 .sql 文件的链接。

查询示例

我想获取 recipe_id 和作为配方一部分的每种成分的 MAX 价格权重的总和(由成分 to_recipe 链接),但如果一个配方有多个成分属于同一个 flyers_item,它应该计算一次.

SELECT itr.recipe_id,
       SUM(itr.weight),
       SUM(max_price_weight),
       SUM(itr.weight + max_price_weight) AS score
FROM
  ( SELECT MAX(itf.max_price_weight) AS max_price_weight,
           itf.flyer_item_id,
           itf.ingredient_id
   FROM
     (SELECT ifi.ingredient_id,
             MAX(i.price_weight) AS max_price_weight,
             ifi.flyer_item_id
      FROM flyer_items i
      JOIN ingredient_to_flyer_item ifi ON i.id = ifi.flyer_item_id
      WHERE i.flyer_id IN (1,
                           2)
      GROUP BY ifi.ingredient_id ) itf
   GROUP BY itf.flyer_item_id) itf2
JOIN `ingredient_to_recipe` AS itr ON itf2.`ingredient_id` = itr.`ingredient_id`
WHERE recipe_id = 5730
GROUP BY itr.`recipe_id`
ORDER BY score DESC
LIMIT 0,10

查询几乎可以正常工作,因为大多数结果都很好,但是对于某些行,某些成分被忽略了,并且没有按应有的方式计入分数。

测试用例

| recipe_id | 'score' with current query | what 'score' should be | explanation                                                                 |
|-----------|----------------------------|------------------------|-----------------------------------------------------------------------------|
| 8376      | 51                         | 51                     | Good result                                                                 |
| 3152      | 1                          | 18                     | Only 1 ingredient having a score of one is counted, should be 4 ingredients |
| 4771      | 41                         | 45                     | One ingredient worth score 4 is ignored                                     |
| 10230     | 40                         | 40                     | Good result                                                                 |
| 8958      | 39                         | 39                     | Good result                                                                 |
| 4656      | 28                         | 34                     | One ingredient worth 6 is ignored                                           |
| 11338     | 1                          | 10                     | 2 ingredients, worth 4 and 5 are ignored                                    |

我很难找到一种简单的方法来解释它。让我知道是否还有其他帮助。

这里是运行查询、测试示例和测试用例的演示数据库链接:https://nofile.io/f/F4YSEu8DWmT/meta.zip

非常感谢。

更新(按 Rick James 的要求):

这是我能做到的最远距离。结果总是很好,在子查询中也是如此,但是,我已经完全按“flyer_item_id”取出了组。所以通过这个查询,我得到了很好的分数,但是如果食谱的许多成分是相同的 flyer_item_item,它们将被计算多次(对于 recipe_id = 10557 的得分将是 59 而不是好的 56,因为 2 个成分价值 3位于同一个 flyers_item 中)。我唯一需要做的就是为每个菜谱的每个 flyer_item_id 计算一个 MAX(price_weight)(我最初尝试通过在第一个 group_by 成分 ID 上按“flyer_item_id”分组。

SELECT itr.recipe_id,
       SUM(itr.weight) as total_ingredient_weight,
       SUM(itf.price_weight) as total_price_weight,
       SUM(itr.weight+itf.price_weight) as score
FROM
  (SELECT fi1.id, MAX(fi1.price_weight) as price_weight, ingredient_to_flyer_item.ingredient_id as ingredient_id, recipe_id
FROM flyer_items fi1
INNER JOIN (
    SELECT flyer_items.id as id, MAX(price_weight) as price_weight, ingredient_to_flyer_item.ingredient_id as ingredient_id
    FROM flyer_items
    JOIN ingredient_to_flyer_item ON flyer_items.id = ingredient_to_flyer_item.flyer_item_id
    GROUP BY id
) fi2 ON fi1.id = fi2.id AND fi1.price_weight = fi2.price_weight
JOIN ingredient_to_flyer_item ON fi1.id = ingredient_to_flyer_item.flyer_item_id
JOIN ingredient_to_recipe ON ingredient_to_flyer_item.ingredient_id = ingredient_to_recipe.ingredient_id
GROUP BY ingredient_to_flyer_item.ingredient_id) AS itf
INNER JOIN `ingredient_to_recipe` AS `itr` ON `itf`.`ingredient_id` = `itr`.`ingredient_id`
GROUP BY `itr`.`recipe_id`
ORDER BY `score` DESC
LIMIT 10

这是解释,但我不确定它是否有用,因为最后一个工作部分仍然缺失:

| id | select_type | table                    | partitions | type   | possible_keys                 | key           | key_len | ref                                                   | rows   | filtered | Extra                           |   |
|----|-------------|--------------------------|------------|--------|-------------------------------|---------------|---------|-------------------------------------------------------|--------|----------|---------------------------------|---|
| 1  | PRIMARY     | itr                      | NULL       | ALL    | recipe_id,ingredient_id       | NULL          | NULL    | NULL                                                  | 151800 | 100.00   | Using temporary; Using filesort |   |
| 1  | PRIMARY     | <derived2>               | NULL       | ref    | <auto_key0>                   | <auto_key0>   | 4       | metadata3.itr.ingredient_id                           | 10     | 100.00   | NULL                            |   |
| 2  | DERIVED     | ingredient_to_flyer_item | NULL       | ALL    | NULL                          | NULL          | NULL    | NULL                                                  | 249    | 100.00   | Using temporary; Using filesort |   |
| 2  | DERIVED     | fi1                      | NULL       | eq_ref | id_2,id,price_weight          | id_2          | 4       | metadata3.ingredient_to_flyer_item.flyer_item_id      | 1      | 100.00   | NULL                            |   |
| 2  | DERIVED     | <derived3>               | NULL       | ref    | <auto_key0>                   | <auto_key0>   | 9       | metadata3.ingredient_to_flyer_item.flyer_item_id,m... | 10     | 100.00   | NULL                            |   |
| 2  | DERIVED     | ingredient_to_recipe     | NULL       | ref    | ingredient_id                 | ingredient_id | 4       | metadata3.ingredient_to_flyer_item.ingredient_id      | 40     | 100.00   | NULL                            |   |
| 3  | DERIVED     | ingredient_to_flyer_item | NULL       | ALL    | NULL                          | NULL          | NULL    | NULL                                                  | 249    | 100.00   | Using temporary; Using filesort |   |
| 3  | DERIVED     | flyer_items              | NULL       | eq_ref | id_2,id,flyer_id,price_weight | id_2          | 4       | metadata3.ingredient_to_flyer_item.flyer_item_id      | 1      | 100.00   | NULL                            |   |

更新 2

我设法找到了一个有效的查询,但现在我必须让它更快,运行时间超过 500 毫秒。

SELECT sum(ff.price_weight) as price_weight, sum(ff.weight) as weight, sum(ff.price_weight+ff.weight) as score, ff.recipe_id FROM
(
SELECT DISTINCT
       itf.flyer_item_id as flyer_item_id,
       itf.recipe_id,
       itf.weight,
       aprice_weight AS price_weight
FROM
  (SELECT itfin.flyer_item_id AS flyer_item_id,
          itfin.price_weight AS aprice_weight,
          itfin.ingredient_id,
          itr.recipe_id,
          itr.weight
   FROM
     (SELECT ifi2.flyer_item_id, ifi2.ingredient_id as ingredient_id, MAX(ifi2.price_weight) as price_weight
        FROM
          ingredient_to_flyer_item ifi1
        INNER JOIN (
                SELECT id, MAX(price_weight) as price_weight, ingredient_to_flyer_item.ingredient_id as ingredient_id, ingredient_to_flyer_item.flyer_item_id
                FROM ingredient_to_flyer_item
                GROUP BY ingredient_id
            ) ifi2 ON ifi1.price_weight = ifi2.price_weight AND ifi1.ingredient_id = ifi2.ingredient_id
        WHERE flyer_id IN (1,2)
        GROUP BY ifi1.ingredient_id) AS itfin
      INNER JOIN `ingredient_to_recipe` AS `itr` ON `itfin`.`ingredient_id` = `itr`.`ingredient_id`

     ) AS itf
) ff
GROUP BY recipe_id
ORDER BY `score` DESC
LIMIT 20

这里是解释:

| id | select_type | table                    | partitions | type  | possible_keys                                | key           | key_len | ref                 | rows | filtered | Extra                           |   |
|----|-------------|--------------------------|------------|-------|----------------------------------------------|---------------|---------|---------------------|------|----------|---------------------------------|---|
| 1  | PRIMARY     | <derived2>               | NULL       | ALL   | NULL                                         | NULL          | NULL    | NULL                | 1318 | 100.00   | Using temporary; Using filesort |   |
| 2  | DERIVED     | <derived4>               | NULL       | ALL   | NULL                                         | NULL          | NULL    | NULL                | 37   | 100.00   | Using temporary                 |   |
| 2  | DERIVED     | itr                      | NULL       | ref   | ingredient_id                                | ingredient_id | 4       | itfin.ingredient_id | 35   | 100.00   | NULL                            |   |
| 4  | DERIVED     | <derived5>               | NULL       | ALL   | NULL                                         | NULL          | NULL    | NULL                | 249  | 100.00   | Using temporary; Using filesort |   |
| 4  | DERIVED     | ifi1                     | NULL       | ref   | ingredient_id,itx_full,price_weight,flyer_id | ingredient_id | 4       | ifi2.ingredient_id  | 1    | 12.50    | Using where                     |   |
| 5  | DERIVED     | ingredient_to_flyer_item | NULL       | index | ingredient_id,itx_full                       | ingredient_id | 4       | NULL                | 249  | 100.00   | NULL                            |   |

【问题讨论】:

您能否仅提供 MCVE 作为文本而不是 zip 文件。 @Strawberry 即使是一个简单的例子,在线上的数量也会很困难。 :\ 删除ORDER BY price_weight DESC。在子查询中排序没有意义。 在子查询中有flyer_id 列。您没有按此列进行分组,因此您应该使用聚合函数,例如 max(flyer_id)ingredient_id 列也存在相同的错误。 请对列使用前缀。我们无法猜测列来自哪些表。 【参考方案1】:

听起来像“explode-implode”。这是查询有JOINGROUP BY 的地方。

    JOIN 从连接表中收集适当的行组合; 然后 GROUP BYCOUNTsSUMs 等,为您提供聚合的膨胀值。

有两个常见的修复方法,都涉及与JOIN 分开进行聚合。

案例一:

SELECT  ...
        ( SELECT SUM(x) FROM t2 WHERE id = ... ) AS sum_x,
        ...
    FROM t1 ...

如果您需要来自 t2 的多个聚合,这种情况会变得很笨拙,因为它一次只允许一个。

案例 2:

SELECT ...
    FROM ( SELECT grp,
                  SUM(x) AS sum_x,
                  COUNT(*) AS ct
           FROM t2 ) AS s
    JOIN t1 ON t1.grp = s.grp

您有 2 个JOINs 和 3 个GROUP BYs,所以我建议您从内到外调试(并重写)您的查询。

        SELECT  ifi.ingredient_id,
                MAX(price_weight) as max_price_weight,
                flyer_item_id
            from  flyer_items i
            join  ingredient_to_flyer_item ifi  ON i.id = ifi.flyer_item_id
            where  flyer_id in (1, 2)
            group by  ifi.ingredient_id 

但我帮不了你,因为你没有通过它所在的表(或别名)来限定 price_weight。(其他一些列也是如此。)

(实际上,MAXMIN 不会得到夸大的值;AVG 会得到稍微错误的值;COUNTSUM 得到“错误”的值。)

因此,我将把剩下的作为“练习”留给读者”。

索引

itr:  (ingredient_id, recipe_id)  -- for the JOIN and WHERE and GROUP BY
itr:  (recipe_id, ingredient_id, weight)  -- for 1st Update
(There is no optimization available for the ORDER BY and LIMIT)
flyer_items:  (flyer_id, price_weight) -- unless flyer_id is the PRIMARY KEY
ifi:  (flyer_item_id, ingredient_id)
ifi:  (ingredient_id, flyer_item_id)  -- for 1st Update

请为相关表提供`SHOW CREATE TABLE。

请提供EXPLAIN SELECT ...

如果ingredient_to_flyer_item 是一个many:many 映射表,请按照提示here 操作。 ingredient_to_recipe 也一样?

GROUP BY itf.flyer_item_id 可能无效,因为它不包括非聚合的ifi.ingredient_id。参见“only_full_group_by”。

重新制定

完成对INDEXes 的评估后,请尝试以下操作。 注意:我不知道它是否能正常工作。

JOIN  `ingredient_to_recipe` AS itr  ON itf2.`ingredient_id` = itr.`ingredient_id`

JOIN ( SELECT recipe_id,
              ingredient_id,
              SUM(weight) AS sum_weight
           FROM ingredient_to_recipe ) AS itr

并更改初始 SELECT 以将 SUMs 替换为这些计算的总和。 (我怀疑我没有正确处理ingredient_id。)

您正在运行什么版本的 mysql/MariaDB?

【讨论】:

我刚刚更新了查询以使所有列(包括 price_weight)都符合表的要求。我试图解构它,甚至从另一种方式重建它,但没有成功。我了解正在发生的事情,但无法找到可行的解决方案。 我使用的是 MySQL 5.7.14。我正在尝试您的指示,我会尽快提供更新。 我已经更新了新的查询,不完整,但我可以用好的数据去最远。我还添加了解释。 查看更新 2,我找到了一个有效的查询,但希望加快速度。 @JeffB。 - 对于第一次更新,在看到EXPLAIN 之后,我在 itr 上添加了另一个索引:(recipe_id, ingredient_id, weight)。重要的是recipe_id是第一位的。【参考方案2】:

我一直想看看这个,但不幸的是直到现在还没有时间。我认为这个查询会给你你正在寻找的结果。

SELECT recipe_id, SUM(weight) AS weight, SUM(max_price_weight) AS price_weight, SUM(weight + max_price_weight) AS score 
FROM (SELECT recipe_id, ingredient_id, MAX(weight) AS weight, MAX(price_weight) AS max_price_weight
      FROM (SELECT itr.recipe_id, MIN(itr.ingredient_id) AS ingredient_id, MAX(itr.weight) AS weight, fi.id, MAX(fi.price_weight) AS price_weight
            FROM ingredient_to_recipe itr 
            JOIN ingredient_to_flyer_item itfi ON itfi.ingredient_id = itr.ingredient_id 
            JOIN flyer_items fi ON fi.id = itfi.flyer_item_id 
            GROUP BY itr.recipe_id, fi.id) ri
      GROUP BY recipe_id, ingredient_id) r
GROUP BY recipe_id
ORDER BY score DESC
LIMIT 10

它首先按flyer_item_id 分组,然后按MIN(ingredient_id) 分组,以考虑配方中具有相同flyer_item_id 的成分。然后将结果相加得到你想要的分数。如果我使用带有

的查询
HAVING recipe_id IN (8376, 3152, 4771, 10230, 8958, 4656, 11338)

子句给出以下结果,与您上面的“应该是什么分数”列相匹配:

recipe_id   weight  price_weight    score   
8376        10      41              51
4771        5       40              45
10230       10      30              40
8958        15      24              39
4656        15      19              34
3152        0       18              18
11338       0       10              10

我不确定此查询在您的系统上执行的速度有多快,它与您在我的笔记本电脑上的查询相当(我预计会慢一些)。我很确定有一些优化是可能的,但同样没有时间彻底研究它们。

我希望这能为您找到可行的解决方案提供更多帮助。

【讨论】:

【参考方案3】:

我不确定我是否完全理解了这个问题。在我看来,您按错误的列 flyer_items.id 分组。您应该改为按列 ingredient_id 进行分组。如果你这样做,它更有意义(对我来说)。这是我的看法:

select
    itr.recipe_id,
    sum(itr.weight),
    sum(max_price_weight),
    sum(itr.weight + max_price_weight) as score
  from (
    select
        ifi.ingredient_id, 
        max(price_weight) as max_price_weight
      from flyer_items i
      join ingredients_to_flyer_item ifi on i.id = ifi.flyer_item_id
      where flyer_id in (1, 2)
      group by ifi.ingredient_id
    ) itf
  join `ingredient_to_recipe` as itr on itf.`ingredient_id` = itr.`ingredient_id`
  group by itr.`recipe_id`
  order by score desc
  limit 0,10;

希望对你有帮助。

【讨论】:

我以前试过。唯一的问题是,如果我开始按 ifi.ingredient_id 分组。如果相同的成分存在于 2 个不同的传单中,它将在分数中计算两次(来自 flyers_item 的 price_weight)。它是同样的问题,但另一种方式。我尝试在 flyer_item_id 和成分 ID 上找到要结合的东西。如果我找不到让它按我希望工作的方法,我可能会使用这个查询。 “计数了两次”——这是我的“explode-implode”。

以上是关于在多个表上选择最大值,而不计算两次的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Access 2016 中的另一列上选择具有最大值的不同行

连接 2 个表,在另一个表上选择最高值

如何在表格上选择并计算出现的一些值

在子表上选择查询

在正则表达式上选择一行没有重复条目

如何做到这一点,当在OptionMenu上选择一个Option并按下一个按钮,然后对该特定选项进行操作时