Saltearse al contenido

Avanzado: cubos virtuales, padre-hijo, miembros calculados

Esta página cubre cinco patrones avanzados de modelado que van más allá del cubo estándar en estrella: combinar varias tablas de hechos en un único cubo, jerarquías padre-hijo, miembros calculados definidos en el esquema, conjuntos con nombre reutilizables y dimensiones puente (muchos-a-muchos).

Cubos multifact (la respuesta de Mondrian 4 a los “cubos virtuales”)

Mondrian 3 incluía un elemento <VirtualCube> que unía dos o más cubos normales. Mondrian 4 no tiene elemento <VirtualCube>. La funcionalidad se absorbió en el modelo <Cube> regular: un único cubo puede contener varios elementos <MeasureGroup>, cada uno apuntando a una tabla de hechos diferente. Esto logra todo lo que hacía un cubo virtual, con menos ceremonia y mejor planificación de consultas.

Se utiliza un cubo con varios grupos de medidas cuando se tiene:

  • Tablas de hechos con granularidades diferentes — por ejemplo, una a nivel de día y otra a nivel de mes.
  • Tablas de hechos con dimensionalidades diferentes — por ejemplo, una que cubre Product, Time y Customer, otra que cubre Product, Time y Warehouse.
  • Una tabla agregada que se quiere registrar junto al fact base (un patrón habitual — los grupos de medidas agregados usan type="aggregate").

Ejemplo: Sales y Warehouse en un solo cubo

Warehouse and Sales:
dimensions:
- source: "Time"
- source: "Product"
- source: "Store"
- source: "Customer"
- source: "Warehouse"
measure_groups:
- name: "Sales"
table: "sales_fact_1997"
measures:
- name: "Unit Sales"
column: "unit_sales"
aggregator: "sum"
format_string: "Standard"
- name: "Store Sales"
column: "store_sales"
aggregator: "sum"
format_string: "#,###.00"
- name: "Store Cost"
column: "store_cost"
aggregator: "sum"
format_string: "#,###.00"
dimension_links:
- type: "foreign_key"
dimension: "Time"
foreign_key_column: "time_id"
- type: "foreign_key"
dimension: "Product"
foreign_key_column: "product_id"
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "foreign_key"
dimension: "Customer"
foreign_key_column: "customer_id"
- type: "no_link"
dimension: "Warehouse"
- name: "Warehouse"
table: "inventory_fact_1997"
measures:
- name: "Units Ordered"
column: "units_ordered"
aggregator: "sum"
- name: "Warehouse Sales"
column: "warehouse_sales"
aggregator: "sum"
- name: "Warehouse Cost"
column: "warehouse_cost"
aggregator: "sum"
dimension_links:
- type: "foreign_key"
dimension: "Time"
foreign_key_column: "time_id"
- type: "foreign_key"
dimension: "Product"
foreign_key_column: "product_id"
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "foreign_key"
dimension: "Warehouse"
foreign_key_column: "warehouse_id"
- type: "no_link"
dimension: "Customer"
calculated_members:
- name: "Profit Per Unit Shipped"
dimension: "Measures"
formula: "([Measures].[Store Sales] - [Measures].[Store Cost]) / [Measures].[Units\
\ Ordered]"

Cómo gestiona Mondrian las dimensiones no conformes

Las dimensiones compartidas por ambos grupos de medidas — Time y Product en el ejemplo anterior — son dimensiones conformes. Mondrian sincroniza automáticamente el contexto entre los grupos de medidas para estas. Si el contexto actual es [Time].[1997].[Q2] y [Product].[Beer], las medidas de ambos grupos se resuelven correctamente.

Las dimensiones que pertenecen a un solo grupo de medidas son no conformes. Cuando el contexto actual incluye [Customer].[Jane Smith], una medida del grupo Warehouse (que tiene <NoLink dimension="Customer"/>) devuelve NULL en lugar de un agregado incorrecto. Este es el comportamiento correcto y esperado.

Grupos de medidas agregados

