I have a fairly strong preference for choosing simple solutions over complex solutions, and using Oracle-supplied packages over writing custom code – provided the difference in cost (whether that’s in human effort, run-time resources or licence fees) is acceptable. Sometimes, though, the gap between simplicity and cost is so extreme that a hand-crafted solution is clearly the better choice. Here’s an idea prompted by a recent visit to a site that makes use of materialized views and also happens to be licensed for the partitioning option.
The client has one materialized view that is particularly large, but need only be refreshed once per day. Unfortunately the users may want to run queries while the MV is being refreshed so a “complete refresh” using the “truncate / insert append” mechanism isn’t an option, and the call to dbms_mview.refresh results in a long and expensive (in terms of undo, redo and time) delete/insert cycle to replace all the data. Here’s a possible alternative that eliminates a huge amount of undo, redo and time, doesn’t seem to cost anything (beyond the – not so cheap – partitioning license), and doesn’t introduce complicated code.
In outline, you can do the following:
- Create a materialized view on a prebuilt partitioned table with a single partition.
- Create a simple clone of the materialized view table
- Every 24 hours (or any other time you want) truncate and re-populate the clone then exchange it with the single partition
Queries that are running against the partitioned table will follow the data segment as the exchange takes place so continuity should be no problem. You can use the append and nologging options for the materialized view and its clone, eliminating lots of undo and redo; it’s even safe to compress the table, and the index will be the minimum size of the data. Here’s some sample code – first we drop and create the relevant objects – note the compress, pctfree and nologging used on the segments relating to materialized views:
rem rem Script: mv_exchange.sql rem Author: Jonathan Lewis rem Dated: Aug 2013 rem Purpose: rem rem Last tested rem 11.2.0.2 rem drop materialized view mv1; drop table mv1; drop table mv_clone; drop table t1; create table t1 as with generator as ( select --+ materialize rownum id from dual connect by level <= 1e4 -- > comment to avoid wordpress format issue ) select mod(rownum-1, 1000) n1, trunc(dbms_random.value(0,1000)) val, trunc(dbms_random.value(0,1000)) qty, rpad('x',100) padding from generator v1, generator v2 where rownum <= 1e4 -- > comment to avoid wordpress format issue ; create table mv1 ( n1, sum_val, sum_qty, count_rows ) compress pctfree 0 partition by range (n1) ( partition p_only values less than (maxvalue) ) as select n1, sum(val) sum_val, sum(qty) sum_qty, count(*) count_rows from t1 group by n1 ; create index mv1_i1 on mv1(n1) local pctfree 0; create materialized view mv1 on prebuilt table never refresh enable query rewrite as select n1, sum(val) sum_val, sum(qty) sum_qty, count(*) count_rows from t1 group by n1 ; create table mv_clone compress pctfree 0 nologging as select * from mv1 where rownum < 1 ; create index mvc_i1 on mv_clone(n1) nologging pctfree 0;
Now we can do a quick check to see that the materialized view will be used – but we have to allow “stale” data to let the optimizer use a prebuilt MV [Correction: there are alternatives, see comments 2 and 7 below].
alter session set query_rewrite_integrity = 'stale_tolerated'; set autotrace on explain; select n1, count(*) ct, sum(qty)/count(*) my_avg from t1 where n1 in (150) group by n1 ; set autotrace off
Now we can insert some new data to the base table, and run through the procedure of refreshing the materialized view.
insert into t1 select n1 + 50, val + 50, qty + 50, padding from t1 where qty > 900 ; commit; truncate table mv_clone reuse storage; insert /*+ append */ into mv_clone select /*+ norewrite */ n1, sum(val) sum_val, sum(qty) sum_qty, count(*) count_rows from t1 group by n1 ; commit; begin dbms_stats.gather_table_stats( ownname => user, tabname =>'MV_CLONE', method_opt => 'for all columns size 1' ); end; / alter table mv1 exchange partition p_only with table mv_clone including indexes without validation ;
A couple of important details:
- I’ve included the “reuse storage” option with the truncate call because I know the data will always reuse the same amount of space.
- The “no_rewrite” hint on the SQL to refresh the clone is necessary, or Oracle would rewrite the query to use the (out of date) MV to populate the new MV
- The global (table-level) stats on the MV will be out of date after the exchange – you may want to address this, but they may be good enough. (In 12c you can set the preferences on the simple table to generate a synopsis so that if you also keep synopses on the partitioned table the global stats can be kept up to date.)
It’s interesting to note that 12c has enhanced the call to refresh materialized views with an “out of place” option. This actually does roughly what I’ve done above – it creates a copy of the materialized view in a separate table (with a name like RV$nnnnn – where nnnnn is the hexadecimal representation of the object_id of the original materialized view) – and then swaps the object ids of the old and new versions of the materialized views. Unfortunately it doesn’t seem to use an append strategy (though if you have indexes on the materialized view and you set them to nologging option they are recreated – after the new table has been populated – with the nologging option). Most significantly, though, you can’t use the “atomic” option to use this feature to refresh multiple materialized views to the same SCN.
As a general guideline – if you’re doing this kind of regular truncate/populate code (or drop/recreate) it’s a nice idea to use a separate tablespace for the objects which are constantly allocating and releasing space. Doing this probably won’t give you any performance benefits (though it’s possible that there may be some reduction in contention for space management blocks) but from a management perspective it’s nice to have a simple overview of the different characteristic behaviour of different types of object; it also gives you the ability to set up different storage management – uniform vs. autoallocate extents, ASSM vs. freelists.
Jonathan,
Thanks for sharing this problem and your interesting approach to solve it. Couple of questions, if you don’t mind
1. Is it safe to assume that the MV was/could not be “fast refreshed” ?
2. I was wondering whether DBMS_REDEFINITION can be used in place of partitioning (and EXCHANGE PARTITION) in this approach. I suspect it may be more expensive (in terms of resource utilization) than the EXCHANGE PARTITION but partitioning is still an extra-cost option.
Comment by Narendra — September 12, 2013 @ 10:08 pm BST Sep 12,2013 |
Narenda,
1) Yes, I believe it wasn’t considered appropriate
2) Without working through the details I can’t work out whether it would be possible to subvert the routines that create the replacement table – but it might be possible. In terms of work, though it would be very similar because it’s “create table, insert/append” then modify the data dictionary. The main benefit would come, as you say, from not having to pay the partitioning license.
Comment by Jonathan Lewis — September 13, 2013 @ 7:51 pm BST Sep 13,2013 |
Hi Jonathan,
thanx for the interesting post. I want to add, that you can use “ALTER MATERIALIZED VIEW mv1 CONSIDER FRESH” to be able to use query rewrite not only in stale_tolerated but also in trusted query_rewrite_integrity mode.
Cheers Mathias
Comment by Mathias Zarick — September 13, 2013 @ 3:49 pm BST Sep 13,2013 |
Mathias,
Excellent point, thanks – I’d completely forgotten about that possibility.
Comment by Jonathan Lewis — September 13, 2013 @ 7:09 pm BST Sep 13,2013 |
Jonathan,
your example already contains all the relevant information but perhaps it’s worth noting that for the use in partition exchange the clone has to be a complete structural clone: different compression settings for an index for example may lead to “ORA-28665 table and partition must have same compression attribute” – so the tuning of segment design should be completed before the exchange is implemented (of course that’s always a good idea, I assume …).
The article is a relief: some years ago I used a similar strategy (but not with regular materialized view) to refresh dimension tables and was suprised that I could not find similar examples – and so I feared that there was a major flaw in the design that I didn’t realize…
Martin
Comment by Martin Preiss — September 14, 2013 @ 12:01 am BST Sep 14,2013 |
[…] и чтобы не забылось, – ссылка на пост Дж.Льюиса MV Refresh, где маэстро описывает экономичный (в плане redo/undo) […]
Pingback by 12c: о пользе Temporary Undo и реализации out-of-place обновления матвью в 11g | Oracle mechanics — September 16, 2013 @ 9:24 pm BST Sep 16,2013 |
Good idea. I had used the method of having two tables “behind” one synonym. Users query the synonym pointing to table A while table B is being refreshed. Once the B refresh is complete, the synonym switches from table A to table B. Obviously the refresh’s can use TRUNCATE and APPEND. But not Query Rewrite — which is the advantage an MV has.
Comment by Hemant K Chitale — September 18, 2013 @ 7:36 am BST Sep 18,2013 |
Hi Jonathan,
I can vague remember the combination of truncate reuse storage with (parallel) insert append did cause problems effectively not reusing the space and always appending new extents. Not sure if it was in Oracle 8 or 9.
Test show that in 11 this combination works fine – nice to know I can use it again:)
Can you confirm?
Thanks,
Jaromir D.B. Nemec
Comment by Jaromir D.B. Nemec — September 18, 2013 @ 8:31 am BST Sep 18,2013 |
Jaromir,
I don’t recall seeing this bug – but if it was reported for 8 or 9 it’s likely that a report was raised and the bug fixed. There have been various anomalies with excess space usage on parallel insert/append, of course, because of the way that every parallel execution slave behaves as if it’s the sole creator of the data segment. Various people (including me) have written notes about that behaviour as it has evolved – I think Christian Antognini may be the most recent commentator on that issue.
Comment by Jonathan Lewis — September 20, 2013 @ 9:30 am BST Sep 20,2013 |
Hi Jonathan,
just a brief correction:
A Materialized View on Prebuilt Table does not need QUERY_REWRITE_INTEGRITY=stale_tolerated in general.
See here for an example:
Kind regards
Uwe
Comment by Uwe Hesse — September 20, 2013 @ 11:44 am BST Sep 20,2013 |
Uwe,
Thanks for the demonstration.
Mathias made the same note in an earlier comment – but I was so distracted by his reminder of “consider fresh” for a specific view that I failed to make any comment about it.
Comment by Jonathan Lewis — September 20, 2013 @ 1:51 pm BST Sep 20,2013 |
Thank you for making my code look nice. I forgot to copy
SQL> create table t as select 1 as col1 from dual;
CONSIDER FRESH is also not generally needed for MVs on prebuilt tables. qed :-)
Comment by Uwe Hesse — September 20, 2013 @ 2:03 pm BST Sep 20,2013 |
Uwe,
I’ve added the “create table” to the original comment.
I’ll have to add a comment at the end of the article summing up the two comments.
Comment by Jonathan Lewis — September 20, 2013 @ 2:18 pm BST Sep 20,2013 |
Uwe,
Still, CONSIDER FRESH is needed after you exchange partition on the prebuilt table, because the materialized view becomes stale.
Regards,
Piotr
Comment by Piotr Hajkowski — July 3, 2014 @ 12:14 pm BST Jul 3,2014 |
Thank for this post. We are considering implement this solution in a productive environment. Now we have problems with the read access to the materialized view while the refresh process is executed. What happen with the select over the materialized view while the exchange partition is been processed?
Comment by Andrés — October 21, 2014 @ 3:04 pm BST Oct 21,2014 |
[…] технологии «двойных/дублирующих матвью» в виде prebuilt partitioned table with a single partition, либо другими методами, в этом […]
Pingback by 12c: Простейшее использование INMEMORY при обновлении Materialized View | Oracle mechanics — January 15, 2018 @ 6:14 pm GMT Jan 15,2018 |
[…] time ago I wrote a blog note describing a hack for refreshing a large materialized view with minimum overhead by taking advantage of a single-partition partitioned table. This note […]
Pingback by 12c MView refresh | Oracle Scratchpad — February 15, 2022 @ 12:14 pm GMT Feb 15,2022 |
[…] Out of place MV refresh – 11g style (Sept 2013) – Meeting the client’s requirements for refreshing an MV before 12c “out of place” refresh existed. […]
Pingback by Materialized Views catalogue | Oracle Scratchpad — February 16, 2022 @ 6:36 pm GMT Feb 16,2022 |