【问题标题】:Optimising MySQL queries with heavy joins使用重连接优化 MySQL 查询
【发布时间】:2012-10-07 11:03:36
【问题描述】:

我目前经营一个网站,该网站在列表中跟踪最新分数和评级。该列表有数千个经常更新的条目,并且该列表应该可以按这些分数和评级列进行排序。

我获取这些数据的 SQL 目前看起来像(大致):

SELECT e.*, SUM(sa.amount) AS score, AVG(ra.rating) AS rating
FROM entries e 
LEFT JOIN score_adjustments sa ON sa.entry_id = e.id
    HAVING sa.created BETWEEN ... AND ... 
LEFT JOIN rating_adjustments ra ON ra.entry_id = e.id
    HAVING ra.rating > 0 
ORDER BY score 
LIMIT 0, 10

表格在哪里(简化):

entries:
    id: INT(11) PRIMARY
    ...other data...

score_adjustments:
    id: INT(11), PRIMARY
    entry_id: INT(11), INDEX, FOREIGN KEY (entries.id)
    created: DATETIME
    amount: INT(4)

rating_adjustments:
    id: INT(11), PRIMARY
    entry_id: INT(11), INDEX, FOREIGN KEY (entries.id)
    rating: DOUBLE

大约有 300,000 个score_adjustments 条目,并且它们以每天大约 5,000 个的速度增长。 rating_adjustments 大约是那个的 1/4。

现在,我不是 DBA 专家,但我猜一直调用 SUM()AVG() 并不是一件好事——尤其是当 sara 包含数十万条记录时——对吧?

我已经对查询进行了缓存,但我希望查询本身快速 - 但仍尽可能保持最新。我想知道是否有人可以分享任何解决方案来优化像这样的繁重的连接/聚合查询?如有必要,我愿意进行结构更改。

编辑 1

添加了有关查询的更多信息。

【问题讨论】:

  • 实际查询会更好。
  • 几个索引通常可以解决问题,但是如果没有表结构、当前索引、实际查询和数据量,那就只能猜测了。在我们得到所有这些之后,这只是猜测。
  • @ypercube 添加了查询的近似表示
  • @GolezTrol 我已经添加了表结构
  • @Ryall 更新了我的答案,包括一个带有示例触发器的 sqlfiddle。

标签: mysql database-design database-optimization


【解决方案1】:

你的数据很糟糕clustered

InnoDB 将存储具有物理上靠近的“关闭”PK 的行。由于您的子表使用代理 PK,因此它们的行将随机存储。当需要对“主”表中的给定行进行计算时,DBMS 必须到处跳转以从子表中收集相关行。

尝试使用更“自然”的键来代替代理键,将父键的 PK 放在前缘,类似于:

score_adjustments:
    entry_id: INT(11), FOREIGN KEY (entries.id)
    created: DATETIME
    amount: INT(4)
    PRIMARY KEY (entry_id, created)

rating_adjustments:
    entry_id: INT(11), FOREIGN KEY (entries.id)
    rating_no: INT(11)
    rating: DOUBLE
    PRIMARY KEY (entry_id, rating_no)

注意:这假设created 的分辨率足够好,并且添加了rating_no 以允许每个entry_id 进行多个评级。这只是一个示例 - 您可以根据需要更改 PK。

这将“强制”属于同一entry_id 的行在物理上靠得很近存储,因此只需对 PK/集群键进行范围扫描并使用很少的 I/O 即可计算 SUM 或 AVG。

或者(例如,如果您使用不支持集群的 MyISAM),cover 带有索引的查询,因此在查询期间根本不会触及子表。


最重要的是,您可以对设计进行非规范化,并将当前结果缓存在父表中:

  • 将 SUM(score_adjustments.amount) 存储为物理字段,并在每次从 score_adjustments 插入、更新或删除行时通过触发器对其进行调整。
  • 将 SUM(rating_adjustments.rating) 存储为“S” COUNT(rating_adjustments.rating) 存储为“C”。当将一行添加到rating_adjustments 时,将其添加到 S 并递增 C。在运行时计算 S/C 以获得平均值。以类似方式处理更新和删除。

【讨论】:

  • 非常感谢,我现在会好好阅读这篇文章,让您知道我做了哪些更改以及结果。
  • 在我的情况下,created 不足以进行分数调整,所以我是否只需在 PK 末尾添加一个score_no(或等效项)?另外,不直接存储AVG(rating_adjustments.rating)而不是SUM()COUNT()的原因是什么?
  • @Ryall 这样一来,您可以轻松地仅根据当前子行更新它,而无需从子表中查询其他行。
  • 啊,是的,这是有道理的。第一次看错了。谢谢。
【解决方案2】:

如果您担心性能问题,您可以将 score 和 rating 列添加到相应的表中,并在插入时更新它们或使用触发器更新引用的表。这将在每次更新时缓存新结果,并且您不必每次都重新计算它们,从而显着减少获得结果所需的连接量......只是猜测,但在大多数情况下,您的查询结果可能是获取的次数多于更新的次数。

查看这个 sql fiddle http://sqlfiddle.com/#!2/b7101/1 以了解如何制作触发器及其效果,我只在插入时添加了触发器,您可以很容易地添加更新触发器,如果​​您曾经删除数据添加删除触发器。

没有添加 datetime 字段,如果 between ... and ... 参数经常更改,您可能仍需要每次手动执行此操作,否则您只需将 between 子句添加到 score_update 触发器。

【讨论】:

  • 这是一个很好的例子,谢谢。不知道 SQL Fiddle 的存在,似乎是一个方便的工具!
  • 我可以向您保证,它不只是看起来,它是,尤其是在与他人交谈时,否则我可以在本地数据库上完成。如果这对您有用,请将其作为正确答案进行检查,以便其他人知道。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-04
  • 2020-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多