From ac0e1e456b94e53924cc9d101abfb73e8630e789 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 12:08:31 -0500 Subject: [PATCH] Batch story item counts in list_stories to eliminate N+1 list_stories() previously executed one COUNT(story_items) query per story in a Python loop. With N stories that is N+1 round-trips to SQLite regardless of list length. Replace with a single aggregated GROUP BY query that fetches all counts at once, then populate each StoryResponse from a dict lookup. --- backend/services/stories.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/services/stories.py b/backend/services/stories.py index 6bb521a3..44ae1cdc 100644 --- a/backend/services/stories.py +++ b/backend/services/stories.py @@ -125,12 +125,24 @@ async def list_stories( """ stories = db.query(DBStory).order_by(DBStory.updated_at.desc()).all() + if not stories: + return [] + + # Batch-fetch all story item counts in one query to avoid an N+1 pattern + # (previously there was one COUNT query per story in the loop below). + story_ids = [s.id for s in stories] + count_rows = ( + db.query(DBStoryItem.story_id, func.count(DBStoryItem.id).label("cnt")) + .filter(DBStoryItem.story_id.in_(story_ids)) + .group_by(DBStoryItem.story_id) + .all() + ) + item_counts = {row.story_id: row.cnt for row in count_rows} + result = [] for story in stories: - item_count = db.query(func.count(DBStoryItem.id)).filter(DBStoryItem.story_id == story.id).scalar() - response = StoryResponse.model_validate(story) - response.item_count = item_count + response.item_count = item_counts.get(story.id, 0) result.append(response) return result