--- id: e014f59b-60ff-4602-91bc-076b3a73f0e8 type: fix title: "Fix: batch PitchingCardRatings lookup in pitcher sort (paper-dynasty-database #19)" tags: [paper-dynasty-database, python, peewee, pandas, performance, fix, n+1-queries] importance: 0.65 confidence: 0.8 created: "2026-03-03T23:04:16.395675+00:00" updated: "2026-03-03T23:04:17.048422+00:00" relations: - target: 6ebf27b5-c1eb-4e4d-b12a-62e7fdbc9406 type: RELATED_TO direction: outgoing strength: 0.7 edge_id: 0bcc341f-f60d-4e48-afd0-ddb45d9452b5 - target: 0fdd32ea-6b6a-4cd0-aa4b-117184e0c81d type: RELATED_TO direction: outgoing strength: 0.7 edge_id: d53ccdea-72f2-4342-8cc0-fb2b2c0eb056 - target: b9375a89-6e0f-4722-bca7-f1cd655de81a type: RELATED_TO direction: outgoing strength: 0.7 edge_id: e496d0d4-b7fb-4c61-8738-2a86da490803 --- ## Problem `sort_pitchers()` and `sort_starters()` in `app/routers_v2/teams.py` called `PitchingCardRatings.get_or_none()` twice per row inside a `DataFrame.apply()` — once for vs_hand="L" and once for "R". With 30 pitchers this was 60 queries. ## Solution Before the `apply`, batch-fetch all ratings for all card IDs in one query and build a `(pitchingcard_id, vs_hand) → rating` dict. The closure does O(1) dict lookups. ```python card_ids = pitcher_df["id"].tolist() ratings_map = { (r.pitchingcard_id, r.vs_hand): r for r in PitchingCardRatings.select().where( (PitchingCardRatings.pitchingcard_id << card_ids) & (PitchingCardRatings.vs_hand << ["L", "R"]) ) } def get_total_ops(df_data): vlval = ratings_map.get((df_data["id"], "L")) vrval = ratings_map.get((df_data["id"], "R")) ... ``` ## Files Changed - `app/routers_v2/teams.py` — both `sort_pitchers` and nested `sort_starters` ## Pattern General pattern for Peewee + pandas: never do model lookups inside `DataFrame.apply`. Batch fetch with `model << id_list`, build a dict, use dict lookup in the closure.