Al registrar una tabla agregada junto a su tabla de hechos base, el grupo de medidas agregado utiliza type="aggregate" y <CopyLink> en lugar de <ForeignKeyLink> para las dimensiones consolidadas. Consulte la sección Esquema físico — CopyLink para obtener todos los detalles.

Jerarquías padre-hijo

Una jerarquía convencional tiene un número fijo de niveles y cada miembro de un nivel determinado tiene su padre en el nivel superior. Una jerarquía padre-hijo solo tiene un nivel real (más el miembro All opcional), pero los miembros pueden ser padres de otros miembros dentro del mismo nivel. Este es el modelo adecuado para organigramas, árboles de productos, cuentas del libro mayor, regiones geográficas con profundidad variable y estructuras recursivas similares.

Definir una jerarquía padre-hijo en Mondrian 4

En Mondrian 4 se expresa la estructura padre-hijo en un <Attribute> dentro de un <Dimension>. Añada el atributo parentAttribute apuntando al atributo que contiene la clave del padre de cada miembro. En la dimensión Employees de FoodMart, la columna autorreferenciada es supervisor_id:

<Dimension name="Employee" table="employee" key="Employee Id">
<Attributes>
<Attribute name="Employee Id" keyColumn="employee_id" nameColumn="full_name"/>
<Attribute name="Manager Id" keyColumn="supervisor_id"/>
<!-- additional descriptive attributes -->
<Attribute name="Position Title" keyColumn="position_title" hasHierarchy="false"/>
<Attribute name="Gender" keyColumn="gender" hasHierarchy="false"/>
<Attribute name="Education Level" keyColumn="education_level" hasHierarchy="false"/>
</Attributes>
<Hierarchies>
<Hierarchy name="Employees" allMemberName="All Employees">
<Level attribute="Employee Id" parentAttribute="Manager Id" nullParentValue="0"/>
</Hierarchy>
</Hierarchies>
</Dimension>

Atributos clave en <Level>:

  • parentAttribute — el nombre del atributo que proporciona la clave del padre de cada miembro. Este único atributo es la señal para Mondrian de que la jerarquía es padre-hijo.
  • nullParentValue — el valor que indica “sin padre” (es decir, un miembro raíz). Su valor por defecto es null, pero muchos esquemas utilizan 0 o -1 en su lugar porque algunas bases de datos no indexan los valores nulos.

Optimización de jerarquías padre-hijo

La implementación ingenua del rollup padre-hijo es costosa: Mondrian debe emitir una sentencia SQL por nodo para sumar todos los descendientes. Para jerarquías superficiales con unas pocas docenas de miembros, esto es aceptable. Para árboles más profundos o más amplios — con cientos o miles de miembros — notará una degradación significativa del rendimiento. También existe una segunda restricción: no se puede definir una medida distinct-count en ningún cubo que contenga una jerarquía padre-hijo no optimizada, porque Mondrian no puede expresar la deduplicación necesaria en SQL estándar.

La solución es una tabla de cierre.

Tablas de cierre

Una tabla de cierre es una tabla SQL plana que precalcula cada par antepasado-descendiente a cada profundidad. Para la tabla employee tiene este aspecto:

supervisor_idemployee_iddistance
110
121
132
141
153
162
220
231
252
261
330
351
440
550
660

Cada fila indica “el empleado X es un descendiente del supervisor Y a profundidad D”. Lo crucial es que cada empleado aparece como su propio descendiente a distancia 0 (el cierre reflexivo). Con esta tabla disponible, Mondrian puede calcular cualquier agregado de subárbol con una única unión SQL — sin iteración.

Se declara la tabla de cierre en el elemento <Level> mediante el hijo <Closure>:

<Hierarchy name="Employees" allMemberName="All Employees">
<Level attribute="Employee Id" parentAttribute="Manager Id" nullParentValue="0">
<Closure table="employee_closure"
parentColumn="supervisor_id"
childColumn="employee_id"/>
</Level>
</Hierarchy>

