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]"<Cube name="Warehouse and Sales">
<Dimensions> <Dimension source="Time"/> <Dimension source="Product"/> <Dimension source="Store"/> <Dimension source="Customer"/> <Dimension source="Warehouse"/> </Dimensions>
<MeasureGroups>
<!-- Fact table 1: sales transactions --> <MeasureGroup name="Sales" table="sales_fact_1997"> <Measures> <Measure name="Unit Sales" column="unit_sales" aggregator="sum" formatString="Standard"/> <Measure name="Store Sales" column="store_sales" aggregator="sum" formatString="#,###.00"/> <Measure name="Store Cost" column="store_cost" aggregator="sum" formatString="#,###.00"/> </Measures> <DimensionLinks> <ForeignKeyLink dimension="Time" foreignKeyColumn="time_id"/> <ForeignKeyLink dimension="Product" foreignKeyColumn="product_id"/> <ForeignKeyLink dimension="Store" foreignKeyColumn="store_id"/> <ForeignKeyLink dimension="Customer" foreignKeyColumn="customer_id"/> <NoLink dimension="Warehouse"/> </DimensionLinks> </MeasureGroup>
<!-- Fact table 2: warehouse stock movements --> <MeasureGroup name="Warehouse" table="inventory_fact_1997"> <Measures> <Measure name="Units Ordered" column="units_ordered" aggregator="sum"/> <Measure name="Warehouse Sales" column="warehouse_sales" aggregator="sum"/> <Measure name="Warehouse Cost" column="warehouse_cost" aggregator="sum"/> </Measures> <DimensionLinks> <ForeignKeyLink dimension="Time" foreignKeyColumn="time_id"/> <ForeignKeyLink dimension="Product" foreignKeyColumn="product_id"/> <ForeignKeyLink dimension="Store" foreignKeyColumn="store_id"/> <ForeignKeyLink dimension="Warehouse" foreignKeyColumn="warehouse_id"/> <NoLink dimension="Customer"/> </DimensionLinks> </MeasureGroup>
</MeasureGroups>
<CalculatedMembers> <CalculatedMember name="Profit Per Unit Shipped" dimension="Measures"> <Formula>([Measures].[Store Sales] - [Measures].[Store Cost]) / [Measures].[Units Ordered]</Formula> </CalculatedMember> </CalculatedMembers>
</Cube>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 esnull, pero muchos esquemas utilizan0o-1en 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_id | employee_id | distance |
|---|---|---|
| 1 | 1 | 0 |
| 1 | 2 | 1 |
| 1 | 3 | 2 |
| 1 | 4 | 1 |
| 1 | 5 | 3 |
| 1 | 6 | 2 |
| 2 | 2 | 0 |
| 2 | 3 | 1 |
| 2 | 5 | 2 |
| 2 | 6 | 1 |
| 3 | 3 | 0 |
| 3 | 5 | 1 |
| 4 | 4 | 0 |
| 5 | 5 | 0 |
| 6 | 6 | 0 |
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"<CalculatedMembers> <CalculatedMember name="Profit" dimension="Measures"> <Formula>[Measures].[Store Sales] - [Measures].[Store Cost]</Formula> <CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/> </CalculatedMember></CalculatedMembers>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"<CalculatedMember name="Profit" dimension="Measures" formula="[Measures].[Store Sales] - [Measures].[Store Cost]"> <CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/></CalculatedMember>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:
| Propiedad | Propósito | Valor de ejemplo |
|---|---|---|
FORMAT_STRING | Controla cómo se muestra el valor en los clientes | "$#,##0.00" |
DATATYPE | Indica a los clientes XMLA el tipo de retorno | "Numeric", "Integer", "String" |
SOLVE_ORDER | Prioridad 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')"<CalculatedMember name="Conditional Profit" dimension="Measures"> <Formula>[Measures].[Store Sales] - [Measures].[Store Cost]</Formula> <CalculatedMemberProperty name="FORMAT_STRING" expression="Iif(Value < 0, '|($#,##0.00)|style=red', '|$#,##0.00|style=green')"/></CalculatedMember>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%"<CalculatedMember name="Profit Growth" dimension="Measures" formula="([Measures].[Profit] - [Measures].[Profit last Period]) / [Measures].[Profit last Period]" visible="true"> <CalculatedMemberProperty name="FORMAT_STRING" value="0.0%"/></CalculatedMember>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])"<Cube name="Warehouse"> <!-- ... dimensions and measure groups ... -->
<NamedSets> <NamedSet name="Top Sellers"> <Formula> TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse Sales]) </Formula> </NamedSet> </NamedSets></Cube>Después podrá usar [Top Sellers] directamente en MDX:
SELECT {[Measures].[Warehouse Sales]} ON COLUMNS, {[Top Sellers]} ON ROWSFROM [Warehouse]WHERE [Time].[Year].[1997]Lo que podría devolver:
| Warehouse | Warehouse Sales |
|---|---|
| Treehouse Distribution | 31,116.37 |
| Jorge Garcia, Inc. | 30,743.77 |
| Artesia Warehousing, Inc. | 29,207.96 |
| Jorgensen Service Storage | 22,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.25total balance = 1800 3 Carol 0.75La 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"<MeasureGroup name="Balances" table="account_fact"> <Measures> <Measure name="Balance" column="balance" aggregator="sum"/> </Measures> <DimensionLinks> <ForeignKeyLink dimension="Date" foreignKeyColumn="date_key"/> <BridgeLink dimension="Customer" bridgeTable="account_owner" factForeignKeyColumn="account_id" bridgeFactKeyColumn="account_id" bridgeDimensionKeyColumn="customer_id"/> </DimensionLinks></MeasureGroup>| Atributo | Obligatorio | Descripción |
|---|---|---|
dimension | sí | La dimensión muchos-a-muchos que este enlace resuelve. |
bridgeTable | sí | La tabla física que contiene el mapeo fact↔dimensión. Debe estar declarada en <PhysicalSchema>. |
factForeignKeyColumn | sí | Columna en la tabla de hechos a la que vuelve la unión del puente (la clave de granularidad del fact). |
bridgeFactKeyColumn | sí | Columna en el puente que coincide con factForeignKeyColumn. |
bridgeDimensionKeyColumn | sí | Columna en el puente que coincide con la clave de la dimensión. |
aggregation | no | fullCount (por defecto) o weighted. Véase más abajo. |
weightColumn | no | Columna 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"<Table name="account_fact"> <Key><Column name="account_id"/></Key></Table>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"<BridgeLink dimension="Customer" bridgeTable="account_owner" factForeignKeyColumn="account_id" bridgeFactKeyColumn="account_id" bridgeDimensionKeyColumn="customer_id" aggregation="weighted" weightColumn="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"<Dimension name="Customer" table="dim_customer" key="Customer"> <Attributes> <Attribute name="Segment" keyColumn="segment"/> <Attribute name="Customer" keyColumn="customer_id" nameColumn="customer_name"/> </Attributes> <Hierarchies> <Hierarchy name="By Segment" allMemberName="All Customers"> <Level attribute="Segment"/> <Level attribute="Customer"/> </Hierarchy> </Hierarchies></Dimension>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 quefullCountpueda deduplicar. (Esto es lo que hace que los rollups multinivel sean correctos.) weightedrequiere unaweightColumn;fullCountignora 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):fullCountdeduplica la expresión calc en la granularidad del fact, yweightedla 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"<Measure name="Order Amount" column="amount" aggregator="sum" distinctKeyColumn="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"→SUMsobre las claves distintas (la formasum_distinctde LookML).aggregator="avg"→AVGsobre las claves distintas (average_distinctde 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
distinctKeyColumndebe 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"yaggregator="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
DISTINCTa 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/avgsimple. Este es el objetivo natural parasum_distinct/average_distinctde 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.