Skip to content

Add index to support epics filtering by award emoji

What does this MR do?

Improve query to filter epics without a given award emoji. This work is supposed to support the query executed once !57001 (merged) gets merged.

Migration output

== 20210329192716 AddCompositeIndexToAwardEmoji: migrating ====================
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:award_emoji, [:user_id, :name, :awardable_type, :awardable_id], {:name=>"idx_award_emoji_on_user_emoji_name_awardable_type_awardable_id", :algorithm=>:concurrently})
   -> 0.0022s
-- execute("SET statement_timeout TO 0")
   -> 0.0004s
-- add_index(:award_emoji, [:user_id, :name, :awardable_type, :awardable_id], {:name=>"idx_award_emoji_on_user_emoji_name_awardable_type_awardable_id", :algorithm=>:concurrently})
   -> 0.0097s
-- execute("RESET ALL")
   -> 0.0007s
== 20210329192716 AddCompositeIndexToAwardEmoji: migrated (0.0139s) ===========

== 20210331180118 RemoveDeprecatedIndexFromAwardEmoji: migrating ==============
-- transaction_open?()
   -> 0.0000s
-- indexes(:award_emoji)
   -> 0.0018s
-- remove_index(:award_emoji, {:algorithm=>:concurrently, :name=>"index_award_emoji_on_user_id_and_name"})
   -> 0.0040s
== 20210331180118 RemoveDeprecatedIndexFromAwardEmoji: migrated (0.0066s) =====

SQL Query

  • This query will only be executed by epic boards with 20 LIMIT, however it can be executed via GraphQL with max 100 LIMIT.
    So we are using the worst-case scenario to measure: 100 LIMIT for gitlab-org group. Also the same query which is used to filter issues by reaction emoji could take benefit from the index added here.

  • Timings were taken from database-lab without warming up caches, they can be higher than the production environment.

SELECT          "epics".* 
FROM            "epics" 
LEFT OUTER JOIN "boards_epic_board_positions" 
ON              "boards_epic_board_positions"."epic_id" = "epics"."id" 
WHERE           "epics"."group_id" IN (WITH recursive "base_and_descendants" AS ( 
                                       ( 
                                              SELECT "namespaces".* 
                                              FROM   "namespaces" 
                                              WHERE  "namespaces"."type" = 'Group' 
                                              AND    "namespaces"."id" = 9970) 
                                UNION 
                                      ( 
                                             SELECT "namespaces".* 
                                             FROM   "namespaces", 
                                                    "base_and_descendants" 
                                             WHERE  "namespaces"."type" = 'Group' 
                                             AND    "namespaces"."parent_id" = "base_and_descendants"."id"))SELECT   "namespaces"."id"
                                 FROM     "base_and_descendants" AS "namespaces") 
       AND      "epics"."state_id" = 1 
       AND      NOT ( 
                         EXISTS 
                         ( 
                                SELECT true 
                                FROM   "award_emoji" 
                                WHERE  "award_emoji"."user_id" = 426128 
                                AND    "award_emoji"."awardable_type" = 'Epic' 
                                AND    "award_emoji"."awardable_id" = "epics"."id" 
                                AND    "award_emoji"."name" = 'thumbsup')) 
       AND      ( 
                         "boards_epic_board_positions"."epic_board_id" = 4 
                OR       "boards_epic_board_positions"."epic_board_id" IS NULL) 
       ORDER BY boards_epic_board_positions.relative_position ASC nulls last, 
                epics.id DESC limit 100

Query plan and timings WITHOUT INDEX

