fix: use group_by instead of find_many(distinct) in /tag/list to avoid full table scan#23136
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a significant performance regression in the Key improvements:
Confidence Score: 5/5
Last reviewed commit: 2c1c6f1 |
tests/test_litellm/proxy/management_endpoints/test_tag_management_endpoints.py
Show resolved
Hide resolved
tests/test_litellm/proxy/management_endpoints/test_tag_management_endpoints.py
Outdated
Show resolved
Hide resolved
tests/test_litellm/proxy/management_endpoints/test_tag_management_endpoints.py
Show resolved
Hide resolved
…d full table scan
The list_tags endpoint used Prisma's find_many(distinct=["tag"]) to discover
dynamic tags from LiteLLM_DailyTagSpend. Prisma's distinct is a client-side
post-processing filter that fetches all rows with all columns before
deduplicating, causing a full table scan of 3M+ rows on every usage page load.
Replace with Prisma's group_by() which generates proper GROUP BY SQL, letting
the database handle deduplication efficiently. NULL tags are filtered out via
where={"tag": {"not": None}}, and created_at/updated_at are preserved via
_min/_max aggregates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e905c9b to
2c1c6f1
Compare
| "created_at": row.get("_min", {}).get("created_at").isoformat() if row.get("_min", {}).get("created_at") else None, | ||
| "updated_at": row.get("_max", {}).get("updated_at").isoformat() if row.get("_max", {}).get("updated_at") else None, |
There was a problem hiding this comment.
Nitpick:
I don't think the if-else guard is needed here. The group_by query returns "dynamically" created tags which are only created by the spend tracking system and are guaranteed to have a spend row corresponding to them.
| # Verify group_by was used instead of find_many(distinct=...) | ||
| mock_db.litellm_dailytagspend.group_by.assert_called_once_with( | ||
| by=["tag"], | ||
| where={"tag": {"not": None}}, | ||
| _min={"created_at": True}, | ||
| _max={"updated_at": True}, | ||
| ) |
There was a problem hiding this comment.
Thank you for adding this test but let's update it to only test the response and not the exact db call that's being made.
- Remove unused LiteLLM_DailyTagSpendTable class and datetime import - Simplify created_at/updated_at access (values guaranteed by spend system) - Remove DB call assertions from tests, keep response-only assertions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d398d1f
into
BerriAI:litellm_oss_staging_03_11_2026
Summary
/tag/listendpoint usedfind_many(distinct=["tag"])onLiteLLM_DailyTagSpendto discover dynamic tags. Prisma'sdistinctis a client-side post-processing filter that fetches all rows with all columns before deduplicating, causing a full table scan of 3M+ rows on every usage page load.group_by()which generates properGROUP BYSQL, letting the database handle deduplication efficiently.where={"tag": {"not": None}}.created_atandupdated_atare preserved in the response via_min/_maxaggregates.list_tagsendpoint.Alternative: single raw SQL query
If raw SQL is acceptable, the entire endpoint could be simplified to a single query using
UNION ALL, eliminating the need for two separate queries and Python-side deduplication:This pushes all filtering and deduplication into the database. The
NOT INsubquery is safe here becausetag_nameis the primary key ofLiteLLM_TagTable(never NULL). This approach goes against the project's CLAUDE.md guideline to prefer Prisma model methods over raw SQL, so the current PR usesgroup_by()instead.Test plan
test_list_tags_with_dynamic_tags— verifiesgroup_byis called, dynamic tags are merged, duplicates with stored tags are excluded, andcreated_at/updated_atare presenttest_list_tags_no_dynamic_tags— verifies correct behavior when no dynamic tags existtest_tag_management_endpoints.pycontinue to pass (12/12)🤖 Generated with Claude Code