El esquema físico también necesita un <Link> para que Mondrian pueda unir employee con employee_closure:

<Table name="employee" keyColumn="employee_id"/>
<Table name="employee_closure"/>
<Link source="employee_closure" target="employee" foreignKeyColumn="employee_id"/>

Para obtener el mejor rendimiento, añada los siguientes índices:

CREATE UNIQUE INDEX employee_closure_pk
ON employee_closure (supervisor_id, employee_id);
CREATE INDEX employee_closure_emp
ON employee_closure (employee_id);

Declare tanto supervisor_id como employee_id como NOT NULL — algunos optimizadores de base de datos manejan significativamente mejor las columnas indexadas no nulas.

Poblar tablas de cierre

Mondrian no puebla la tabla de cierre — esa es tarea de la capa ETL. La tabla debe refrescarse cada vez que cambie la jerarquía. Si utiliza Pentaho Data Integration (Kettle), hay un paso integrado Closure Generator que se encarga automáticamente de esto como parte de su canalización de carga.

Si no usa Kettle, puede poblar la tabla con un procedimiento almacenado. Aquí tiene un ejemplo de MySQL que siembra los pares consigo mismos y luego itera hacia afuera un nivel de profundidad a la vez:

DELIMITER //
CREATE PROCEDURE populate_employee_closure()
BEGIN
DECLARE distance INT;
TRUNCATE TABLE employee_closure;
SET distance = 0;
-- seed with self-pairs (distance 0)
INSERT INTO employee_closure (supervisor_id, employee_id, distance)
SELECT employee_id, employee_id, distance
FROM employee;
-- for each (root, leaf) in the closure add (root, leaf->child)
REPEAT
SET distance = distance + 1;
INSERT INTO employee_closure (supervisor_id, employee_id, distance)
SELECT ec.supervisor_id, e.employee_id, distance
FROM employee_closure ec
JOIN employee e ON ec.employee_id = e.supervisor_id
WHERE ec.distance = distance - 1;
UNTIL (ROW_COUNT() = 0)
END REPEAT;
END //
DELIMITER ;

Ejecute este procedimiento después de cada carga que modifique la tabla employee.

Miembros calculados

Un miembro calculado es un miembro de cubo cuyo valor proviene de una fórmula MDX en lugar de una columna de la tabla de hechos. Puede definir miembros calculados en la dimensión Measures (creando medidas calculadas) o en cualquier otra dimensión del cubo.

En lugar de repetir la fórmula en cada consulta MDX con una cláusula WITH MEMBER, la define una vez en el esquema y queda automáticamente disponible en todas las consultas contra ese cubo:

calculated_members:
- name: "Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
value: "$#,##0.00"

También puede escribir la fórmula como atributo XML si prefiere la brevedad:

calculated_members:
- name: "Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
value: "$#,##0.00"

Ambas formas producen resultados idénticos — y como el conversor pliega el hijo <Formula> y el atributo formula en la misma clave YAML formula:, ambas formas XML producen el mismo YAML mostrado anteriormente.

CalculatedMemberProperty

<CalculatedMemberProperty> establece las propiedades de solve-order de MDX, cadenas de formato e información de tipo en el miembro calculado. Las propiedades más utilizadas son:

PropiedadPropósitoValor de ejemplo
FORMAT_STRINGControla cómo se muestra el valor en los clientes"$#,##0.00"
DATATYPEIndica a los clientes XMLA el tipo de retorno"Numeric", "Integer", "String"
SOLVE_ORDERPrioridad cuando varios miembros calculados están en el ámbito"2000"

FORMAT_STRING puede contener una expresión condicional en lugar de un literal:

calculated_members:
- name: "Conditional Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
expression: "Iif(Value < 0, '|($#,##0.00)|style=red', '|$#,##0.00|style=green')"

Cuando Mondrian renderiza una celda, primero evalúa la expresión para obtener una cadena de formato, y luego aplica esa cadena de formato al valor de la celda.

Visibilidad

