# Using Aggregate Awareness > Tutorial on how to optimize the query time by using Aggregate Awareness :::tip Important Please refer to [Aggregate Awareness](/docs/aggregate-awareness) to see the full documentations. ::: ## Introduction To improve query performance and reduce query costs when reporting from large datasets or tables, data teams typically create materialized/persisted views (physical tables) of different aggregations. This tutorial will show an example on how to create and utilize such persisted aggregations using Holistics' **Aggregate Awareness**. ## Scenario Let's say we have an `ecommerce` Dataset with these Relationships: ```aml Dataset ecommerce { __engine__: 'aql' label: 'Ecommerce' description: '' data_source_name: 'hlite_demo' models: [ demo_order_items, demo_orders, demo_products, demo_merchants, demo_countries, demo_cities, demo_categories, demo_users ] relationships: [ relationship(demo_order_items.product_id > demo_products.id, true) , relationship(demo_order_items.order_id > demo_orders.id, true) , relationship(demo_orders.user_id > demo_users.id, true) , relationship(demo_users.city_id > demo_cities.id, true) , relationship(demo_cities.country_code > demo_countries.code, true) , relationship(demo_products.merchant_id > demo_merchants.id, true) , relationship(demo_products.category_id > demo_categories.id, true) ] owner: 'scott.bui@holistics.io' } ``` ## Use case: Sum of Item Quantity sold over Year ### Without Aggregate Awareness To find out the **Sum of item quantity sold over Year**, we can make this exploration: * Table Fields: * `orders.created_at` (transform: `Year`) * `order_items.quantity` (aggregation: `Sum`) If we run the above Executed Query with `EXPLAIN ANALYZE`, we get this query execution analysis: ```sql EXPLAIN ANALYZE SELECT TO_CHAR((CAST ( (DATE_TRUNC ( 'year', (CAST ( "demo_orders"."created_at" AS timestamptz )) AT TIME ZONE 'Europe/London' )) AT TIME ZONE 'Europe/London' AS timestamptz )) AT TIME ZONE 'Europe/London', 'YYYY-MM-DD HH24:MI:SS.US') AS "dy_do_ca_01c493", SUM("demo_order_items"."quantity") AS "s_doi_q_df1043" FROM "demo"."order_items" "demo_order_items" LEFT JOIN "demo"."orders" "demo_orders" ON "demo_order_items"."order_id" = "demo_orders"."id" GROUP BY 1 ``` Some highlights: The Database has to * Aggregate on **54,783** rows * Use **793kB** of memory * Take **62.797ms** in total ### Using Aggregate Awareness #### 1. Define the Pre-Aggregate For our use case, we can define a Pre-Aggregate with: * 1 Dimension: * `demo_orders.created_at` (time_granularity: `year`) * 1 Measure: * `demo_order_items.quantity` (aggregation: `sum`) * Persistence: * FullPersistence. This is the most basic/straightforward persistence mode to use. ```aml pre_aggregates: { aggregated_quantity: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) time_granularity: 'year' } measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } persistence: FullPersistence { schema: 'persisted' } } } ``` #### 2. Persist the Pre-Aggregate Before actually persisting the Pre-Aggregate, we need to create the database schema to store the going-to-be-persisted tables. In many databases, we can create the schema using a simple SQL: ```sql CREATE SCHEMA persisted; ``` The schema name that we choose is `persisted`. Make sure it matches the `schema` that you define in the `persistence` of your Pre-Aggregate. Then, we need to trigger the persistence. There are 3 ways to trigger a Pre-Aggregate Persistence in Holistics: * API * Schedules * UI Let's use the UI because it is most convenient for this tutorial: 1. Go to the **List** view of the Dataset 2. Click the Run button on our Pre-Aggregate (`aggregated_quantity`) 3. Confirm 4. Wait for the persistence job to finish #### 3. Test the exploration Now when we run the same exploration again, we will see that Holistics automatically uses the aggregated table! If we run the new Executed Query with `EXPLAIN ANALYZE`, we get this query execution analysis: ```sql EXPLAIN ANALYZE SELECT TO_CHAR((CAST ( "aggregated_quantity"."pa_created_at" AS timestamptz )) AT TIME ZONE 'Europe/London', 'YYYY-MM-DD HH24:MI:SS.US') AS "dy_do_ca_01c493", MAX("aggregated_quantity"."pa_sum_quantity") AS "s_doi_q_df1043" FROM "persisted"."HPA_8d3841d4cbe47c86:f0c246a01792a43a_T1716200122" "aggregated_quantity" GROUP BY 1 ``` We immediately got a huge performance boost: the Database only has to * Aggregate on **5** rows (10,000 times less data) * Use **40kB** of memory (20 times less memory) * Take **0.276ms** in total (> 200 times faster) ## Use case: Sum of Item Quantity sold by Category over Year ### Without Pre-Aggregate Because of the new dimension `demo_categories.name`, Holistics cannot re-use our existing Pre-Aggregate `aggregated_quantity`, because `aggregated_quantity` has _coarser_ granularity than our exploration. If we run the above Executed Query with `EXPLAIN ANALYZE`, we get this query execution analysis: ```sql EXPLAIN ANALYZE SELECT TO_CHAR((CAST ( (DATE_TRUNC ( 'year', (CAST ( "demo_orders"."created_at" AS timestamptz )) AT TIME ZONE 'Europe/London' )) AT TIME ZONE 'Europe/London' AS timestamptz )) AT TIME ZONE 'Europe/London', 'YYYY-MM-DD HH24:MI:SS.US') AS "dy_do_ca_01c493", "demo_categories"."name" AS "dc_n_8a4e8a", SUM("demo_order_items"."quantity") AS "s_doi_q_df1043" FROM "demo"."order_items" "demo_order_items" LEFT JOIN "demo"."orders" "demo_orders" ON "demo_order_items"."order_id" = "demo_orders"."id" LEFT JOIN "demo"."products" "demo_products" ON "demo_order_items"."product_id" = "demo_products"."id" LEFT JOIN "demo"."categories" "demo_categories" ON "demo_products"."category_id" = "demo_categories"."id" GROUP BY 1, 2 ``` Some highlights: The Database has to * Aggregate on **54,783** rows * Take **102.314ms** in total ### Using Aggregate Awareness #### 1. Define the Pre-Aggregate In this case, we can update our existing Pre-Aggregate to support more dimensions. If we look again at the Relationships, we would notice that `Categories` is on the one-side of the relationship with `Products`. Therefore, we only need to add `demo_products.id` into our Pre-Aggregate and Holistics will take care of the rest! ```aml pre_aggregates: { aggregated_quantity: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) time_granularity: 'year' } // BEGIN new codes dimension pa_product_id { for: r(demo_products.id) } // END new codes measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } persistence: FullPersistence { schema: 'persisted' } } } ``` #### 2. Persist the Pre-Aggregate Re-persist the Pre-Aggregate using the same steps as the first use case: 1. Go to the **List** view of the Dataset 2. Click the Run button on our Pre-Aggregate (`aggregated_quantity`) 3. Confirm 4. Wait for the persistence job to finish #### 3. Test the exploration We see that Holistics is able to use our Pre-Aggregate! If we run the above Executed Query with `EXPLAIN ANALYZE`, we get this query execution analysis: ```sql EXPLAIN ANALYZE SELECT TO_CHAR((CAST ( "aggregated_quantity"."pa_created_at" AS timestamptz )) AT TIME ZONE 'Europe/London', 'YYYY-MM-DD HH24:MI:SS.US') AS "dy_do_ca_01c493", "demo_categories"."name" AS "dc_n_8a4e8a", SUM("aggregated_quantity"."pa_sum_quantity") AS "s_doi_q_df1043" FROM "persisted"."HPA_8d3841d4cbe47c86:2f77e1908c1b7df2_T1716202922" "aggregated_quantity" LEFT JOIN "demo"."products" "demo_products" ON "aggregated_quantity"."pa_product_id" = "demo_products"."id" LEFT JOIN "demo"."categories" "demo_categories" ON "demo_products"."category_id" = "demo_categories"."id" GROUP BY 1, 2 ``` The Database only has to * Aggregate on **15,289** rows (nearly 4 times less data) * Take **17.010ms** in total (> 200 times faster) #### 4. Bonus! As shown above, Holistics Aggregate Awareness is relationship-aware and can leverage the Dataset Relationships to make the most out of your Pre-Aggregates. Using the exact same Pre-Aggregate `aggregated_quantity` that we defined earlier, we can also efficiently perform many other explorations such as: **without** having to add dimensions like `demo_categories.name`, `demo_products.name`, or `demo_merchants.name` into the Pre-Aggregate! ## Use case: Persist Pre-Aggregate using dbt :::info SQL Dialect Note This example uses Postgresql SQL Dialect in `persistence.table_name` and in the dbt SQL. ::: #### 1. Define Holistics Pre-Aggregate To let Holistics know that you will be handling the persistence yourself, use `ExternalPersistence`: ```aml pre_aggregates: { aggregated_quantity: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) time_granularity: 'year' } dimension pa_product_id { for: r(demo_products.id) } measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } // BEGIN new codes persistence: ExternalPersistence { table_name: '"persisted"."aggregated_quantity"' } // END new codes } } ``` #### 2. Persists the Pre-Aggregate using dbt ```sql {{ config(materialized='table', schema='persisted', alias='aggregated_quantity') }} SELECT DATE_TRUNC ( 'year', "demo_orders"."created_at" ) AS "pa_created_at", "demo_products"."id" AS "pa_product_id", SUM("demo_order_items"."quantity") AS "pa_sum_quantity" FROM "demo"."order_items" "demo_order_items" LEFT JOIN "demo"."orders" "demo_orders" ON "demo_order_items"."order_id" = "demo_orders"."id" LEFT JOIN "demo"."products" "demo_products" ON "demo_order_items"."product_id" = "demo_products"."id" GROUP BY 1, 2 ``` ## Use case: Sum of Item Quantity sold over Month Earlier, we pre-aggregated on the `time_granularity` of `year`, so Holistics cannot use that pre-aggregate for aggregations on `month` granulariy. #### 1. Define the Pre-Aggregate Now, to pre-aggregate for `month`, here are some options: 1. **Modify** the `time_granularity` of `aggregated_quantity` to `month` * This makes the Pre-Aggregate less fast for `year` aggregations. However, it should still be very fast. 2. **Create** a new Pre-Aggregate with `time_granularity: 'month'` * This makes it fast to do aggregations on both `month` and `year`. However, it will cost more storage to store the pre-aggregated data and more maintenance effort. ```aml pre_aggregates: { aggregated_quantity: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) // BEGIN new codes time_granularity: 'month' // END new codes } measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } persistence: FullPersistence { schema: 'persisted' } } } ``` ```aml pre_aggregates: { aggregated_quantity: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) time_granularity: 'year' } measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } persistence: FullPersistence { schema: 'persisted' } } // BEGIN new codes aggregated_quantity_by_month: PreAggregate { dimension pa_created_at { for: r(demo_orders.created_at) time_granularity: 'month' } measure pa_sum_quantity { for: r(demo_order_items.quantity) aggregation_type: 'sum' } persistence: FullPersistence { schema: 'persisted' } } // END new codes } ``` #### 2. Persist the Pre-Aggregate Re-persist the Pre-Aggregate using the same steps as the first use case: 1. Go to the **List** view of the Dataset 2. Click the Run button on our Pre-Aggregate (`aggregated_quantity`) 3. Confirm 4. Wait for the persistence job to finish #### 3. Test the "Create" option Let's say we go with the option to "Create" a new Pre-Aggregate, this will be the result: Holistics is also time-granularity-aware. Thus, it can use `aggregated_quantity_by_month` (that we just created) for aggregation on `quarter` granularity as well! If we try aggregating on `year` again, Holistics can use the initial Pre-Aggregate `aggregated_quantity`: