Understanding table fan out
To demonstrate why symmetric aggregates are so useful, let’s start with explaining what table fan out means. Fan out occurs when a table containing metrics joins to a dimension table in a way that results in multiple matching rows, leading to inflated results during aggregation. This example uses two tables -customers and orders.
Each row in the customers table represents a unique customer:
| customer_id | name | loyalty_points |
|---|---|---|
| 1 | Bloberta Smith | 100 |
| 2 | Blobzilla Lee | 200 |
| 3 | Blobina Chen | 150 |
orders table represents a unique order placed by a customer:
| order_id | customer_id | order_date | amount |
|---|---|---|---|
| 101 | 1 | 2025-03-25 | $50 |
| 102 | 1 | 2025-03-26 | $30 |
| 103 | 2 | 2025-03-27 | $80 |
| 104 | 3 | 2025-03-28 | $20 |
| 105 | 3 | 2025-03-29 | $40 |
one-to-many.
The fan out of data occurs when the tables are joined together. The following table demonstrates what a joined table might look like - note that the customer’s loyalty_points are repeated for each order:
| customer_id | name | loyalty_points | order_id | amount |
|---|---|---|---|---|
| 1 | Bloberta Smith | 100 | 101 | $50 |
| 1 | Bloberta Smith | 100 | 102 | $30 |
| 2 | Blobzilla Lee | 200 | 103 | $80 |
| 3 | Blobina Chen | 150 | 104 | $20 |
| 3 | Blobina Chen | 150 | 104 | $20 |
| 3 | Blobina Chen | 150 | 105 | $40 |
Handling fan out from joins
If you were to sum the totalloyalty_points for each customer from the join table, you’d end up overstating Bloberta’s and Blobina’s points by x the number of orders. To prevent incorrect results like these, it’s important to declare a primary_key for tables and, when defining joins, a relationship_type.
Before Omni calculates an aggregation on a fanned out table, Omni will check the table’s primary key - the unique key per row - to ensure values aren’t double-counted in the aggregation. If Omni detects potential for fan out, it will prompt you to define primary keys within your views.
To handle the example from the previous section, you could define the following in the model:
customers table
orders table
Relationship between customers and orders
Handling fan out in materialized tables
Let’s say you pushed the logic to join customers and orders down into your transformations layer. As this is now a single materialized table, Omni no longer has the context of the relationship between the underlying models. In this scenario, you can leverage theaggregate_type and the custom_primary_key_sql parameters to tell Omni how to properly aggregate on nested fields:
Symmetric aggregates on a materialized table
Handling fan out from unnested columns
While similar to the join scenario, this example performs an unnest join on an array instead of joining two tables together. For example, this could occur when you have anorders table and an array of objects that represent the individual order items.
You can leverage the custom_primary_key_sql_for_quick_aggs parameter to handle this scenario:
Symmetric aggregates on an unnested column
SUM but not MIN or MAX - based on the order_item_price dimension will be assigned a custom primary key of order_item_id. In this example, that will apply to the total_price measure.