Establezca visible="false" en un <CalculatedMember> (o en un <Measure>) para ocultarlo de los exploradores de miembros de las herramientas cliente. Esto es útil cuando se construye un resultado mediante pasos intermedios que no deberían exponerse directamente:

<Measure name="Store Cost" column="store_cost" aggregator="sum"
formatString="#,###.00" visible="false"/>
<CalculatedMember name="Margin" dimension="Measures" visible="false">
<Formula>([Measures].[Store Sales] - [Measures].[Store Cost]) / [Measures].[Store Cost]</Formula>
</CalculatedMember>
<CalculatedMember name="Store Sqft" dimension="Measures" visible="false">
<Formula>[Store].Properties("Sqft")</Formula>
</CalculatedMember>
<CalculatedMember name="Margin per Sqft" dimension="Measures" visible="true">
<Formula>[Measures].[Margin] / [Measures].[Store Cost]</Formula>
<CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
</CalculatedMember>

Solo “Margin per Sqft” aparece en el cliente; los demás son auxiliares.

Miembros calculados en cubos multifact

<CalculatedMembers> se sitúa en el nivel <Cube> y puede referenciar medidas de cualquiera de los grupos de medidas del cubo. Este es el hogar natural para los KPI entre tablas de hechos:

calculated_members:
- name: "Profit Growth"
dimension: "Measures"
visible: true
formula: "([Measures].[Profit] - [Measures].[Profit last Period]) / [Measures].[Profit\
\ last Period]"
properties:
- name: "FORMAT_STRING"
value: "0.0%"

Conjuntos con nombre

Un conjunto con nombre es una expresión de conjunto MDX reutilizable definida en el esquema. Es el análogo en el esquema de una cláusula WITH SET en una consulta MDX. Una vez definido, el conjunto está implícitamente disponible en cada consulta contra el cubo (o esquema) donde se declara.

Conjuntos con nombre a nivel de cubo

Declare un conjunto con nombre dentro de un <Cube> para que esté disponible en todas las consultas contra ese cubo:

named_sets:
- name: "Top Sellers"
formula: "TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse\
\ Sales])"

Después podrá usar [Top Sellers] directamente en MDX:

SELECT
{[Measures].[Warehouse Sales]} ON COLUMNS,
{[Top Sellers]} ON ROWS
FROM [Warehouse]
WHERE [Time].[Year].[1997]

Lo que podría devolver:

WarehouseWarehouse Sales
Treehouse Distribution31,116.37
Jorge Garcia, Inc.30,743.77
Artesia Warehousing, Inc.29,207.96
Jorgensen Service Storage22,869.79
Destination, Inc.22,187.42

Conjuntos con nombre a nivel de esquema

También puede declarar conjuntos con nombre a nivel de esquema, fuera de cualquier cubo. Los conjuntos con nombre a nivel de esquema están disponibles en todos los cubos del esquema, pero solo son válidos en cubos que contienen las dimensiones a las que hace referencia la fórmula:

<Schema name="FoodMart">
<Cube name="Sales" .../>
<Cube name="Warehouse" .../>
<NamedSets>
<NamedSet name="CA Cities"
formula="{[Store].[USA].[CA].Children}"/>
<NamedSet name="Top CA Cities">
<Formula>TopCount([CA Cities], 2, [Measures].[Unit Sales])</Formula>
</NamedSet>
</NamedSets>
</Schema>

[CA Cities] es válido en cualquier cubo que tenga una dimensión [Store]. Usarlo en un cubo sin esa dimensión genera un error en tiempo de consulta, no en tiempo de carga del esquema.

El atributo formula y el elemento hijo <Formula> son equivalentes. Use la forma de atributo para expresiones cortas, el elemento hijo para mayor legibilidad en las más largas.

Dimensiones puente (muchos-a-muchos)

Una dimensión normal tiene una relación uno-a-muchos con el fact: cada fila del fact pertenece exactamente a un cliente, un producto, un día. Una relación muchos-a-muchos (o puente) es diferente — una única fila del fact puede pertenecer a varios miembros de la dimensión a la vez.

