Una ontología para la capa semántica

Las capas semánticas se venden como el sitio donde poner las definiciones de métricas. Escribe revenue una vez, exponlo en todas partes, deja de discutir en reuniones.

Y entonces dos equipos reportan cifras de ingresos distintas, ambas técnicamente correctas, y la discusión se muda del panel al archivo de definiciones.

Las definiciones nunca fueron la parte difícil.

Las métricas son la capa visible, no la que sostiene el peso

Una métrica es una agregación sobre un conjunto de cosas. Cambia qué cuenta como una de esas cosas y el número cambia, sin que la fórmula se toque.

sum(amount) no es ambiguo. Qué importes es donde vive el desacuerdo:

  • ¿Un pedido cancelado sigue siendo un pedido?
  • ¿Un cliente que firmó dos contratos es un cliente o dos?
  • ¿Un pedido pertenece a la fecha en que se hizo, se pagó o se envió?
  • ¿Una devolución es un pedido negativo, o es otra cosa distinta?

Ninguna de estas es una pregunta sobre métricas. Son preguntas sobre qué es un pedido y qué es un cliente, y sobre si dos registros se refieren a la misma cosa real. Una capa semántica que solo responde la pregunta de la métrica hereda todos estos desacuerdos y no les da ningún sitio donde vivir.

Esa parte que falta es la ontología: no las fórmulas, sino las entidades sobre las que las fórmulas cuantifican.

A qué se compromete una ontología

A tres cosas, y cada una es una decisión que alguien tiene que tomar de forma explícita.

Identidad. Qué hace que dos registros sean la misma cosa. Es la que más se omite, y la que provoca dobles conteos durante años.

Granularidad. Qué significa una fila. “Una fila por pedido” y “una fila por línea de pedido” son mundos distintos, y las métricas se rompen en silencio cuando un modelo pasa de uno a otro.

Relaciones. Cómo se conectan las entidades y, sobre todo, con qué cardinalidad. Un join que abre en abanico sin que nadie lo declarara es la causa más común de un total inflado.

erDiagram
    CLIENTE ||--o{ PEDIDO : realiza
    PEDIDO  ||--|{ LINEA_PEDIDO : contiene
    PEDIDO  ||--o{ DEVOLUCION : "puede devolverse en"
    PRODUCTO ||--o{ LINEA_PEDIDO : "aparece en"
    CLIENTE {
        string cliente_id PK
        string cuenta_id "identidad: una por entidad legal"
    }
    PEDIDO {
        string pedido_id PK
        string cliente_id FK
        date   fecha_pedido
        date   fecha_reconocimiento "grano: fecha de ingreso"
        string estado
    }
    LINEA_PEDIDO {
        string linea_id PK
        string pedido_id FK
        numeric importe
    }
theory of small decisions

Lee el diagrama como un conjunto de compromisos, no como un dibujo. Dice que un pedido tiene muchas líneas, así que cualquier métrica que sume importe al grano de pedido está mal. Dice que un cliente se mapea a una cuenta, así que “número de clientes” necesita declarar a cuál de las dos se refiere. Dice que el ingreso tiene su propia fecha, distinta de cuándo se hizo el pedido.

Cada una de esas es una frase que alguien pudo discutir en una reunión. Escritas, dejan de relitigarse cada trimestre.

Hacerla ejecutable

Una ontología que vive en un diagrama se pudre. La versión que sobrevive es la que lee el motor de consultas, porque entonces no puede desviarse de lo que los números hacen de verdad.

entities:
  - name: pedido
    primary_key: pedido_id
    grain: "una fila por pedido"
    # La identidad es una decisión, no un tecnicismo: dos filas con el mismo
    # id de origen desde sistemas distintos son el mismo pedido solo después
    # de que esta regla lo diga.
    identity:
      resolve_by: [sistema_origen, id_origen]
    relationships:
      - to: cliente
        type: many_to_one
        via: cliente_id
      - to: linea_pedido
        type: one_to_many
        via: pedido_id

  - name: linea_pedido
    primary_key: linea_id
    grain: "una fila por línea dentro de un pedido"

metrics:
  - name: ingresos
    description: >-
      Neto de devoluciones, reconocido en fecha_reconocimiento y no en
      fecha_pedido, excluyendo pedidos cancelados.
    entity: linea_pedido
    agg: sum
    expr: importe
    filters:
      - "pedido.estado != 'cancelado'"
    time_dimension: pedido.fecha_reconocimiento

La línea entity: linea_pedido hace un trabajo silencioso e importante. Declara el grano al que corre la suma, que es lo que impide al motor abrir en abanico un join a nivel de pedido y contar el mismo dinero dos veces.

Cómo se resuelve una pregunta

El valor de escribir esto es que una pregunta deja de responderla quien componga el SQL, y pasa a responderla el modelo.

flowchart LR
    Q["ingresos por cliente,<br/>último trimestre"] --> M[métrica: ingresos]
    M --> G[grano: linea_pedido]
    M --> F["filtro: estado != cancelado"]
    M --> T[tiempo: fecha_reconocimiento]
    G --> J{ruta de join}
    J --> O[pedido]
    O --> C[cliente]
    C --> ID["identidad: cuenta_id"]
    ID --> R[una fila por cuenta]
theory of small decisions

Dos analistas que hagan la misma pregunta recorren ahora el mismo camino. No porque se hayan puesto de acuerdo, sino porque el desacuerdo se zanjó una vez, aguas arriba, y se codificó.

Dónde debe vivir la ontología

La parte difícil no es modelar. Es la propiedad: una ontología mantenida por un equipo central se desvía de los dominios que describe, porque la gente que sabe qué es un contrato no trabaja ahí.

Este es el argumento para empujar las definiciones hacia los dominios dueños del dato, y dejar la capa semántica como el contrato entre ellos en lugar del sitio donde se inventa el significado.

No hace falta adoptar el modelo organizativo entero para quedarse con la parte útil: la definición pertenece a donde está el conocimiento, y el trabajo de la capa es hacerla legible en todas partes.

Modos de fallo que conviene nombrar

SíntomaSuele significar
Dos equipos, dos cifras de ingresos, ambas defendiblesSin identidad acordada para la entidad
Los totales se inflan tras un join nuevoCardinalidad no declarada, abanico silencioso
Una métrica se rompe al refactorizar un modeloEl grano nunca se declaró, solo se supuso
“¿Qué fecha?” preguntado cada trimestreSemántica temporal dejada a quien escribe la consulta
Definiciones correctas pero sin usarOntología gobernada lejos del dominio

El patrón en los cinco: la fórmula estaba bien, y lo que había debajo nunca se escribió.

Por dónde empezar

No por un catálogo de métricas. Empieza por las cinco o seis entidades sobre las que el negocio más discute, y por cada una escribe las tres frases: qué hace que dos sean la misma, qué significa una fila, y cómo se conecta con las demás.

Es poco lucido, y lleva una tarde por entidad. Pero esas frases son de lo que depende en silencio cada métrica, y dejarlas sin escribir es como una capa semántica acaba siendo un sitio más donde se almacena el mismo desacuerdo.

Capa semántica - dbt - Modelado de datos - YAML