From 1bdc1b9b174779db39f2220a4cd7449321efca35 Mon Sep 17 00:00:00 2001 From: Etienne Chabert Date: Sun, 23 Aug 2026 01:44:49 +0200 Subject: [PATCH 1/2] perf(spend_tracking): index LiteLLM_SpendLogs by (api_key, startTime) Every per-key spend read filters WHERE api_key = ... AND startTime in a range (/spend/logs?api_key=..., the key-filtered UI logs page, external billing readers), but LiteLLM_SpendLogs carries no api_key index, so each such query scans every logged request on the instance. Composite with startTime to match the query shape, mirroring the existing (startTime, request_id) composite. Measured on Postgres 18 with 1.6M spend rows: one key's 7-day SUM goes from a 93.9ms parallel seq scan to a 0.7ms bitmap index scan (~140x); a batch job reading per-key spend for 10k keys went from 20.5s to 1.7s. --- .../migration.sql | 2 ++ litellm-proxy-extras/litellm_proxy_extras/schema.prisma | 1 + litellm/proxy/schema.prisma | 1 + schema.prisma | 1 + 4 files changed, 5 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260823000000_add_spend_logs_api_key_starttime_index/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260823000000_add_spend_logs_api_key_starttime_index/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260823000000_add_spend_logs_api_key_starttime_index/migration.sql new file mode 100644 index 00000000000..9a061aaed43 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260823000000_add_spend_logs_api_key_starttime_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" ON "LiteLLM_SpendLogs"("api_key", "startTime"); diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 5582cf930d7..78e1631e114 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -647,6 +647,7 @@ model LiteLLM_SpendLogs { @@index([startTime, request_id]) @@index([end_user]) @@index([session_id]) + @@index([api_key, startTime]) } // View spend, model, api_key per request diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 5582cf930d7..78e1631e114 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -647,6 +647,7 @@ model LiteLLM_SpendLogs { @@index([startTime, request_id]) @@index([end_user]) @@index([session_id]) + @@index([api_key, startTime]) } // View spend, model, api_key per request diff --git a/schema.prisma b/schema.prisma index 5582cf930d7..78e1631e114 100644 --- a/schema.prisma +++ b/schema.prisma @@ -647,6 +647,7 @@ model LiteLLM_SpendLogs { @@index([startTime, request_id]) @@index([end_user]) @@index([session_id]) + @@index([api_key, startTime]) } // View spend, model, api_key per request From 4c18f557b1c08948348bfb348658cce355ebe6a3 Mon Sep 17 00:00:00 2001 From: Etienne Chabert Date: Tue, 1 Sep 2026 16:50:15 +0200 Subject: [PATCH 2/2] fix(db_scripts): carry the new spend index through the partition runbooks partition_spend_logs.sql and unpartition_spend_logs.sql hardcode every index Prisma defines on LiteLLM_SpendLogs, because `LIKE ... INCLUDING DEFAULTS INCLUDING GENERATED` copies columns but not indexes. They also rename the old table's indexes aside first, since index names are unique per schema and a surviving name makes `CREATE INDEX IF NOT EXISTS` a silent no-op. The new (api_key, startTime) index was in neither list, so an operator who partitions (or unpartitions) after this migration lands gets a replacement table without it, and Prisma will not recreate it: the migration is already recorded as applied, and `migrate deploy` skips its drift sanity check when there is nothing pending. Verified on Postgres 18 against the shipped migration statements: * unpatched partition script -> parent table has no api_key index, and re-running the migration's own `CREATE INDEX IF NOT EXISTS` reports success while being skipped, because the legacy table still owns the name. The recovery an operator would reach for silently does nothing. * patched -> index survives partitioning, propagates to every partition (LiteLLM_SpendLogs_p*_api_key_startTime_idx) and to DEFAULT, is chosen by the planner for the key+date-range query shape the endpoints use, and survives the unpartition round-trip with all rows intact. --- db_scripts/partition_spend_logs.sql | 5 +++++ db_scripts/unpartition_spend_logs.sql | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/db_scripts/partition_spend_logs.sql b/db_scripts/partition_spend_logs.sql index 4e4a93539d7..c153a67eaec 100644 --- a/db_scripts/partition_spend_logs.sql +++ b/db_scripts/partition_spend_logs.sql @@ -53,6 +53,8 @@ ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_end_user_idx" RENAME TO "LiteLLM_SpendLogs_legacy_end_user_idx"; ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_session_id_idx" RENAME TO "LiteLLM_SpendLogs_legacy_session_id_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" + RENAME TO "LiteLLM_SpendLogs_legacy_api_key_startTime_idx"; CREATE TABLE "LiteLLM_SpendLogs" ( LIKE "LiteLLM_SpendLogs_legacy" INCLUDING DEFAULTS INCLUDING GENERATED @@ -78,6 +80,9 @@ CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_end_user_idx" CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_session_id_idx" ON "LiteLLM_SpendLogs" ("session_id"); +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" + ON "LiteLLM_SpendLogs" ("api_key", "startTime"); + -- Safety net: any row whose startTime has no explicit partition lands here so -- writes never fail. The cleanup job never drops the DEFAULT partition. CREATE TABLE IF NOT EXISTS "LiteLLM_SpendLogs_pdefault" diff --git a/db_scripts/unpartition_spend_logs.sql b/db_scripts/unpartition_spend_logs.sql index 0bd82513e4a..2555eca212b 100644 --- a/db_scripts/unpartition_spend_logs.sql +++ b/db_scripts/unpartition_spend_logs.sql @@ -40,6 +40,8 @@ ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_end_user_idx" RENAME TO "LiteLLM_SpendLogs_partitioned_end_user_idx"; ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_session_id_idx" RENAME TO "LiteLLM_SpendLogs_partitioned_session_id_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" + RENAME TO "LiteLLM_SpendLogs_partitioned_api_key_startTime_idx"; CREATE TABLE "LiteLLM_SpendLogs" ( LIKE "LiteLLM_SpendLogs_partitioned" INCLUDING DEFAULTS INCLUDING GENERATED @@ -60,6 +62,9 @@ CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_end_user_idx" CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_session_id_idx" ON "LiteLLM_SpendLogs" ("session_id"); +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_api_key_startTime_idx" + ON "LiteLLM_SpendLogs" ("api_key", "startTime"); + INSERT INTO "LiteLLM_SpendLogs" SELECT * FROM "LiteLLM_SpendLogs_partitioned" ON CONFLICT ("request_id") DO NOTHING;