El ejemplo clásico es una cuenta bancaria conjunta. Una cuenta tiene un único saldo, pero puede estar copropiedad de dos o más clientes:

ACCOUNTS (fact) OWNERSHIP (bridge)
acct year balance acct customer weight
1 2024 1000 1 Alice 0.50
2 2024 500 1 Bob 0.50
3 2025 300 2 Bob 1.00
3 Alice 0.25
total balance = 1800 3 Carol 0.75

La cuenta 1 es propiedad tanto de Alice como de Bob. No hay una única columna customer_id que se pueda poner en la tabla de hechos, por lo que un <ForeignKeyLink> simple no puede modelar esto. En su lugar, la relación vive en una tabla puente independiente (OWNERSHIP) que asigna cuentas a clientes, opcionalmente con un peso de propiedad.

El problema del fan-out

La forma ingenua de responder “saldo por cliente” es unir fact → bridge → customer y SUM(balance). Pero esa unión se expande (fan-out): la única fila de $1000 de la cuenta 1 se convierte en dos filas (una para Alice, otra para Bob). Sume ingenuamente entre todos los clientes y obtendrá 3100, no el verdadero 1800 — la cuenta 1 se contó dos veces, la cuenta 3 se contó dos veces. Este doble conteo es el riesgo principal del modelado muchos-a-muchos.

El Mondrian de Saiku gestiona correctamente el fan-out con un <BridgeLink>.

Declarar un enlace puente

Un <BridgeLink> reemplaza al <ForeignKeyLink> para la dimensión muchos-a-muchos dentro de los <DimensionLinks> del grupo de medidas:

measure_groups:
- name: "Balances"
table: "account_fact"
measures:
- name: "Balance"
column: "balance"
aggregator: "sum"
dimension_links:
- type: "foreign_key"
dimension: "Date"
foreign_key_column: "date_key"
- type: "bridge"
dimension: "Customer"
bridge_table: "account_owner"
fact_foreign_key_column: "account_id"
bridge_fact_key_column: "account_id"
bridge_dimension_key_column: "customer_id"
AtributoObligatorioDescripción
dimensionLa dimensión muchos-a-muchos que este enlace resuelve.
bridgeTableLa tabla física que contiene el mapeo fact↔dimensión. Debe estar declarada en <PhysicalSchema>.
factForeignKeyColumnColumna en la tabla de hechos a la que vuelve la unión del puente (la clave de granularidad del fact).
bridgeFactKeyColumnColumna en el puente que coincide con factForeignKeyColumn.
bridgeDimensionKeyColumnColumna en el puente que coincide con la clave de la dimensión.
aggregationnofullCount (por defecto) o weighted. Véase más abajo.
weightColumnnoColumna en el puente que contiene el peso de asignación. Obligatoria cuando aggregation="weighted".

La dimensión puente también requiere que se declare la granularidad del fact, para que Saiku pueda deduplicar el fan-out. Decláreselo como <Key> de la tabla de hechos en el esquema físico:

tables:
- name: "account_fact"
key:
- "account_id"

Asignación: full-count vs weighted

Hay dos formas honestas de atribuir un valor de fact compartido entre sus propietarios.

fullCount (el predeterminado) acredita el valor completo a todos los propietarios. Cada cliente ve el saldo completo de cada cuenta en la que está:

Balance by Customer (fullCount):
Alice 1300 (acct1 1000 + acct3 300)
Bob 1500 (acct1 1000 + acct2 500)
Carol 300 (acct3 300)

Los números por cliente se solapan deliberadamente — Alice y Bob ven ambos los $1000 completos de la cuenta 1 — que es lo que se quiere para “¿sobre cuánto saldo tiene autoridad de firma este cliente?”. Cuando los totales de full-count se acumulan entre propietarios — el total general (All Customers), o cualquier nivel intermedio (véase Dimensiones puente multinivel más abajo) — Saiku aplica un agregado simétrico: deduplica de vuelta a la granularidad del fact antes de sumar, de modo que el total general es el verdadero 1800, no el 3100 expandido.