Limit  (cost=4318.95..4318.98 rows=13 width=1205) (actual time=8209.075..8209.127 rows=100 loops=1)
   Buffers: shared hit=15113 read=8473 dirtied=256
   I/O Timings: read=7965.406
   ->  Sort  (cost=4318.95..4318.98 rows=13 width=1205) (actual time=8209.073..8209.097 rows=100 loops=1)
         Sort Key: boards_epic_board_positions.relative_position, epics.id DESC
         Sort Method: top-N heapsort  Memory: 212kB
         Buffers: shared hit=15113 read=8473 dirtied=256
         I/O Timings: read=7965.406
         ->  Nested Loop Anti Join  (cost=4231.08..4318.71 rows=13 width=1205) (actual time=4559.970..8195.658 rows=3871 loops=1)
               Buffers: shared hit=15110 read=8473 dirtied=256
               I/O Timings: read=7965.406
               ->  Hash Right Join  (cost=4230.65..4286.40 rows=13 width=1205) (actual time=4530.884..4540.380 rows=3871 loops=1)
                     Hash Cond: (boards_epic_board_positions.epic_id = epics.id)
                     Filter: ((boards_epic_board_positions.epic_board_id = 4) OR (boards_epic_board_positions.epic_board_id IS NULL))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=3348 read=4605 dirtied=242
                     I/O Timings: read=4408.664
                     ->  Seq Scan on public.boards_epic_board_positions  (cost=0.00..51.30 rows=1130 width=20) (actual time=0.007..0.008 rows=0 loops=1)
                     ->  Hash  (cost=4214.72..4214.72 rows=1274 width=1201) (actual time=4530.844..4530.854 rows=3871 loops=1)
                           Buckets: 4096  Batches: 1  Memory Usage: 3566kB
                           Buffers: shared hit=3348 read=4605 dirtied=242
                           I/O Timings: read=4408.664
                           ->  Nested Loop  (cost=1432.83..4214.72 rows=1274 width=1201) (actual time=602.633..4505.722 rows=3871 loops=1)
                                 Buffers: shared hit=3348 read=4605 dirtied=242
                                 I/O Timings: read=4408.664
                                 ->  HashAggregate  (cost=1432.41..1434.12 rows=171 width=4) (actual time=595.356..595.671 rows=233 loops=1)
                                       Group Key: namespaces.id
                                       Buffers: shared hit=760 read=401 dirtied=2
                                       I/O Timings: read=580.870
                                       ->  CTE Scan on base_and_descendants namespaces  (cost=1426.85..1430.27 rows=171 width=4) (actual time=6.980..594.746 rows=233 loops=1)
                                             Buffers: shared hit=760 read=401 dirtied=2
                                             I/O Timings: read=580.870
                                             CTE base_and_descendants
                                               ->  Recursive Union  (cost=0.43..1426.85 rows=171 width=344) (actual time=6.971..593.299 rows=233 loops=1)
                                                     Buffers: shared hit=760 read=401 dirtied=2
                                                     I/O Timings: read=580.870
                                                     ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_1  (cost=0.43..3.45 rows=1 width=344) (actual time=6.955..6.958 rows=1 loops=1)
                                                           Index Cond: (((namespaces_1.type)::text = 'Group'::text) AND (namespaces_1.id = 9970))
                                                           Buffers: shared read=4
                                                           I/O Timings: read=6.877
                                                     ->  Nested Loop  (cost=0.56..142.00 rows=17 width=344) (actual time=5.327..97.271 rows=39 loops=6)
                                                           Buffers: shared hit=760 read=397 dirtied=2
                                                           I/O Timings: read=573.993
                                                           ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.001..0.043 rows=39 loops=6)
                                                           ->  Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces namespaces_2  (cost=0.56..14.16 rows=2 width=344) (actual time=1.411..2.498 rows=1 loops=233)
                                                                 Index Cond: (namespaces_2.parent_id = base_and_descendants.id)
                                                                 Filter: ((namespaces_2.type)::text = 'Group'::text)
                                                                 Rows Removed by Filter: 0
                                                                 Buffers: shared hit=760 read=397 dirtied=2
                                                                 I/O Timings: read=573.993
                                 ->  Index Scan using index_epics_on_group_id on public.epics  (cost=0.42..16.19 rows=7 width=1201) (actual time=0.705..16.742 rows=17 loops=233)
                                       Index Cond: (epics.group_id = namespaces.id)
                                       Filter: (epics.state_id = 1)
                                       Rows Removed by Filter: 9
                                       Buffers: shared hit=2588 read=4204 dirtied=240
                                       I/O Timings: read=3827.794
               ->  Index Scan using index_award_emoji_on_awardable_type_and_awardable_id on public.award_emoji  (cost=0.43..2.34 rows=1 width=4) (actual time=0.942..0.942 rows=0 loops=3871)
                     Index Cond: (((award_emoji.awardable_type)::text = 'Epic'::text) AND (award_emoji.awardable_id = epics.id))
                     Filter: ((award_emoji.user_id = 426128) AND ((award_emoji.name)::text = 'thumbsup'::text))
                     Rows Removed by Filter: 1
                     Buffers: shared hit=11762 read=3868 dirtied=14
                     I/O Timings: read=3556.742
Time: 8.219 s
  - planning: 8.755 ms
  - execution: 8.210 s
    - I/O read: 7.965 s
    - I/O write: N/A

Shared buffers:
  - hits: 15113 (~118.10 MiB) from the buffer pool
  - reads: 8473 (~66.20 MiB) from the OS file cache, including disk I/O
  - dirtied: 256 (~2.00 MiB)
  - writes: 0

Query plan and timings WITH INDEX

