@@ -771,6 +771,41 @@ Shipment.objects.select_related('shipper', 'recipient', 'carrier_connection')
771771Shipment.objects.prefetch_related(' parcels' , ' customs' , ' rates' )
772772```
773773
774+ ### ⚠️ N+1 Query Prevention (Critical!)
775+
776+ N+1 queries are a common Django ORM pitfall that can severely degrade performance,
777+ especially on serverless databases (Aurora Serverless) where each query incurs connection overhead.
778+
779+ ** Always review loops that touch the database:**
780+
781+ ``` python
782+ # ❌ BAD - N+1: one UPDATE per tracker in a loop
783+ for tracker in trackers:
784+ tracker.status = compute_status(tracker)
785+ tracker.save() # N individual UPDATE queries
786+
787+ # ❌ BAD - N+1: one SELECT per related object in a loop
788+ for shipment in shipments:
789+ print (shipment.carrier.name) # N individual SELECT queries (lazy loading)
790+
791+ # ✅ GOOD - Bulk update: single UPDATE for all trackers
792+ for tracker in trackers:
793+ tracker.status = compute_status(tracker)
794+ Tracking.objects.bulk_update(trackers, [" status" , " updated_at" ])
795+
796+ # ✅ GOOD - Prefetch/select related: single JOIN query
797+ shipments = Shipment.objects.select_related(" carrier" ).filter(... )
798+ for shipment in shipments:
799+ print (shipment.carrier.name) # No extra queries
800+ ```
801+
802+ ** Key patterns to watch for:**
803+ - ` model.save() ` inside a loop → use ` bulk_update() ` or ` bulk_create() `
804+ - ` model.related_field.attribute ` without ` select_related ` → add ` select_related() `
805+ - ` model.related_set.all() ` without ` prefetch_related ` → add ` prefetch_related() `
806+ - ` update_or_create() ` in high-concurrency paths → use split ` create() ` /` filter().update() ` to avoid ` SELECT FOR UPDATE ` lock contention
807+ - Individual ` filter().update() ` calls in a loop → collect changes and use ` bulk_update() `
808+
774809---
775810
776811## Background Jobs (Huey)
0 commit comments