Ir al contenido

Performance en Odoo: índices, read_group y batch processing

Evitar N+1, optimizar queries y escoger bien entre search_read y read
20 de agosto de 2026 por
Performance en Odoo: índices, read_group y batch processing
ATEMI, Aitor Atencia

Odoo · Performance · ORM

Un módulo que va bien con 200 registros de demo y se arrastra con 200.000 en producción casi siempre tiene el mismo origen: un bucle Python que dispara una query por iteración. La solución no es "optimizar después", es no escribir el N+1 la primera vez.

Logo Odoo
El log de queries SQL (--log-sql) es la herramienta más honesta para ver si tu código escala.

El problema clásico

N+1: una query por registro en vez de una para todos

Acceder a un campo relacional dentro de un bucle for record in records: parece inofensivo, pero cada acceso a record.partner_id.name en una recordset sin prefetch puede disparar una query SQL individual. Con 10 registros no se nota; con 10.000, el request tarda minutos.

# Mal: N queries, una por factura
for invoice in invoices:
    total += invoice.partner_id.credit_limit

# Bien: el ORM precarga partner_id de todo el recordset de una vez
partners = invoices.mapped("partner_id")
credit_by_partner = {p.id: p.credit_limit for p in partners}
total = sum(credit_by_partner[inv.partner_id.id] for inv in invoices)

Agregaciones

read_group / _read_group: sumar sin traer registros a Python

Cuando necesitas totales, medias o conteos agrupados, traer todos los registros a Python con browse() y sumarlos en un bucle es tirar el trabajo que PostgreSQL hace bien de forma nativa. _read_group genera un GROUP BY SQL y devuelve solo los agregados, sin materializar cada registro.

# Mal: trae todas las líneas de venta a Python para sumar
lines = self.env["sale.order.line"].search([("order_id.state", "=", "sale")])
total_by_product = {}
for line in lines:
    total_by_product.setdefault(line.product_id, 0)
    total_by_product[line.product_id] += line.price_subtotal

# Bien: agregación en SQL, un solo GROUP BY
result = self.env["sale.order.line"]._read_group(
    domain=[("order_id.state", "=", "sale")],
    groupby=["product_id"],
    aggregates=["price_subtotal:sum"],
)

Escritura masiva

Batch processing: agrupa antes de escribir

Igual que leer registro a registro es lento, escribir registro a registro también lo es: cada record.write(vals) dentro de un bucle dispara sus propios triggers de @api.depends, @api.constrains y reglas de seguridad. Agrupar los IDs y llamar a write() una sola vez sobre el recordset completo reduce drásticamente el número de recomputaciones.

# Mal: un write() por registro, N recomputaciones de campos calculados
for order in orders:
    order.write({"state": "done"})

# Bien: un único write() sobre todo el recordset
orders.write({"state": "done"})
SituaciónQué hacer
Sumar/contar/agrupar registros_read_group, nunca bucle Python
Actualizar el mismo campo en varios registroswrite() sobre el recordset, no uno a uno
Sospechas de N+1Activar --log-sql y contar queries por request
Necesitas solo IDs o un camposearch_read con fields concreto, no browse completo

Resumen

El rendimiento en Odoo se decide en el patrón de acceso a datos, no en microoptimizaciones. Evita bucles que consultan o escriben registro a registro, delega las agregaciones a _read_group y mide con --log-sql antes de asumir dónde está el cuello de botella.

en Odoo
Testing en Odoo: unit tests, mocks y env.ref()
Cómo escribir tests fiables, preparar datos de prueba y evitar falsos positivos