From 64fc5166174d5d47fd055d55c0d88a67a9a95196 Mon Sep 17 00:00:00 2001 From: David Riddle Date: Mon, 30 Mar 2026 13:05:06 -0500 Subject: [PATCH] Add composite indexes to events table to improve query performance We observed that certain queries were taking 30+ seconds on a large, production foundation. After exploring what was happening, we managed to cut query time down to sub-millisecond by adding a few composite indexes. --- ...4120000_add_composite_indexes_to_events.rb | 72 +++++++++++++++++++ ...00_add_composite_indexes_to_events_spec.rb | 47 ++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 db/migrations/20260324120000_add_composite_indexes_to_events.rb create mode 100644 spec/migrations/20260324120000_add_composite_indexes_to_events_spec.rb diff --git a/db/migrations/20260324120000_add_composite_indexes_to_events.rb b/db/migrations/20260324120000_add_composite_indexes_to_events.rb new file mode 100644 index 0000000000..5a6058ee56 --- /dev/null +++ b/db/migrations/20260324120000_add_composite_indexes_to_events.rb @@ -0,0 +1,72 @@ +# rubocop:disable Metrics/BlockLength +Sequel.migration do + no_transaction # required for concurrently option on postgres + + up do + if database_type == :postgres + VCAP::Migration.with_concurrent_timeout(self) do + add_index :events, %i[actee created_at guid], + name: :events_actee_created_at_guid_index, + if_not_exists: true, + concurrently: true + + add_index :events, %i[space_guid created_at guid], + name: :events_space_guid_created_at_guid_index, + if_not_exists: true, + concurrently: true + + add_index :events, %i[organization_guid created_at guid], + name: :events_organization_guid_created_at_guid_index, + if_not_exists: true, + concurrently: true + end + else + alter_table(:events) do + # rubocop:disable Sequel/ConcurrentIndex + unless @db.indexes(:events).key?(:events_actee_created_at_guid_index) + add_index %i[actee created_at guid], + name: :events_actee_created_at_guid_index + end + unless @db.indexes(:events).key?(:events_space_guid_created_at_guid_index) + add_index %i[space_guid created_at guid], + name: :events_space_guid_created_at_guid_index + end + unless @db.indexes(:events).key?(:events_organization_guid_created_at_guid_index) + add_index %i[organization_guid created_at guid], + name: :events_organization_guid_created_at_guid_index + end + # rubocop:enable Sequel/ConcurrentIndex + end + end + end + + down do + if database_type == :postgres + VCAP::Migration.with_concurrent_timeout(self) do + drop_index :events, nil, + name: :events_actee_created_at_guid_index, + if_exists: true, + concurrently: true + + drop_index :events, nil, + name: :events_space_guid_created_at_guid_index, + if_exists: true, + concurrently: true + + drop_index :events, nil, + name: :events_organization_guid_created_at_guid_index, + if_exists: true, + concurrently: true + end + else + alter_table(:events) do + # rubocop:disable Sequel/ConcurrentIndex + drop_index nil, name: :events_actee_created_at_guid_index if @db.indexes(:events).key?(:events_actee_created_at_guid_index) + drop_index nil, name: :events_space_guid_created_at_guid_index if @db.indexes(:events).key?(:events_space_guid_created_at_guid_index) + drop_index nil, name: :events_organization_guid_created_at_guid_index if @db.indexes(:events).key?(:events_organization_guid_created_at_guid_index) + # rubocop:enable Sequel/ConcurrentIndex + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/migrations/20260324120000_add_composite_indexes_to_events_spec.rb b/spec/migrations/20260324120000_add_composite_indexes_to_events_spec.rb new file mode 100644 index 0000000000..a2c39d5879 --- /dev/null +++ b/spec/migrations/20260324120000_add_composite_indexes_to_events_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' +require 'migrations/helpers/migration_shared_context' + +RSpec.describe 'migration to add composite indexes to events table', isolation: :truncation, type: :migration do + include_context 'migration' do + let(:migration_filename) { '20260324120000_add_composite_indexes_to_events.rb' } + end + + describe 'events table' do + it 'adds composite indexes and handles idempotency gracefully' do + # Before migration: composite indexes should not exist + expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index) + + # Test up migration + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error + + expect(db.indexes(:events)).to include(:events_actee_created_at_guid_index) + expect(db.indexes(:events)).to include(:events_space_guid_created_at_guid_index) + expect(db.indexes(:events)).to include(:events_organization_guid_created_at_guid_index) + + # Verify index column order + expect(db.indexes(:events)[:events_actee_created_at_guid_index][:columns]).to eq(%i[actee created_at guid]) + expect(db.indexes(:events)[:events_space_guid_created_at_guid_index][:columns]).to eq(%i[space_guid created_at guid]) + expect(db.indexes(:events)[:events_organization_guid_created_at_guid_index][:columns]).to eq(%i[organization_guid created_at guid]) + + # Test up migration idempotency: running again should not fail + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error + expect(db.indexes(:events)).to include(:events_actee_created_at_guid_index) + expect(db.indexes(:events)).to include(:events_space_guid_created_at_guid_index) + expect(db.indexes(:events)).to include(:events_organization_guid_created_at_guid_index) + + # Test down migration + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error + expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index) + + # Test down migration idempotency: running again should not fail + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error + expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index) + expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index) + end + end +end