FASE 7b: DB Performance — Pooling, Stock Summary, N+1 fix

Cambios implementados:

1. Connection pooling (tenant_db.py):
   - psycopg2.pool.ThreadedConnectionPool para master y tenants
   - Wrapper _PooledConnection que devuelve al pool en .close()
   - Cero cambios en blueprints (backward compatible)

2. Tabla inventory_stock_summary + triggers (v3.2):
   - O(1) stock lookup en vez de SUM() sobre historial completo
   - Trigger AFTER INSERT en inventory_operations recalcula stock
   - Poblada inicialmente en ambos tenants
   - Refactor en 6 archivos de servicios para usar la nueva tabla

3. Fix N+1 en process_sale (pos_engine.py):
   - Precarga retail_price en bulk query FOR UPDATE
   - Elimina SELECT individual por item en loop

4. Índices críticos:
   - idx_parts_name_part + pattern_ops (master)
   - idx_inv_ops_inventory_branch_created (tenants)
   - idx_wi_part_stock_positive (master, ya existía desde Fase 1)

Tests: 73/73 pasando (compat + fase3 + fase5 + fase6)
Migración: v3.2_db_performance.sql
This commit is contained in:
2026-04-27 07:34:31 +00:00
parent 175dda6212
commit e3c85fd647
9 changed files with 173 additions and 40 deletions

View File

@@ -25,15 +25,33 @@ def _safe_g(attr, default=None):
def get_stock(conn, inventory_id, branch_id=None):
"""Get current stock for an inventory item. Optionally filter by branch.
Uses Redis cache first, falls back to PostgreSQL SUM query.
Uses Redis cache first, then inventory_stock_summary, falls back to
PostgreSQL SUM query.
"""
# Try Redis first
cached = get_cached_stock(inventory_id, branch_id)
if cached is not None:
return cached
# Fallback to PostgreSQL
# Use inventory_stock_summary (O(1) lookup)
cur = conn.cursor()
if branch_id:
cur.execute(
"SELECT stock FROM inventory_stock_summary WHERE inventory_id = %s AND branch_id = %s",
(inventory_id, branch_id)
)
else:
cur.execute(
"SELECT stock FROM inventory_stock_summary WHERE inventory_id = %s",
(inventory_id,)
)
row = cur.fetchone()
if row is not None:
cur.close()
set_cached_stock(inventory_id, row[0], branch_id)
return row[0]
# Fallback to PostgreSQL SUM (legacy, should not reach here if trigger works)
if branch_id:
cur.execute(
"SELECT COALESCE(SUM(quantity), 0) FROM inventory_operations WHERE inventory_id = %s AND branch_id = %s",
@@ -55,21 +73,17 @@ def get_stock(conn, inventory_id, branch_id=None):
def get_stock_bulk(conn, branch_id=None):
"""Get stock for all items. Returns dict {inventory_id: stock_quantity}.
Uses PostgreSQL directly (bulk operation, Redis wouldn't help much here
unless we pre-populated all keys).
Uses inventory_stock_summary for O(1) bulk lookup.
"""
cur = conn.cursor()
if branch_id:
cur.execute("""
SELECT inventory_id, COALESCE(SUM(quantity), 0)
FROM inventory_operations WHERE branch_id = %s
GROUP BY inventory_id
SELECT inventory_id, stock
FROM inventory_stock_summary WHERE branch_id = %s
""", (branch_id,))
else:
cur.execute("""
SELECT inventory_id, COALESCE(SUM(quantity), 0)
FROM inventory_operations
GROUP BY inventory_id
SELECT inventory_id, stock FROM inventory_stock_summary
""")
stock_map = {r[0]: r[1] for r in cur.fetchall()}
cur.close()