大规模的一对多性能

Posted

技术标签:

【中文标题】大规模的一对多性能【英文标题】:Doctrine one-to-many performance at scale 【发布时间】:2021-10-06 05:04:25 【问题描述】:

我用postpost_reaction 建立了一个一对多的关系。概念很简单,帖子可以被点赞,每个点赞都存储在post_reaction 表中,以及点赞者和反应类型(点赞、喜欢等)

一切正常™,但是随着规模的扩大,性能会下降,即随着 post_reaction 表的增长。

出于测试目的,我生成了 200 个帖子,并给每个帖子 1000 个反馈。这导致在post_reaction 表中存储了 200,000 个总反应。

我的 Twig 模板提供了一个帖子列表,限制为 20 个。当模板迭代显示每个帖子时,它会调用 post.reactions|length 来计算反应数量。这将执行以下数据库查询:

SELECT
  t0.reaction AS reaction_1,
  t0.id AS id_2,
  t0.created AS created_3,
  t0.post_id AS post_id_4,
  t0.user AS user_5
FROM
  post_reaction t0
WHERE
  t0.post_id = ?

对于我正在呈现的 20 个帖子,每次运行此查询平均需要 4-7 毫秒。仅仅为了统计帖子,这总共需要大约 100 毫秒的数据库查询。

这似乎并不算太​​糟糕,但是我们观察到在应用程序中处理这么多数据会产生一些开销。

查看整个请求的分析器,我们看到以下内容: 我们在此请求中的总体处理时间为 585 毫秒

components/news_post.html.twig 是调用post.reactions|length 触发数据库查询的组件。如果我们在不询问反应的情况下发起相同的请求,我们会观察到以下情况。 我们在此请求中的总体处理时间为 179 毫秒

快 406 毫秒/69.4%。我相信这主要归因于学说中的开销,因为它将 20,000 行处理为对象,仅供我们稍后计算。

为了缓解这种情况,我想看看将反应加入我的帖子查询是否会有所帮助。

SELECT
  p0_.replies_allowed AS replies_allowed_0,
  p0_.highlight_date AS highlight_date_1,
  p0_.title AS title_2,
  p0_.content AS content_3,
  p0_.id AS id_4,
  p0_.created AS created_5,
  p0_.updated AS updated_6,
  p0_.news_feed_id AS news_feed_id_7,
  p0_.created_by_id AS created_by_id_8,
  p0_.updated_by_id AS updated_by_id_9
FROM
  post p0_
  INNER JOIN post_reaction p1_ ON (p1_.post_id = p0_.id)
WHERE
  p0_.news_feed_id = ?
ORDER BY
  CASE WHEN p0_.highlight_date > ? THEN 0 ELSE 1 END ASC,
  p0_.created DESC
LIMIT
  20

但是它会导致查询中的 LIMIT 20 子句出现问题,因为由于此数据集中的反应数量,加入反应只允许返回一篇帖子。

我不确定我是否应该继续开发一种方法来让加入成为可能,或者探索一种替代方法,无论是什么方法。理想情况下,我想减少 406 毫秒的额外执行时间,因为它几乎占页面总处理时间的 70%,只是为了计算喜欢..


编辑:根据要求,show create table post_reaction 的输出

CREATE TABLE `post_reaction` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `post_id` int(11) DEFAULT NULL,
  `user` int(11) DEFAULT NULL,
  `reaction` int(11) NOT NULL,
  `reaction_timestamp` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_1B3A8E564B89032C` (`post_id`),
  KEY `IDX_1B3A8E568D93D649` (`user`),
  CONSTRAINT `FK_1B3A8E564B89032C` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`),
  CONSTRAINT `FK_1B3A8E568D93D649` FOREIGN KEY (`user`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200786 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

【问题讨论】:

你的查询加入post和post_reaction需要group by p0_.id;然后您将在每个帖子中获得一行,并且可以对 post_reaction 列进行某种计数或求和或求和,以获得您想要的每个帖子的任何摘要数据 @ysth 让我走到了一半!我可以计算所有反应,但曲线球是post_reaction 有一个列reaction,我需要按该列计算所有不同的值。这样我就可以分别计算喜欢、笑声和所有这些。它基本上是一个用作枚举的 int。我想不出一种简单的方法将其合并到查询中,尤其是在限制结果时 假设你可以硬核所有可能的反应值,这是select ..., sum(case when reaction=likevalue then 1 end) likes, sum(case when reaction=laughvalue then 1 end) laughs,... 如果不能硬核,在mysql中搜索pivot查询 【参考方案1】:

(第二次查询)不要将JOIN 转换为post_reaction,因为您没有使用其中的任何列。

ORDER BY 的复杂性使得不可能更快地查看所有 1000 个反应。因此LIMIT 对性能的影响很小。

请提供SHOW CREATE TABLE post_reaction,我们可以在那里进行一些改进。但是您当然需要一些以post_id 开头的索引开始。通过重新排列 PRIMARY KEY 以从该列开始,我们可能会得到一些改进。

(我对控制器或树枝一无所知。它们似乎是昂贵的部分?)

更多

“计算 [for each post] 的反应次数”——这是一个不会花费很长时间的 SQL 查询:

SELECT post_id,
       COUNT(*) AS reaction_count
    FROM post_reaction
    GROUP BY post_id;

不遍历帖子;一次没有 20 个;只需简单地通过该表中的索引即可完成所有操作。

我在 92 个国家/地区的 50 万个城市的表上尝试了等效查询。耗时 0.13 秒。

这里的教训是,当 SQL 被要求在很多行上执行很多相同的事情时,它会大放异彩。

【讨论】:

对于第二个查询,是的,我只是在尝试连接,即使我选择了输出,我也没有办法让学说用额外的值初始化对象上的映射属性.该限制主要针对查询的post 部分。如果我要提取每个帖子+所有反应,我会在应用程序级别遇到 OOM 错误,试图处理从数据库接收到的大量数据。我已经编辑了我的帖子以包含创建表 sql。 最后使用控制器/树枝位,它们能够调用后续的数据库查询。这就是为什么树枝在第一个示例中花费大量时间的原因,因为当它尝试渲染每个帖子时,它本质上是在调用 $post->getReactions() 这在当时是未初始化的并且学说去获取它;因此分析器知道这是渲染树枝模板时所花费的时间,因为那是它被调用的地方。我试图限制每次遍历每个帖子时都回到教义来询问反应。 @SteppingHat - 查看我添加的内容。我的结论是教义阻碍了你。

以上是关于大规模的一对多性能的主要内容,如果未能解决你的问题,请参考以下文章

一对多双向性能问题

GraphQL 一对多性能问题和限制

一对多关系中的 PHP 和 API 调用性能和最佳实践

21.Yii2.0框架多表关联一对多查询之性能优化--模型的使用

Django Postgres ArrayField 与一对多关系

SQLAlchemy 增删改查 一对多 多对多