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.
This commit is contained in:
Shivam Rawat 2026-08-18 14:17:17 -07:00
parent c6fad3683d
commit ab3bcb95ef
4 changed files with 53 additions and 0 deletions

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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"
)