Limit  (cost=4290.42..4290.45 rows=13 width=1205) (actual time=4266.384..4266.419 rows=100 loops=1)
   Buffers: shared hit=3351 read=4609 dirtied=242
   I/O Timings: read=4147.863
   ->  Sort  (cost=4290.42..4290.45 rows=13 width=1205) (actual time=4266.382..4266.403 rows=100 loops=1)
         Sort Key: boards_epic_board_positions.relative_position, epics.id DESC
         Sort Method: top-N heapsort  Memory: 212kB
         Buffers: shared hit=3351 read=4609 dirtied=242
         I/O Timings: read=4147.863
         ->  Nested Loop Anti Join  (cost=4231.21..4290.18 rows=13 width=1205) (actual time=4257.541..4262.946 rows=3871 loops=1)
               Buffers: shared hit=3348 read=4609 dirtied=242
               I/O Timings: read=4147.863
               ->  Hash Right Join  (cost=4230.65..4286.40 rows=13 width=1205) (actual time=4257.253..4260.582 rows=3871 loops=1)
                     Hash Cond: (boards_epic_board_positions.epic_id = epics.id)
                     Filter: ((boards_epic_board_positions.epic_board_id = 4) OR (boards_epic_board_positions.epic_board_id IS NULL))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=3348 read=4605 dirtied=242
                     I/O Timings: read=4147.619
                     ->  Seq Scan on public.boards_epic_board_positions  (cost=0.00..51.30 rows=1130 width=20) (actual time=0.008..0.009 rows=0 loops=1)
                     ->  Hash  (cost=4214.72..4214.72 rows=1274 width=1201) (actual time=4257.218..4257.228 rows=3871 loops=1)
                           Buckets: 4096  Batches: 1  Memory Usage: 3566kB
                           Buffers: shared hit=3348 read=4605 dirtied=242
                           I/O Timings: read=4147.619
                           ->  Nested Loop  (cost=1432.83..4214.72 rows=1274 width=1201) (actual time=563.662..4234.031 rows=3871 loops=1)
                                 Buffers: shared hit=3348 read=4605 dirtied=242
                                 I/O Timings: read=4147.619
                                 ->  HashAggregate  (cost=1432.41..1434.12 rows=171 width=4) (actual time=557.926..558.244 rows=233 loops=1)
                                       Group Key: namespaces.id
                                       Buffers: shared hit=760 read=401 dirtied=2
                                       I/O Timings: read=545.746
                                       ->  CTE Scan on base_and_descendants namespaces  (cost=1426.85..1430.27 rows=171 width=4) (actual time=11.605..557.374 rows=233 loops=1)
                                             Buffers: shared hit=760 read=401 dirtied=2
                                             I/O Timings: read=545.746
                                             CTE base_and_descendants
                                               ->  Recursive Union  (cost=0.43..1426.85 rows=171 width=344) (actual time=11.598..556.301 rows=233 loops=1)
                                                     Buffers: shared hit=760 read=401 dirtied=2
                                                     I/O Timings: read=545.746
                                                     ->  Index Scan using index_namespaces_on_type_and_id_partial on public.namespaces namespaces_1  (cost=0.43..3.45 rows=1 width=344) (actual time=11.577..11.580 rows=1 loops=1)
                                                           Index Cond: (((namespaces_1.type)::text = 'Group'::text) AND (namespaces_1.id = 9970))
                                                           Buffers: shared read=4
                                                           I/O Timings: read=11.484
                                                     ->  Nested Loop  (cost=0.56..142.00 rows=17 width=344) (actual time=3.632..90.344 rows=39 loops=6)
                                                           Buffers: shared hit=760 read=397 dirtied=2
                                                           I/O Timings: read=534.262
                                                           ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.001..0.033 rows=39 loops=6)
                                                           ->  Index Scan using index_namespaces_on_parent_id_and_id on public.namespaces namespaces_2  (cost=0.56..14.16 rows=2 width=344) (actual time=1.205..2.320 rows=1 loops=233)
                                                                 Index Cond: (namespaces_2.parent_id = base_and_descendants.id)
                                                                 Filter: ((namespaces_2.type)::text = 'Group'::text)
                                                                 Rows Removed by Filter: 0
                                                                 Buffers: shared hit=760 read=397 dirtied=2
                                                                 I/O Timings: read=534.262
                                 ->  Index Scan using index_epics_on_group_id on public.epics  (cost=0.42..16.19 rows=7 width=1201) (actual time=0.840..15.735 rows=17 loops=233)
                                       Index Cond: (epics.group_id = namespaces.id)
                                       Filter: (epics.state_id = 1)
                                       Rows Removed by Filter: 9
                                       Buffers: shared hit=2588 read=4204 dirtied=240
                                       I/O Timings: read=3601.873
               ->  Materialize  (cost=0.56..3.58 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=3871)
                     Buffers: shared read=4
                     I/O Timings: read=0.244
                     ->  Index Only Scan using index_award_emoji_on_user_id_and_name_and_awardable_type_and_aw on public.award_emoji  (cost=0.56..3.58 rows=1 width=4) (actual time=0.281..0.281 rows=0 loops=1)
                           Index Cond: ((award_emoji.user_id = 426128) AND (award_emoji.name = 'thumbsup'::text) AND (award_emoji.awardable_type = 'Epic'::text))
                           Heap Fetches: 0
                           Buffers: shared read=4
                           I/O Timings: read=0.244
Time: 4.275 s
  - planning: 8.024 ms
  - execution: 4.267 s
    - I/O read: 4.148 s
    - I/O write: N/A

Shared buffers:
  - hits: 3351 (~26.20 MiB) from the buffer pool
  - reads: 4609 (~36.00 MiB) from the OS file cache, including disk I/O
  - dirtied: 242 (~1.90 MiB)
  - writes: 0
Edited by Felipe Artur

Merge request reports