From 57d06518d419ad067a01ff1dc6836a44d5b4b685 Mon Sep 17 00:00:00 2001 From: Martin Brennan <mjrbrennan@gmail.com> Date: Mon, 26 Oct 2020 14:30:31 +1000 Subject: [PATCH] FIX: Prevent slow bookmark first post reminder at query for topic (#11024) On forums with a large amount of posts when a user had a bookmark in the topic, PostgreSQL was using an inefficient query plan to fetch the first post of the topic. When running this ActiveRecord query: ``` topic.posts.with_deleted.where(post_number: 1).first ``` The following query plan was produced: ``` Limit (cost=0.43..583.49 rows=1 width=891) (actual time=3850.515..3850.515 rows=1 loops=1) -> Index Scan using posts_pkey on posts (cost=0.43..391231.51 rows=671 width=891) (actual time=3850.514..3850.514 rows=1 loops=1) Filter: ((topic_id = 160918) AND (post_number = 1)) Rows Removed by Filter: 2274520 Planning time: 0.200 ms Execution time: 3850.559 ms (6 rows) ``` The issue here is the combination of ORDER BY and LIMIT causing the ineficcient Index Scan using posts_pkey on posts to be used. When we correct the AR call to this: ``` topic.posts.with_deleted.find_by(post_number: 1) ``` We end up with a query that still has a LIMIT but no ORDER BY, which in turn creates a much more efficient query plan: ``` Limit (cost=0.43..1.44 rows=1 width=891) (actual time=0.033..0.034 rows=1 loops=1) -> Index Scan using index_posts_on_topic_id_and_post_number on posts (cost=0.43..678.82 rows=671 width=891) (actua l time=0.033..0.033 rows=1 loops=1) Index Cond: ((topic_id = 160918) AND (post_number = 1)) Planning time: 0.167 ms Execution time: 0.072 ms (5 rows) ``` This query plan uses the correct index, `Index Scan using index_posts_on_topic_id_and_post_number on posts`. Note that this is only a problem on forums with a larger amount of posts; tiny forums would not notice the difference. On large forums a query for a topic that takes 1s without a bookmark can take 8-30 seconds, and even end up with 502 errors from nginx. --- lib/topic_view.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index c10c99a72fd..49c9e45b49e 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -349,8 +349,12 @@ class TopicView end def first_post_bookmark_reminder_at - @topic.posts.with_deleted.where(post_number: 1).first - .bookmarks.where(user: @user).pluck_first(:reminder_at) + @first_post_bookmark_reminder_at ||= \ + begin + first_post = @topic.posts.with_deleted.find_by(post_number: 1) + return if !first_post + first_post.bookmarks.where(user: @user).pluck_first(:reminder_at) + end end MAX_PARTICIPANTS = 24