在多个表上选择最大值,而不计算两次
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”。这是查询有JOIN
和GROUP BY
的地方。
JOIN
从连接表中收集适当的行组合; 然后
GROUP BY
COUNTs
、SUMs
等,为您提供聚合的膨胀值。
有两个常见的修复方法,都涉及与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
。(其他一些列也是如此。)
(实际上,MAX
和 MIN
不会得到夸大的值;AVG
会得到稍微错误的值;COUNT
和 SUM
得到“错误”的值。)
因此,我将把剩下的作为“练习”留给读者”。
索引
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”。以上是关于在多个表上选择最大值,而不计算两次的主要内容,如果未能解决你的问题,请参考以下文章