perf: add covering indexes for referral network queries

Add composite indexes on advertising_campaign_registrations(user_id,
created_at) and transactions(user_id, type, is_completed, amount_kopeks)
to enable index-only scans. Uses CREATE INDEX CONCURRENTLY for zero
downtime. Also enable transaction_per_migration in Alembic env.py.
This commit is contained in:
Fringg
2026-03-20 02:31:03 +03:00
parent 6a4ce3dd38
commit b5471b7720
4 changed files with 67 additions and 3 deletions
+5 -1
View File
@@ -1574,6 +1574,7 @@ class Transaction(Base):
Index('ix_transactions_type_created_completed', 'type', 'created_at', 'is_completed'),
Index('ix_transactions_user_created', 'user_id', 'created_at'),
Index('ix_transactions_type_method_created', 'type', 'payment_method', 'created_at'),
Index('ix_transactions_user_type_completed_amount', 'user_id', 'type', 'is_completed', 'amount_kopeks'),
)
id = Column(Integer, primary_key=True, index=True)
@@ -2490,7 +2491,10 @@ class AdvertisingCampaign(Base):
class AdvertisingCampaignRegistration(Base):
__tablename__ = 'advertising_campaign_registrations'
__table_args__ = (UniqueConstraint('campaign_id', 'user_id', name='uq_campaign_user'),)
__table_args__ = (
UniqueConstraint('campaign_id', 'user_id', name='uq_campaign_user'),
Index('ix_campaign_reg_user_created', 'user_id', 'created_at'),
)
id = Column(Integer, primary_key=True, index=True)
campaign_id = Column(Integer, ForeignKey('advertising_campaigns.id', ondelete='CASCADE'), nullable=False)
+5 -1
View File
@@ -45,7 +45,11 @@ def run_migrations_offline() -> None:
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
transaction_per_migration=True,
)
with context.begin_transaction():
context.run_migrations()
@@ -0,0 +1,56 @@
"""add performance indexes for referral network queries
Revision ID: 0041
Revises: 0040
Create Date: 2026-03-20
"""
from typing import Sequence, Union
from alembic import op
revision: str = '0041'
down_revision: Union[str, None] = '0040'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# CREATE INDEX CONCURRENTLY cannot run inside a transaction.
# autocommit_block() temporarily disables the transaction wrapper.
#
# NOTE: If a concurrent index creation fails midway, PostgreSQL leaves behind
# an INVALID index. Check with:
# SELECT indexrelname FROM pg_stat_user_indexes
# JOIN pg_index ON pg_index.indexrelid = pg_stat_user_indexes.indexrelid
# WHERE NOT pg_index.indisvalid;
# Then drop the invalid index and re-run the migration.
with op.get_context().autocommit_block():
# Index on advertising_campaign_registrations(user_id, created_at)
# Fixes sequential scan in _fetch_campaign_registrations which filters by user_id
# and uses ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at)
op.create_index(
'ix_campaign_reg_user_created',
'advertising_campaign_registrations',
['user_id', 'created_at'],
if_not_exists=True,
postgresql_concurrently=True,
)
# Covering composite index on transactions(user_id, type, is_completed, amount_kopeks)
# Enables index-only scans for aggregation queries in referral network stats:
# _fetch_personal_spent, _fetch_branch_revenue, _fetch_campaign_stats
op.create_index(
'ix_transactions_user_type_completed_amount',
'transactions',
['user_id', 'type', 'is_completed', 'amount_kopeks'],
if_not_exists=True,
postgresql_concurrently=True,
)
def downgrade() -> None:
with op.get_context().autocommit_block():
op.execute('DROP INDEX CONCURRENTLY IF EXISTS ix_transactions_user_type_completed_amount')
op.execute('DROP INDEX CONCURRENTLY IF EXISTS ix_campaign_reg_user_created')
Generated
+1 -1
View File
@@ -1115,7 +1115,7 @@ wheels = [
[[package]]
name = "remnawave-bedolaga-telegram-bot"
version = "3.33.0"
version = "3.34.1"
source = { virtual = "." }
dependencies = [
{ name = "aiogram" },