weighted divide cada valor según la columna de peso del puente, por lo que las partes suman el todo:

dimension_links:
- type: "bridge"
dimension: "Customer"
bridge_table: "account_owner"
fact_foreign_key_column: "account_id"
bridge_fact_key_column: "account_id"
bridge_dimension_key_column: "customer_id"
aggregation: "weighted"
weight_column: "weight"
Balance by Customer (weighted):
Alice 575 (1000×0.50 + 300×0.25)
Bob 1000 (1000×0.50 + 500×1.00)
Carol 225 (300×0.75)
total 1800 (reconciles exactly)

Use weighted para “¿cuál es la cuota económica de este cliente?” y cuando los pesos sumen 1 por fila de fact, cada nivel — incluido el total general — se reconcilia automáticamente con el total del fact.

Dimensiones puente multinivel

Una dimensión puente no se limita a un único nivel — puede tener jerarquía, y los rollups de full-count se mantienen correctos en cada nivel. Suponga que cada cliente pertenece a un segmento (Alice y Bob son Premium, Carol es Standard) y se acumula el puente hasta el segmento:

shared_dimensions:
Customer:
table: "dim_customer"
key: "Customer"
attributes:
- name: "Segment"
key_column: "segment"
- name: "Customer"
key_column: "customer_id"
name_column: "customer_name"
hierarchies:
- name: "By Segment"
all_member_name: "All Customers"
levels:
- "Segment"
- "Customer"
Balance by Segment (fullCount):
Premium 1800 (owns acct1, acct2, acct3 — de-duplicated)
Standard 300 (owns acct3)

La cuenta 1 es propiedad de Alice y Bob, ambos Premium — pero se cuenta una vez en el total Premium, no dos. Este es el agregado simétrico haciendo su trabajo en un nivel intermedio: sin él, Premium leería el valor expandido 2800. La cuenta 3 aparece en ambos segmentos (Alice es Premium, Carol es Standard) — ese es el solapamiento full-count entre segmentos pretendido, distinto del doble conteo dentro de un segmento que la dedup elimina.

Los rollups con pesos no necesitan deduplicación — la cuota ponderada de cada propietario se suma limpiamente, por lo que Premium = 1575, Standard = 225, sigue reconciliándose a 1800.

Qué funciona

Una dimensión puente se comporta como cualquier otra dimensión una vez declarada. Todo lo siguiente funciona de forma nativa:

  • la dimensión puente en filas, columnas o en el slicer (WHERE);
  • cruzada con dimensiones normales de clave foránea (p. ej. Customer × Region), en ejes diferentes o como crossjoin en el mismo eje;
  • jerarquías puente multinivel — los totales de full-count se deduplican correctamente en cada nivel, no solo en la hoja y el total general;
  • varias medidas en una consulta, incluidas las medidas de columna calculada (un CASE o aritmética como revenue - cost), que se deduplican igual que las medidas normales;
  • NON EMPTY (los clientes sin cuentas se suprimen);
  • conjuntos explícitos de miembros y .Members.

Requisitos y limitaciones

  • La tabla de hechos debe declarar su granularidad como <Key> para que fullCount pueda deduplicar. (Esto es lo que hace que los rollups multinivel sean correctos.)
  • weighted requiere una weightColumn; fullCount ignora cualquier peso.
  • Ambas asignaciones cubren tanto las medidas planas de columna real como las medidas de columna calculada (un CASE o aritmética como revenue - cost): fullCount deduplica la expresión calc en la granularidad del fact, y weighted la escala por el peso (SUM(expression × weight)).
  • La unión del puente es de una sola columna en cada salto. Las claves de unión compuestas son una limitación general del camino de unión de Calcite (no específica de los puentes) — si las claves de su puente son multicolumna, modele en su lugar una única clave de granularidad sustituta.

