Un reporte que tarda cuarenta segundos en abrir casi nunca es un problema de motor. Es un problema de cuándo se hace el trabajo.

Si el reporte lee directo de las tablas de origen, cada vez que alguien lo abre el motor rehace lo mismo: unir cinco tablas, descartar duplicados, resolver que un sistema manda el importe en centavos y otro en pesos, aplicar las reglas de negocio y recién ahí agrupar. Ese trabajo no cambia entre una ejecución y la siguiente, pero se paga entero cada vez. Y se paga en el peor momento posible: con alguien mirando la pantalla.

La arquitectura medallion —capas bronze, silver y gold, o como quieras llamarlas— resuelve eso corriendo el trabajo una vez, en batch, y dejando el resultado escrito. El reporte pasa de calcular a leer.

El costo que nadie menciona primero

Conviene decirlo antes que las ventajas: medallion agrega latencia. Un dato que entra al sistema a las 9:05 ya no aparece en el reporte a las 9:05. Aparece cuando corra la capa que lo procesa: puede ser en quince minutos, en una hora o a la madrugada siguiente.

Ese es el trade-off completo. No hay forma de esquivarlo: estás cambiando frescura por velocidad de consulta y por consistencia.

La pregunta correcta no es cuál de los dos es mejor, sino cuál te duele menos. Para la mayoría de los reportes de gestión, que alguien lea datos de hace una hora no cambia ninguna decisión. Que el reporte tarde cuarenta segundos y cada tanto muestre un número distinto al de ayer, sí.

Comparación de dos flujos: sin capas, cinco fuentes llegan directo al reporte que resuelve joins, deduplicación y reconciliación en cada ejecución; con capas, las mismas fuentes pasan por limpieza y modelo, donde el trabajo se hace una vez, y el reporte solo lee.

Qué hace lento a un reporte que lee del origen

Mirémoslo con una consulta. Esto es lo que suele haber detrás de un tablero que “anda lento”:

-- El reporte, leyendo directo del origen.
-- Todo esto se recalcula en cada apertura.
select
    c.segmento,
    date_trunc('month', p.fecha) as mes,
    sum(
        case
            when p.moneda = 'ARS' then p.importe
            else p.importe * fx.tasa
        end
    ) as total
from pedidos p
-- el origen trae reprocesos: hay que quedarse con la última versión
join (
    select id_pedido, max(actualizado_en) as ultima
    from pedidos
    group by id_pedido
) ult
  on ult.id_pedido = p.id_pedido
 and ult.ultima = p.actualizado_en
join clientes_crm c   on c.id_cliente = p.id_cliente
left join clientes_erp e on e.documento = c.documento  -- reconciliar dos sistemas
join tipo_cambio fx   on fx.fecha = p.fecha
                     and fx.moneda = p.moneda
where p.estado not in ('borrador', 'anulado')
group by 1, 2;

Nada de esto está mal escrito. El problema es dónde está: cada apertura del reporte vuelve a deduplicar por max(actualizado_en), vuelve a cruzar CRM con ERP por documento, vuelve a buscar el tipo de cambio.

Hay dos costos, y el segundo es peor que el primero:

El costo de cómputo. La subconsulta de deduplicación recorre pedidos entera antes de poder unirla. El motor tal vez la resuelva rápido con pocos datos, pero crece con la tabla, no con lo que el reporte muestra.

El costo de consistencia. Ese left join entre CRM y ERP por documento es una regla de negocio: qué pasa si el documento está mal cargado, si hay dos clientes con el mismo, si uno existe en un sistema y no en el otro. Esa regla está escrita adentro del reporte. El siguiente reporte que necesite lo mismo la va a escribir de nuevo, y no necesariamente igual. Ahí es donde aparecen dos tableros que muestran números distintos y nadie sabe cuál está bien.

Lo mismo, con el trabajo movido de lugar

La capa de limpieza resuelve lo que es propio de cada fuente: tipos, duplicados, nombres. Una fila por pedido, sin reprocesos:

