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