Un ejemplo completo y cargable — esquema, datos semilla y MDX de ejemplo con números esperados — se incluye en la biblioteca de cubos bajo many-to-many.

Granularidad distinta a nivel de medida (distinctKeyColumn)

Un puente resuelve el fan-out causado por un join. Pero el fan-out también puede provenir de la granularidad de la propia tabla de hechos: una fila de fact que debería contarse una vez está almacenada físicamente como varias filas (un pedido con varias líneas, un evento registrado por toque), y un SUM simple sobre la columna duplica el conteo. Cuando la duplicación está claveada por una columna en la propia tabla de hechos de la medida, no necesita un puente — puede fijar la granularidad de dedup directamente en la medida con distinctKeyColumn.

ORDER_LINES (fact)
order_id line region amount
1 a North 100
1 b North 100 ← amount repeats per line of order 1
2 a South 50
3 a North 300
3 b North 300 ← amount repeats per line of order 3
SUM(amount) = 100+100+50+300+300 = 850 (WRONG — double-counts)
SUM(amount) DISTINCT over order_id = 100 + 50 + 300 = 450 (correct)

Declare la clave de dedup en la medida. Debe resolverse a una columna de la propia tabla de hechos de la medida, y solo está permitida para aggregator="sum" y aggregator="avg":

measures:
- name: "Order Amount"
column: "amount"
aggregator: "sum"
distinct_key_column: "order_id"

Semántica

La medida agrega sobre SELECT DISTINCT (group keys, distinctKeyColumn, operand) — cada clave distinta aporta su valor una vez, incluso cuando la granularidad del fact repite la fila. Reutiliza la misma maquinaria de agregado simétrico que el fan-out del puente (la dedup la dirige la declaración de la medida en lugar de la topología de unión):

  • aggregator="sum"SUM sobre las claves distintas (la forma sum_distinct de LookML).
  • aggregator="avg"AVG sobre las claves distintas (average_distinct de LookML).

Los rollups permanecen correctos en cada nivel: By Region sobre el ejemplo lee North = 400, South = 50 — los valores distintos, nunca el 850 expandido.

Se compone con seguridad de filas

La granularidad distinta se aplica después del filtrado de seguridad por filas, no antes. <PredicateGrant> y los grants de miembros puente filtran las filas del fact dentro de la subconsulta DISTINCT, por lo que la dedup siempre opera exactamente sobre las filas que se permite ver al llamante — no hay ningún camino que deduplique un valor que el rol no puede ver y luego filtre el total. La granularidad distinta es una propiedad fija del esquema (no dependiente del rol), por lo que no añade ninguna nueva dimensión de caché, y la caché de segmentos sigue aislando roles: un valor calentado para los grants de un rol nunca se sirve a otro. (Esto está cubierto por los tests de composición de seguridad de filas, incluyendo un test de no-bleed-de-caché entre roles.)

Requisitos y limitaciones

  • distinctKeyColumn debe resolverse a una columna real de la propia tabla de hechos de la medida. Una clave entre tablas o no resoluble se rechaza en tiempo de carga (fail-closed) — nunca una dedup silenciosamente errónea.
  • Permitido solo para aggregator="sum" y aggregator="avg".
  • Una carga de segmento que mezcle una medida con granularidad distinta con una medida sencilla, o dos medidas con claves distintas diferentes, se divide automáticamente en segmentos homogéneos por medida (el DISTINCT a nivel de petición no puede servir dos granularidades a la vez).
  • Cuando la clave distinta es igual a la propia clave de granularidad de la tabla de hechos (p. ej. su clave primaria), la dedup es un no-op y la medida se comporta como un sum/avg simple. Este es el objetivo natural para sum_distinct/average_distinct de LookML — consulte Migrar desde Looker.

Adaptado de la guía de esquemas del proyecto Mondrian (EPL v1.0). Las dimensiones puente (muchos-a-muchos) y la granularidad distinta a nivel de medida son extensiones de Saiku a Mondrian 4.