From ab3bcb95efa2dc77740d3b87b71d6884082632b5 Mon Sep 17 00:00:00 2001 From: Shivam Rawat Date: Tue, 18 Aug 2026 14:17:17 -0700 Subject: [PATCH] perf(db): index LiteLLM_SpendLogs on (api_key, startTime) The Logs tab filters LiteLLM_SpendLogs by api_key inside a startTime window. The table only had indexes on startTime, (startTime, request_id), end_user and session_id, so the api_key predicate degraded to a full scan of the window on both the pagination count and the paged select, re-run on every page turn. On a multi-month table that pinned a customer's Aurora writer near 100% CPU for ~50 minutes. Built CONCURRENTLY: this is the largest table on most deployments, and a blocking build would stall spend-log inserts for the whole build. --- .../migration.sql | 11 +++++ .../litellm_proxy_extras/schema.prisma | 1 + schema.prisma | 1 + .../test_db_schema_migration.py | 40 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260818000000_add_spendlogs_api_key_start_time_idx/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260818000000_add_spendlogs_api_key_start_time_idx/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260818000000_add_spendlogs_api_key_start_time_idx/migration.sql new file mode 100644 index 00000000000..1d57e456e08 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260818000000_add_spendlogs_api_key_start_time_idx/migration.sql @@ -0,0 +1,11 @@ +-- CreateIndex (CONCURRENTLY) +-- +-- Disclaimer: +-- - CREATE INDEX CONCURRENTLY cannot run inside a transaction. This migration must stay a +-- single statement so Prisma Migrate on PostgreSQL can apply it outside a transaction. +-- - "LiteLLM_SpendLogs" is the largest table on most deployments, so a blocking build would +-- stall spend-log inserts for the whole build; if the build is interrupted, Postgres may +-- leave an INVALID index that must be dropped and recreated. +-- - Do not edit this file after it has been applied to any database: Prisma checksums +-- migrations; add a new migration instead. +CREATE INDEX CONCURRENTLY IF NOT EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" ON "LiteLLM_SpendLogs"("api_key", "startTime" DESC); diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 71345d2ccde..ae173cb49b8 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -645,6 +645,7 @@ model LiteLLM_SpendLogs { @@index([startTime, request_id]) @@index([end_user]) @@index([session_id]) + @@index([api_key, startTime(sort: Desc)], map: "LiteLLM_SpendLogs_api_key_startTime_idx") } // View spend, model, api_key per request diff --git a/schema.prisma b/schema.prisma index 71345d2ccde..ae173cb49b8 100644 --- a/schema.prisma +++ b/schema.prisma @@ -645,6 +645,7 @@ model LiteLLM_SpendLogs { @@index([startTime, request_id]) @@index([end_user]) @@index([session_id]) + @@index([api_key, startTime(sort: Desc)], map: "LiteLLM_SpendLogs_api_key_startTime_idx") } // View spend, model, api_key per request diff --git a/tests/proxy_migration_tests/test_db_schema_migration.py b/tests/proxy_migration_tests/test_db_schema_migration.py index b0d44cd3e1c..0d2eced5620 100644 --- a/tests/proxy_migration_tests/test_db_schema_migration.py +++ b/tests/proxy_migration_tests/test_db_schema_migration.py @@ -68,3 +68,43 @@ def test_schema_migration_in_sync(): assert diff.returncode == 0, f"prisma migrate diff errored: {diff.stderr}" finally: shutil.rmtree(temp_base, ignore_errors=True) + + +SPEND_LOGS_API_KEY_INDEX = "LiteLLM_SpendLogs_api_key_startTime_idx" + +SCHEMA_PATHS = ( + Path("./schema.prisma"), + Path("./litellm-proxy-extras/litellm_proxy_extras/schema.prisma"), +) + + +def _spend_logs_model_block(schema: str) -> str: + start = schema.index("model LiteLLM_SpendLogs {") + return schema[start : schema.index("\n}", start)] + + +@pytest.mark.parametrize("schema_path", SCHEMA_PATHS, ids=lambda p: str(p)) +def test_spend_logs_declares_api_key_index(schema_path: Path): + """The Logs tab filters ``LiteLLM_SpendLogs`` by ``api_key`` inside a ``startTime`` + window. Without an index led by ``api_key`` that degrades to a full scan of the + window on every page turn, which pinned a customer's Aurora writer at ~99% CPU. + """ + block = _spend_logs_model_block(schema_path.read_text()) + assert f'@@index([api_key, startTime(sort: Desc)], map: "{SPEND_LOGS_API_KEY_INDEX}")' in block, ( + f"{schema_path} must keep the api_key-led index on LiteLLM_SpendLogs" + ) + + +def test_spend_logs_api_key_index_has_a_migration(): + """A schema-only index never reaches deployments that run ``prisma migrate deploy``.""" + migrations = Path("./litellm-proxy-extras/litellm_proxy_extras/migrations") + creating = [ + sql + for sql in (path.read_text() for path in migrations.glob("*/migration.sql")) + if SPEND_LOGS_API_KEY_INDEX in sql + ] + assert len(creating) == 1, f"expected exactly one migration creating {SPEND_LOGS_API_KEY_INDEX}, found {len(creating)}" + assert 'ON "LiteLLM_SpendLogs"("api_key", "startTime" DESC)' in creating[0] + assert "CONCURRENTLY" in creating[0], ( + "LiteLLM_SpendLogs is the largest table on most deployments; a blocking build stalls spend-log inserts" + )