Skip to content

Commit b27da11

Browse files
authored
Add composite indexes to events table to improve query performance (#4982)
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.
1 parent 6349d3a commit b27da11

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# rubocop:disable Metrics/BlockLength
2+
Sequel.migration do
3+
no_transaction # required for concurrently option on postgres
4+
5+
up do
6+
if database_type == :postgres
7+
VCAP::Migration.with_concurrent_timeout(self) do
8+
add_index :events, %i[actee created_at guid],
9+
name: :events_actee_created_at_guid_index,
10+
if_not_exists: true,
11+
concurrently: true
12+
13+
add_index :events, %i[space_guid created_at guid],
14+
name: :events_space_guid_created_at_guid_index,
15+
if_not_exists: true,
16+
concurrently: true
17+
18+
add_index :events, %i[organization_guid created_at guid],
19+
name: :events_organization_guid_created_at_guid_index,
20+
if_not_exists: true,
21+
concurrently: true
22+
end
23+
else
24+
alter_table(:events) do
25+
# rubocop:disable Sequel/ConcurrentIndex
26+
unless @db.indexes(:events).key?(:events_actee_created_at_guid_index)
27+
add_index %i[actee created_at guid],
28+
name: :events_actee_created_at_guid_index
29+
end
30+
unless @db.indexes(:events).key?(:events_space_guid_created_at_guid_index)
31+
add_index %i[space_guid created_at guid],
32+
name: :events_space_guid_created_at_guid_index
33+
end
34+
unless @db.indexes(:events).key?(:events_organization_guid_created_at_guid_index)
35+
add_index %i[organization_guid created_at guid],
36+
name: :events_organization_guid_created_at_guid_index
37+
end
38+
# rubocop:enable Sequel/ConcurrentIndex
39+
end
40+
end
41+
end
42+
43+
down do
44+
if database_type == :postgres
45+
VCAP::Migration.with_concurrent_timeout(self) do
46+
drop_index :events, nil,
47+
name: :events_actee_created_at_guid_index,
48+
if_exists: true,
49+
concurrently: true
50+
51+
drop_index :events, nil,
52+
name: :events_space_guid_created_at_guid_index,
53+
if_exists: true,
54+
concurrently: true
55+
56+
drop_index :events, nil,
57+
name: :events_organization_guid_created_at_guid_index,
58+
if_exists: true,
59+
concurrently: true
60+
end
61+
else
62+
alter_table(:events) do
63+
# rubocop:disable Sequel/ConcurrentIndex
64+
drop_index nil, name: :events_actee_created_at_guid_index if @db.indexes(:events).key?(:events_actee_created_at_guid_index)
65+
drop_index nil, name: :events_space_guid_created_at_guid_index if @db.indexes(:events).key?(:events_space_guid_created_at_guid_index)
66+
drop_index nil, name: :events_organization_guid_created_at_guid_index if @db.indexes(:events).key?(:events_organization_guid_created_at_guid_index)
67+
# rubocop:enable Sequel/ConcurrentIndex
68+
end
69+
end
70+
end
71+
end
72+
# rubocop:enable Metrics/BlockLength
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/migration_shared_context'
3+
4+
RSpec.describe 'migration to add composite indexes to events table', isolation: :truncation, type: :migration do
5+
include_context 'migration' do
6+
let(:migration_filename) { '20260324120000_add_composite_indexes_to_events.rb' }
7+
end
8+
9+
describe 'events table' do
10+
it 'adds composite indexes and handles idempotency gracefully' do
11+
# Before migration: composite indexes should not exist
12+
expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index)
13+
expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index)
14+
expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index)
15+
16+
# Test up migration
17+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
18+
19+
expect(db.indexes(:events)).to include(:events_actee_created_at_guid_index)
20+
expect(db.indexes(:events)).to include(:events_space_guid_created_at_guid_index)
21+
expect(db.indexes(:events)).to include(:events_organization_guid_created_at_guid_index)
22+
23+
# Verify index column order
24+
expect(db.indexes(:events)[:events_actee_created_at_guid_index][:columns]).to eq(%i[actee created_at guid])
25+
expect(db.indexes(:events)[:events_space_guid_created_at_guid_index][:columns]).to eq(%i[space_guid created_at guid])
26+
expect(db.indexes(:events)[:events_organization_guid_created_at_guid_index][:columns]).to eq(%i[organization_guid created_at guid])
27+
28+
# Test up migration idempotency: running again should not fail
29+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
30+
expect(db.indexes(:events)).to include(:events_actee_created_at_guid_index)
31+
expect(db.indexes(:events)).to include(:events_space_guid_created_at_guid_index)
32+
expect(db.indexes(:events)).to include(:events_organization_guid_created_at_guid_index)
33+
34+
# Test down migration
35+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error
36+
expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index)
37+
expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index)
38+
expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index)
39+
40+
# Test down migration idempotency: running again should not fail
41+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error
42+
expect(db.indexes(:events)).not_to include(:events_actee_created_at_guid_index)
43+
expect(db.indexes(:events)).not_to include(:events_space_guid_created_at_guid_index)
44+
expect(db.indexes(:events)).not_to include(:events_organization_guid_created_at_guid_index)
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)