Skip to content

perf: add indexes to improve query performance in users, user_traffic, hosts, and internal_squad_inbounds tables#153

Open
ZeN220 wants to merge 1 commit intoremnawave:devfrom
ZeN220:dev
Open

perf: add indexes to improve query performance in users, user_traffic, hosts, and internal_squad_inbounds tables#153
ZeN220 wants to merge 1 commit intoremnawave:devfrom
ZeN220:dev

Conversation

@ZeN220
Copy link

@ZeN220 ZeN220 commented Mar 23, 2026

added indexes for more perfomance

  • users: [status, expire_at], [traffic_limit_strategy, status], [expire_at], [external_squad_uuid], [telegram_id], [created_at] — cron tasks/users dashboard/get user by param in API
  • user_traffic: [online_at] — online calculation
  • hosts: [config_profile_inbound_uuid] — hot path for subscription generation
  • internal_squad_inbounds: [inbound_uuid] — hot path for subscription generation

…fic`, `hosts`, and `internal_squad_inbounds` tables
@snyk-io
Copy link

snyk-io bot commented Mar 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@what-the-diff
Copy link

what-the-diff bot commented Mar 23, 2026

PR Summary

  • Introduction of New Migration File
    A new file named migration.sql has been created. This file serves the purpose of adding several indexes to a variety of tables in our database. These values will help speed up search operations on these tables. The tables that are impacted are:

    • users table: New indexes added on status, expire_at, traffic_limit_strategy, external_squad_uuid, telegram_id and created_at fields.
    • user_traffic table: New index added on online_at field.
    • hosts table: New index added on config_profile_inbound_uuid field.
    • internal_squad_inbounds table: New index added on inbound_uuid field.
  • Modification of schema.prisma File
    The content of the schema.prisma file has been updated. These updates include index annotations for four models. These changes act as a signal to mark where indexing operations are to be performed in the respective tables. The impacted models are:

    • Users model: Indexes annotations added on several fields.
    • UserTraffic model: Index annotation added on the onlineAt field.
    • Hosts model: Index annotation added on the configProfileInboundUuid field.
    • InternalSquadInbounds model: Index annotation added on the inboundUuid field.

By introducing these changes, we ensure that our database operations become more efficient by allowing quicker access to data across various tables.

@greptile-apps
Copy link

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR adds 9 B-tree indexes across the users, user_traffic, hosts, and internal_squad_inbounds tables to speed up cron jobs, the users dashboard, subscription generation, and online-count queries. The index selection is well-reasoned: composite indexes are ordered correctly (higher-selectivity / leading-filter column first), the standalone (expire_at) index on users is not redundant with (status, expire_at) because it covers queries that filter only on expiry, and the (inbound_uuid) index on internal_squad_inbounds correctly supplements the composite PK which cannot efficiently serve lookups on the trailing column alone.

Key considerations:

  • Blocking migration: The migration uses CREATE INDEX (non-concurrent), which acquires a ShareLock that blocks all writes to the indexed table for the full duration of index creation. With six indexes being built on the potentially large users table, this represents a production write outage. CREATE INDEX CONCURRENTLY (outside a transaction) is the safer approach for live deployments.
  • Nullable sparse columns: telegram_id and external_squad_uuid are nullable; partial indexes restricted to IS NOT NULL rows would be smaller, faster, and more memory-efficient if those columns are sparsely populated.
  • The schema changes in schema.prisma are consistent with the migration SQL and introduce no logical issues.

Confidence Score: 4/5

  • Safe to merge after confirming the table-lock impact is acceptable or after switching to CONCURRENTLY index creation.
  • The index choices are correct and the schema/migration are consistent. The one concrete concern is that plain CREATE INDEX will hold a write lock on the users table in production, which may cause a brief write outage during deployment. This is a production-reliability risk worth addressing before merge, but it does not affect correctness.
  • prisma/migrations/20260323175509_add_performance_indexes/migration.sql — review the CREATE INDEX vs CREATE INDEX CONCURRENTLY choice.

Important Files Changed

Filename Overview
prisma/migrations/20260323175509_add_performance_indexes/migration.sql Adds 9 performance indexes across 4 tables; all use plain CREATE INDEX (non-concurrent), which will block writes on affected tables during the migration run in production.
prisma/schema.prisma Schema indexes are well-chosen and consistent with the migration SQL; composite indexes correctly order high-cardinality columns first, and the standalone (expire_at) index covers queries that don't filter by status.

Entity Relationship Diagram

%%{init: {'theme': 'neutral'}}%%
erDiagram
    users {
        BigInt t_id PK
        String uuid UK
        String status
        String traffic_limit_strategy
        DateTime expire_at
        BigInt telegram_id
        String external_squad_uuid FK
        DateTime created_at
    }
    user_traffic {
        BigInt t_id PK
        DateTime online_at
        BigInt used_traffic_bytes
    }
    hosts {
        String uuid PK
        String config_profile_inbound_uuid FK
        Boolean is_disabled
    }
    internal_squad_inbounds {
        String internal_squad_uuid PK
        String inbound_uuid PK
    }
    config_profile_inbounds {
        String uuid PK
        String profile_uuid FK
    }
    external_squads {
        String uuid PK
    }

    users ||--o| user_traffic : "has traffic"
    users }o--o| external_squads : "belongs to"
    hosts }o--o| config_profile_inbounds : "linked to inbound"
    internal_squad_inbounds }o--|| config_profile_inbounds : "references"
Loading

Reviews (1): Last reviewed commit: "feat: add indexes to improve query perfo..." | Re-trigger Greptile

Comment on lines +1 to +26
-- CreateIndex
CREATE INDEX "users_status_expire_at_idx" ON "users"("status", "expire_at");

-- CreateIndex
CREATE INDEX "users_traffic_limit_strategy_status_idx" ON "users"("traffic_limit_strategy", "status");

-- CreateIndex
CREATE INDEX "users_expire_at_idx" ON "users"("expire_at");

-- CreateIndex
CREATE INDEX "users_external_squad_uuid_idx" ON "users"("external_squad_uuid");

-- CreateIndex
CREATE INDEX "users_telegram_id_idx" ON "users"("telegram_id");

-- CreateIndex
CREATE INDEX "users_created_at_idx" ON "users"("created_at");

-- CreateIndex
CREATE INDEX "user_traffic_online_at_idx" ON "user_traffic"("online_at");

-- CreateIndex
CREATE INDEX "hosts_config_profile_inbound_uuid_idx" ON "hosts"("config_profile_inbound_uuid");

-- CreateIndex
CREATE INDEX "internal_squad_inbounds_inbound_uuid_idx" ON "internal_squad_inbounds"("inbound_uuid");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-concurrent index builds may block writes on production

All nine indexes are created with plain CREATE INDEX, which acquires a ShareLock on each table for the full duration of the build. On a live system with many rows in users or user_traffic, every write to those tables (INSERT/UPDATE/DELETE) will be blocked until the corresponding index is finished. Six indexes are being built on users in a single migration, which is especially risky.

The safe alternative is CREATE INDEX CONCURRENTLY, which builds the index without holding a write lock. The catch is that it cannot run inside a transaction, so Prisma's default transaction wrapping must be bypassed by adding the pragma comment at the top of the file:

-- This migration uses CREATE INDEX CONCURRENTLY which requires running outside a transaction.
-- Prisma wraps migrations in transactions by default; the pragma below disables that.

-- migrate:once pragma: disable-transaction
CREATE INDEX CONCURRENTLY "users_status_expire_at_idx" ON "users"("status", "expire_at");
CREATE INDEX CONCURRENTLY "users_traffic_limit_strategy_status_idx" ON "users"("traffic_limit_strategy", "status");
CREATE INDEX CONCURRENTLY "users_expire_at_idx" ON "users"("expire_at");
CREATE INDEX CONCURRENTLY "users_external_squad_uuid_idx" ON "users"("external_squad_uuid");
CREATE INDEX CONCURRENTLY "users_telegram_id_idx" ON "users"("telegram_id");
CREATE INDEX CONCURRENTLY "users_created_at_idx" ON "users"("created_at");
CREATE INDEX CONCURRENTLY "user_traffic_online_at_idx" ON "user_traffic"("online_at");
CREATE INDEX CONCURRENTLY "hosts_config_profile_inbound_uuid_idx" ON "hosts"("config_profile_inbound_uuid");
CREATE INDEX CONCURRENTLY "internal_squad_inbounds_inbound_uuid_idx" ON "internal_squad_inbounds"("inbound_uuid");

Alternatively, if a maintenance window is acceptable, the existing migration can be kept as-is, but that should be a conscious decision documented in the PR.

Comment on lines +86 to +88
@@index([expireAt])
@@index([externalSquadUuid])
@@index([telegramId])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Consider partial indexes for nullable sparse columns

telegramId and externalSquadUuid are both optional (BigInt? / String?), meaning a significant proportion of rows may store NULL. PostgreSQL still indexes every row — including NULL values — in a plain B-tree index, which wastes space and slows down index scans on large tables.

A partial index restricted to non-null rows is smaller, fits better in memory, and gives the planner a stronger cardinality estimate:

CREATE INDEX "users_telegram_id_idx"       ON "users"("telegram_id")       WHERE telegram_id IS NOT NULL;
CREATE INDEX "users_external_squad_uuid_idx" ON "users"("external_squad_uuid") WHERE external_squad_uuid IS NOT NULL;

This can be expressed in Prisma using a @@index with a raw where argument (via the extendedIndexes preview feature), or by editing the generated SQL directly. Worth considering if the columns are sparse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant