Skip to content

Improve performance of changeset cleaner

Administrator requested to merge es/changeset-cleaner-perf into main

Created by: eseliger

  • Doesn't need to run that often.
  • Improve query performance; tl;dr it got down from 59192ms to 7ms. (it was the #1 worst query in k8s)

Before:

explain analyze
DELETE FROM
  changeset_specs cspecs
WHERE
(
  -- The spec is older than the ChangesetSpecTTL
  created_at < now() - interval '3 days'
  AND
  -- and it was never attached to a batch_spec
  batch_spec_id IS NULL
)
OR
(
  -- The spec is older than the BatchSpecTTL
  created_at < now() - interval '8 days'
  AND
  -- and the batch_spec it is attached to is not applied to a batch_change
  NOT EXISTS (SELECT 1 FROM batch_changes WHERE batch_spec_id = cspecs.batch_spec_id)
  AND
  -- and it is not attached to a changeset
  NOT EXISTS (SELECT 1 FROM changesets WHERE current_spec_id = cspecs.id OR previous_spec_id = cspecs.id)
  AND
  -- and it is not created by SSBC
  NOT (SELECT created_from_raw FROM batch_specs WHERE id = cspecs.batch_spec_id)
);

Plan:

                                                                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on changeset_specs cspecs  (cost=2651.72..9714245.14 rows=30361 width=6) (actual time=59182.238..59182.244 rows=0 loops=1)
   ->  Bitmap Heap Scan on changeset_specs cspecs  (cost=2651.72..9714245.14 rows=30361 width=6) (actual time=59182.235..59182.241 rows=0 loops=1)
         Recheck Cond: ((batch_spec_id IS NULL) OR (created_at < (now() - '8 days'::interval)))
         Filter: (((created_at < (now() - '3 days'::interval)) AND (batch_spec_id IS NULL)) OR ((created_at < (now() - '8 days'::interval)) AND (NOT (alternatives: SubPlan 1 or hashed SubPlan 2)) AND (NOT (SubPlan 3)) AND (NOT (SubPlan 4))))
         Rows Removed by Filter: 218455
         Heap Blocks: exact=28308
         ->  BitmapOr  (cost=2651.72..2651.72 rows=242891 width=0) (actual time=689.638..689.640 rows=0 loops=1)
               ->  Bitmap Index Scan on changeset_specs_batch_spec_id  (cost=0.00..1.53 rows=1 width=0) (actual time=0.043..0.043 rows=0 loops=1)
                     Index Cond: (batch_spec_id IS NULL)
               ->  Bitmap Index Scan on changeset_specs_created_at  (cost=0.00..2635.01 rows=242891 width=0) (actual time=689.593..689.593 rows=218455 loops=1)
                     Index Cond: (created_at < (now() - '8 days'::interval))
         SubPlan 1
           ->  Seq Scan on batch_changes  (cost=0.00..7.24 rows=1 width=0) (never executed)
                 Filter: (batch_spec_id = cspecs.batch_spec_id)
         SubPlan 2
           ->  Seq Scan on batch_changes batch_changes_1  (cost=0.00..6.59 rows=259 width=8) (actual time=37.595..37.677 rows=260 loops=1)
         SubPlan 3
           ->  Index Only Scan using changesets_changeset_specs on changesets  (cost=0.28..59.84 rows=2 width=0) (actual time=0.260..0.260 rows=0 loops=217483)
                 Filter: ((current_spec_id = cspecs.id) OR (previous_spec_id = cspecs.id))
                 Rows Removed by Filter: 1452
                 Heap Fetches: 63158658
         SubPlan 4
           ->  Index Scan using batch_specs_pkey on batch_specs  (cost=0.28..2.49 rows=1 width=1) (actual time=0.003..0.003 rows=1 loops=216985)
                 Index Cond: (id = cspecs.batch_spec_id)
 Planning Time: 0.438 ms
 JIT:
   Functions: 34
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 9.215 ms, Inlining 23.278 ms, Optimization 395.151 ms, Emission 276.956 ms, Total 704.600 ms
 Execution Time: 59191.755 ms
(30 rows)

After:

Part 1:

explain analyze
DELETE FROM
  changeset_specs
WHERE
  -- The spec is older than the ChangesetSpecTTL
  created_at < now() - interval '3 days'
  AND
  -- and it was never attached to a batch_spec
  batch_spec_id IS NULL;

Plan:

                                                                     QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on changeset_specs  (cost=0.42..1.92 rows=1 width=6) (actual time=0.058..0.058 rows=0 loops=1)
   ->  Index Scan using changeset_specs_batch_spec_id on changeset_specs  (cost=0.42..1.92 rows=1 width=6) (actual time=0.057..0.057 rows=0 loops=1)
         Index Cond: (batch_spec_id IS NULL)
         Filter: (created_at < (now() - '3 days'::interval))
 Planning Time: 0.116 ms
 Execution Time: 0.083 ms
(6 rows)

Part 2:

explain analyze
WITH candidates AS (
  SELECT cs.id
  FROM changeset_specs cs
  JOIN batch_specs bs ON bs.id = cs.batch_spec_id
  LEFT JOIN batch_changes bc ON bs.id = bc.batch_spec_id
  LEFT JOIN changesets c ON (c.current_spec_id = cs.id OR c.previous_spec_id = cs.id)
  WHERE
    -- The spec is older than the BatchSpecTTL
    cs.created_at < now() - interval '8 days'
    -- and it is not created by SSBC
    AND NOT bs.created_from_raw
    -- and the batch spec it is attached to is not applied to a batch change
    AND bc.id IS NULL
    -- and it is not attached to a changeset
    AND c.id IS NULL
  FOR UPDATE OF cs
)
DELETE FROM changeset_specs
WHERE
  id IN (SELECT id FROM candidates);

Plan:

                                                                                    QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on changeset_specs  (cost=791.93..794.16 rows=1 width=38) (actual time=6.355..6.361 rows=0 loops=1)
   CTE candidates
     ->  LockRows  (cost=122.41..791.49 rows=1 width=32) (actual time=6.350..6.354 rows=0 loops=1)
           ->  Nested Loop Left Join  (cost=122.41..791.48 rows=1 width=32) (actual time=6.349..6.353 rows=0 loops=1)
                 Filter: (c.id IS NULL)
                 Rows Removed by Filter: 183
                 ->  Nested Loop  (cost=110.94..778.86 rows=1 width=26) (actual time=0.555..0.859 rows=183 loops=1)
                       ->  Hash Right Join  (cost=110.52..117.79 rows=1 width=20) (actual time=0.522..0.530 rows=8 loops=1)
                             Hash Cond: (bc.batch_spec_id = bs.id)
                             Filter: (bc.id IS NULL)
                             Rows Removed by Filter: 27
                             ->  Seq Scan on batch_changes bc  (cost=0.00..6.59 rows=259 width=22) (actual time=0.024..0.085 rows=260 loops=1)
                             ->  Hash  (cost=110.08..110.08 rows=35 width=14) (actual time=0.386..0.387 rows=35 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                   ->  Seq Scan on batch_specs bs  (cost=0.00..110.08 rows=35 width=14) (actual time=0.014..0.364 rows=35 loops=1)
                                         Filter: (NOT created_from_raw)
                                         Rows Removed by Filter: 596
                       ->  Index Scan using changeset_specs_batch_spec_id on changeset_specs cs  (cost=0.42..644.27 rows=1680 width=22) (actual time=0.009..0.037 rows=23 loops=8)
                             Index Cond: (batch_spec_id = bs.id)
                             Filter: (created_at < (now() - '8 days'::interval))
                             Rows Removed by Filter: 1
                 ->  Bitmap Heap Scan on changesets c  (cost=11.47..12.60 rows=2 width=30) (actual time=0.029..0.029 rows=1 loops=183)
                       Recheck Cond: ((current_spec_id = cs.id) OR (previous_spec_id = cs.id))
                       Heap Blocks: exact=183
                       ->  BitmapOr  (cost=11.47..11.47 rows=2 width=0) (actual time=0.027..0.027 rows=0 loops=183)
                             ->  Bitmap Index Scan on changesets_changeset_specs  (cost=0.00..0.29 rows=1 width=0) (actual time=0.001..0.001 rows=0 loops=183)
                                   Index Cond: (current_spec_id = cs.id)
                             ->  Bitmap Index Scan on changesets_changeset_specs  (cost=0.00..11.18 rows=1 width=0) (actual time=0.026..0.026 rows=1 loops=183)
                                   Index Cond: (previous_spec_id = cs.id)
   ->  Nested Loop  (cost=0.45..2.67 rows=1 width=38) (actual time=6.353..6.355 rows=0 loops=1)
         ->  HashAggregate  (cost=0.02..0.03 rows=1 width=40) (actual time=6.353..6.354 rows=0 loops=1)
               Group Key: candidates.id
               ->  CTE Scan on candidates  (cost=0.00..0.02 rows=1 width=40) (actual time=6.351..6.352 rows=0 loops=1)
         ->  Index Scan using changeset_specs_pkey on changeset_specs  (cost=0.42..2.64 rows=1 width=14) (never executed)
               Index Cond: (id = candidates.id)
 Planning Time: 1.080 ms
 Execution Time: 6.624 ms
(37 rows)

Test plan

Query plans attached above, and test suite adjusted.

Merge request reports

Loading