-- capa de limpieza: una fila por pedido, tipos resueltos
create table limpio.pedidos as
select distinct on (id_pedido)
    id_pedido,
    id_cliente,
    fecha::date            as fecha,
    importe::numeric(18,2) as importe,
    upper(moneda)          as moneda,
    estado
from origen.pedidos
where estado not in ('borrador', 'anulado')
order by id_pedido, actualizado_en desc;

La capa de modelo resuelve lo que cruza fuentes: la reconciliación entre sistemas y las reglas de negocio, escritas una sola vez:

-- capa de modelo: la reconciliación CRM/ERP vive acá y solo acá
create table modelo.pedidos_enriquecidos as
select
    p.id_pedido,
    p.fecha,
    coalesce(c.segmento, 'sin_segmento') as segmento,
    -- la conversión ya resuelta: el reporte no vuelve a decidir
    case
        when p.moneda = 'ARS' then p.importe
        else p.importe * fx.tasa
    end as importe_ars
from limpio.pedidos p
left join limpio.clientes c on c.id_cliente = p.id_cliente
left join limpio.tipo_cambio fx
       on fx.fecha = p.fecha
      and fx.moneda = p.moneda;

Y el reporte queda así:

select segmento,
       date_trunc('month', fecha) as mes,
       sum(importe_ars)           as total
from modelo.pedidos_enriquecidos
group by 1, 2;

Un group by sobre una tabla. Sin subconsultas, sin joins, sin decisiones.

Lo que realmente se ganó

El cómputo total no bajó: el trabajo sigue existiendo, solo que ahora corre una vez por lote en vez de una vez por apertura. Si el reporte se abre treinta veces por día, la cuenta cierra sola. Si se abre una vez por mes, no.

Pero el ahorro de tiempo es lo menos interesante. Lo que cambia de verdad es que desaparece la reconciliación en tiempo de ejecución.

Cuando cada reporte cruza CRM con ERP por su cuenta, cada uno lo hace un poco distinto: uno usa left join, otro inner, uno normaliza el documento, otro no. Ninguno está mal en aislamiento, y los cuatro dan números diferentes. Ahí empiezan las reuniones para decidir cuál es el número bueno.

Con la regla en una sola tabla, la discusión se mueve de “cuál reporte tiene razón” a “está bien la regla”. Esa segunda discusión se puede resolver: se corrige en un lugar y todos los reportes cambian juntos. La primera no se resuelve nunca del todo.

Cuándo no vale la pena

Esto no es gratis y no siempre conviene:

  • Si el dato tiene que ser del momento. Un tablero de operaciones que mira qué está pasando ahora no puede leer una tabla de hace una hora. Ahí querés ir al origen, o a un stream.
  • Si el reporte se abre muy poco. Mantener capas cuesta: correrlas, monitorearlas, arreglarlas cuando fallan. Para algo que se consulta una vez por trimestre, ese costo no se recupera.
  • Si hay una sola fuente y una sola consulta. Sin cruces entre sistemas no hay reconciliación que centralizar, y buena parte del beneficio desaparece.
  • Si nadie va a mantener las capas. Una capa intermedia desactualizada es peor que no tenerla: da respuestas rápidas y equivocadas.

Lo que quedaría por mirar

Dos cosas que este esquema deja abiertas y conviene decidir a conciencia.

La primera es cada cuánto corre cada capa. No tienen por qué ir a la misma frecuencia: la limpieza puede correr seguido y el modelo unas pocas veces por día. Cuanto más seguido, menos latencia y más costo.

La segunda es qué pasa cuando llega un dato atrasado. Si un pedido de ayer entra hoy, hay que decidir si se reprocesa el período afectado o si se acepta que el número de ayer ya quedó cerrado. Es una decisión de negocio antes que técnica, y es mejor tomarla explícitamente que descubrirla el día que los números no cierran.

Ninguna de las dos tiene una respuesta universal, pero las dos se responden una vez y quedan escritas. Que es, en el fondo, de lo que se trata todo esto.

Ver todos los artículos