In data warehouse environments, it is common to use materialized views to speed up the performance of commonly used queries. A materialized view is simply a pre-built query that stores the results of the query in a table. With the query rewrite capability enabled, Oracle can automatically make use of materialized views without the user even knowing. Let’s take a look at a simple order table for a generic store along with a denormalized table for dates in the order system and some sample data:
CREATE TABLE dim_dates
(date_id DATE,
date_desc VARCHAR2(100),
month_id NUMBER,
month_desc VARCHAR2(100),
year_id NUMBER,
year_desc VARCHAR2(100),
CONSTRAINT dim_dates_pk PRIMARY KEY (date_id));
CREATE TABLE fact_sales
(date_id DATE,
user_id NUMBER,
product_id NUMBER,
amount NUMBER,
CONSTRAINT fact_sales_r01 FOREIGN KEY (date_id)
REFERENCES dim_dates (date_id));
INSERT INTO dim_dates
SELECT dte,
TO_CHAR(dte, 'DD MONTH YYYY'),
TO_CHAR(dte, 'YYYYMM'),
TO_CHAR(dte, 'MONTH YYYY'),
TO_CHAR(dte, 'YYYY'),
TO_CHAR(dte, 'YYYY')
FROM
(SELECT DISTINCT
TRUNC(SYSDATE, 'YEAR') + MOD(rownum, 1000) dte
FROM all_objects);
INSERT INTO fact_sales
SELECT TRUNC(SYSDATE, 'YEAR') + MOD(rownum, 1000),
MOD(rownum, 100),
MOD(rownum, 10),
ABS(DBMS_RANDOM.random)/1000
FROM all_objects;
BEGIN
dbms_stats.gather_table_stats(USER,'DIM_DATES',CASCADE=>TRUE);
END;
/
BEGIN
dbms_stats.gather_table_stats(USER,'FACT_SALES',CASCADE=>TRUE);
END;
/
Now, supposed from our knowledge of the business, that users look at the sales by month much more often than by day. So we create a materialized view to aggregate this information:
CREATE MATERIALIZED VIEW agg_sales_monthly
BUILD IMMEDIATE
REFRESH ON DEMAND
ENABLE QUERY REWRITE
AS
SELECT d.month_id, f.product_id, SUM(f.amount)
FROM fact_sales f, dim_dates d
WHERE f.date_id = d.date_id
GROUP BY d.month_id, f.product_id;
BEGIN
dbms_stats.gather_table_stats(USER,'AGG_SALES_MONTHLY',CASCADE=>TRUE);
END;
/
Now, even if a user does not know about the materialized view, Oracle can make use of it for faster query results (Note: make sure the system or session parameter query_rewrite_enabled = true to allow for rewrites):
EXPLAIN PLAN FOR
SELECT d.month_id, SUM(f.amount)
FROM fact_sales f, dim_dates d
WHERE f.date_id = d.date_id
GROUP BY d.month_id;
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------
Plan hash value: 1473209833
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 33 | 4 |
| 1 | HASH GROUP BY | | 33 | 4 |
| 2 | MAT_VIEW REWRITE ACCESS FULL| AGG_SALES_MONTHLY | 330 | 3 |
--------------------------------------------------------------------------
However, Oracle cannot make use of this aggregate if the wants to look at the data at a different level of aggregation, such as the year:
EXPLAIN PLAN FOR
SELECT d.year_id, SUM(f.amount)
FROM fact_sales f, dim_dates d
WHERE f.date_id = d.date_id
GROUP BY d.year_id;
PLAN_TABLE_OUTPUT
---------------------------------------------------------
Plan hash value: 588016919
---------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
---------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 56|
| 1 | HASH GROUP BY | | 3 | 56|
|* 2 | HASH JOIN | | 57272 | 52|
| 3 | TABLE ACCESS FULL| DIM_DATES | 1000 | 4|
| 4 | TABLE ACCESS FULL| FACT_SALES | 57272 | 48|
---------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("F"."DATE_ID"="D"."DATE_ID")
From the explain plan, it is clear that Oracle is using the base tables to execute this query. However, since year can be derived from month, Oracle should use the materialized view to return these results faster. With Oracle dimensions, we can inform Oracle about these relationships and allow the rewrite to occur:
CREATE DIMENSION dim_time
LEVEL day IS dim_dates.date_id
LEVEL month IS dim_dates.month_id
LEVEL year IS dim_dates.year_id
HIERARCHY time_rollup
(day CHILD OF
month CHILD OF
year);
That dimension lets Oracle know how the time element can be aggregated, which can then be used to form the execution plan of the query. So if we try the year aggregate again:
EXPLAIN PLAN FOR
SELECT d.year_id, SUM(f.amount)
FROM fact_sales f, dim_dates d
WHERE f.date_id = d.date_id
GROUP BY d.year_id;
PLAN_TABLE_OUTPUT
---------------------------------------------------------
Plan hash value: 588016919
---------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
---------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 56|
| 1 | HASH GROUP BY | | 3 | 56|
|* 2 | HASH JOIN | | 57272 | 52|
| 3 | TABLE ACCESS FULL| DIM_DATES | 1000 | 4|
| 4 | TABLE ACCESS FULL| FACT_SALES | 57272 | 48|
---------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("F"."DATE_ID"="D"."DATE_ID")
The rewrite is still not occurring? That’s because Oracle has a default setting for query_rewrite_integrity = enforced. That setting means that rewrites will only occur on relationships that are enforced by constraints. By changing the system or session value for that parameter to trusted, Oracle can make use of relationships that are known, but not explicitly enforced.
ALTER SESSION SET query_rewrite_integrity = trusted;
EXPLAIN PLAN FOR
SELECT d.year_id, SUM(f.amount)
FROM fact_sales f, dim_dates d
WHERE f.date_id = d.date_id
GROUP BY d.year_id;
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------
Plan hash value: 4091167185
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 10|
| 1 | HASH GROUP BY | | 3 | 10|
|* 2 | HASH JOIN | | 710 | 9|
| 3 | VIEW | | 71 | 5|
| 4 | HASH UNIQUE | | 71 | 5|
| 5 | TABLE ACCESS FULL | DIM_DATES | 1000 | 4|
| 6 | MAT_VIEW REWRITE ACCESS FULL| AGG_SALES_MONTHLY | 330 | 3|
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 – access("from$_subquery$_005"."MONTH_ID"="AGG_SALES_MONTHLY"."MONTH_ID")
In conclusion, by using materialized views and dimensions, Oracle can perform some advanced rewrites automatically, which leads to much better performance in your data warehouse environment.
Link to script that contains the examples in this post.