discourse/db/post_migrate
Alan Guo Xiang Tan cd64e88711
PERF: Improve database query perf when loading topics for a category. (#14416)
* PERF: Improve database query perf when loading topics for a category.

Instead of left joining the `topics` table against `categories` by filtering with `categories.id`,
we can improve the query plan by filtering against `topics.category_id`
first before joining which helps to reduce the number of rows in the
topics table that has to be joined against the other tables and also
make better use of our existing index.

The following is a before and after of the query plan for a category
with many subcategories.

Before:

```
                                                                                                       QUERY PLAN

-------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------
 Limit  (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1)
   ->  Nested Loop Left Join  (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1)
         Join Filter: (category_users.category_id = topics.category_id)
         Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1))
         ->  Nested Loop Left Join  (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1)
               Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t
opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL))))
               Rows Removed by Filter: 1
               ->  Nested Loop  (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1)
                     Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1
1)))
                     Rows Removed by Join Filter: 13938306
                     ->  Index Scan using index_topics_on_bumped_at on topics  (cost=0.42..100480.05 rows=715549 width=24) (actual ti
me=0.010..633.015 rows=464623 loops=1)
                           Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text))
                           Rows Removed by Filter: 105321
                     ->  Materialize  (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623)
                           ->  Index Scan using categories_pkey on categories  (cost=0.14..35.89 rows=30 width=8) (actual time=0.006.
.0.040 rows=30 loops=1)
                                 Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1
13,104,98,100,96,108,109,110,111}'::integer[]))
               ->  Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu  (cost=0.43..0.53 rows=1 width=16) (a
ctual time=0.004..0.004 rows=0 loops=31)
                     Index Cond: ((topic_id = topics.id) AND (user_id = 1103877))
         ->  Materialize  (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30)
               ->  Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users  (cost=0.28..2.29 rows=1 width
=8) (actual time=0.004..0.004 rows=0 loops=1)
                     Index Cond: (user_id = 1103877)
 Planning Time: 1.359 ms
 Execution Time: 2453.765 ms
(23 rows)
```

After:

```
                                                                                                                            QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1)
   ->  Nested Loop Left Join  (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1)
         Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11))
         Rows Removed by Filter: 29
         ->  Nested Loop Left Join  (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1)
               Join Filter: (category_users.category_id = topics.category_id)
               Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1))
               ->  Nested Loop Left Join  (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1)
                     Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL))))
                     Rows Removed by Filter: 1
                     ->  Index Scan using index_topics_on_bumped_at on topics  (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1)
                           Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[])))
                           Rows Removed by Filter: 569895
                     ->  Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu  (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60)
                           Index Cond: ((topic_id = topics.id) AND (user_id = 1103877))
               ->  Materialize  (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59)
                     ->  Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users  (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1)
                           Index Cond: (user_id = 1103877)
         ->  Index Scan using categories_pkey on categories  (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59)
               Index Cond: (id = topics.category_id)
 Planning Time: 1.633 ms
 Execution Time: 657.255 ms
(22 rows)
```

* PERF: Optimize index on topics bumped_at.

Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
..
20210125100452_migrate_search_data_after_default_locale_rename.rb FIX: move post_search_data migration into onceoff job (#11851) 2021-01-26 16:29:00 +01:00
20210127140730_undo_add_processed_to_notifications.rb DEV: Replace 'processed' column on notifications with new table (#11864) 2021-01-27 10:29:24 -06:00
20210207232853_fix_topic_timer_duration_minutes.rb FIX: Delete topic timers far in the future (#12125) 2021-02-18 14:18:43 +02:00
20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb FIX: Do not mark group_flair images as secure on upload (#12081) 2021-02-16 12:34:03 +10:00
20210218022739_move_new_since_to_new_table_again.rb FIX: ensure corrected migration runs (#12137) 2021-02-19 11:48:32 +11:00
20210219171329_drop_old_sso_site_settings.rb DEV: Drop old SSO site setting rows from the database (#12148) 2021-02-19 19:05:49 +00:00
20210302164429_drop_flash_onebox_site_setting.rb Drop flash video onebox (#12261) 2021-03-02 17:11:14 +00:00
20210324043327_delete_orphan_post_revisions.rb FIX: delete orphan post revisions (#12502) 2021-03-25 12:34:53 +11:00
20210328233843_fix_bookmarks_with_incorrect_topic_id.rb FIX: Bookmark topics were not being updated when the post moved (#12542) 2021-03-29 11:25:48 +10:00
20210513125608_remove_length_constrain_from_topic_excerpt.rb FIX: errors that're triggering by too long excerpts (#13056) 2021-05-31 14:59:40 +04:00
20210525112226_remove_length_constrain_from_topic_link_url.rb FIX: PG::StringDataRightTruncation when linking posts (#13134) 2021-06-02 15:27:04 +04:00
20210528003603_fix_badge_image_avatar_upload_security_and_acls.rb FIX: Do not mark badge image uploads as secure (#13193) 2021-05-28 12:35:52 +10:00
20210621234939_backfill_email_log_topic_id.rb PERF: optimise backfilling of topic_id (#13545) 2021-06-28 16:16:22 +10:00
20210624023831_remove_highest_seen_post_number_from_topic_users.rb FEATURE: Add last visit indication to topic view page. (#13471) 2021-07-05 14:17:31 +08:00
20210628035905_drop_duration_column_from_topic_timers.rb DEV: Drop duration column from topic timers (#13543) 2021-06-29 09:27:12 +10:00
20210706091905_drop_disable_jump_reply_column_from_user_options.rb DEV: Drop user_options.disable_jump_reply column (#13646) 2021-07-06 10:47:17 +01:00
20210709053030_drop_uploads_verified.rb DEV: Drop uploads verified column (#13677) 2021-07-09 16:16:13 +10:00
20210802131421_remove_post_processed_trigger_option.rb DEV: Remove `PostProcessed` trigger option (#13916) 2021-08-04 22:24:47 +02:00
20210909041448_make_topic_id_nullable_for_bookmarks.rb DEV: Ignore bookmarks.topic_id column and remove references to it in code (#14289) 2021-09-15 10:16:54 +10:00
20210922064213_alter_bumped_at_indexes_on_topics.rb PERF: Improve database query perf when loading topics for a category. (#14416) 2021-09-28 10:05:00 +08:00