{"id":14253,"date":"2020-06-05T21:09:24","date_gmt":"2020-06-05T19:09:24","guid":{"rendered":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/"},"modified":"2020-06-05T21:09:24","modified_gmt":"2020-06-05T19:09:24","slug":"oracle-index-on-joins","status":"publish","type":"post","link":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/","title":{"rendered":"Oracle 12c &#8211; pre-built join index"},"content":{"rendered":"<h2>By Franck Pachot<\/h2>\n<p>.<br \/>\nThis post is part of a series of small examples of recent features. I&#8217;m running this in the Oracle 20c preview in the Oracle Cloud. I have created a few tables in the <a href=\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/oracle-select-from-file\" rel=\"noopener noreferrer\" target=\"_blank\">previous post<\/a> with a mini-snowflake scheme: a fact table CASES with the covid-19 cases per country and day. And a dimension hierarchy for the country with COUNTRIES and CONTINENTS tables.<\/p>\n<p>This title may look strange for people used to Oracle. I am showing the REFRESH FAST ON STATEMENT Materialized View clause here, also known as &#8220;Synchronous Refresh for Materialized Views&#8221;. This name makes sense only when you already know materialized views, complete and fast refreshes, on commit and on-demand refreshes&#8230; But that&#8217;s not what people will look for. Indexes are also refreshed by the statements, synchronously. Imagine that they were called &#8220;Synchronous Refresh for B*Trees&#8221;, do you think they would have been so popular?<\/p>\n<p>A materialized view, like an index, is a redundant structure where data is stored in a different physical layout in order to be optimal for alternative queries. For example, you ingest data per date (which is the case in my covid-19 table &#8211; each day a new row with the covid-19 cases per country). But if I want to query all points for a specific country, those are scattered though the physical segment that is behind the table (or the partition). With an index on the country_code, I can identify easily one country, because the index is sorted on the country. I may need to go to the table to get the rows, and that is expensive, but I can avoid it by adding all the attributes in the index. With Oracle, as with many databases, we can build covering indexes, for real index-only access, even if they don&#8217;t mention those names.<\/p>\n<p>But with my snowflake schema, I&#8217;ll not have the country_code in the fact table and I have to join to a dimension. This is more expensive because the index on the country_name will get the country_id and then I have to go to an index on the fact table to get the rows for this country_id. When it comes to joins, I cannot index the result of the join (I&#8217;m skipping bitmap join indexes here because I&#8217;m talking about covering indexes). What I would like is an index with values from multiple tables.<\/p>\n<p>A materialized view can achieve much more than an index. We can build the result of the join in one table. And no need for event sourcing or streaming here to keep it up to date. No need to denormalize and risk inconsistency. When NoSQL pioneers tell you that storage is cheap and redundancy is the way to scale, just keep your relational database for integrity and build materialized views on top. When they tell you that joins are expensive, just materialize them upfront. Before 12c, keeping those materialized views consistent with the source required either:<\/p>\n<ol>\n<li>materialized view logs which is similar to event sourcing except that ON COMMIT refresh is strongly consistent<\/li>\n<li>partition change tracking which is ok for bulk changes, when scaling big data<\/li>\n<\/ol>\n<p>This is different from indexes which are maintained immediately: when you update the row, the index is synchronized because your session has the values and the rowid and can go directly to update the index entry.<\/p>\n<h3>refresh fast on statement<\/h3>\n<p>In 12c you have the benefit from both: index-like fast maintenance with rowid access, and the MView possibility of querying pre-build joins. Here is an example on the tables created in the <a href=\"https:\/\/www.dbi-services.com\/blog\/oracle-select-from-file\" rel=\"noopener noreferrer\" target=\"_blank\">previous post<\/a>.<\/p>\n<pre><code>\nSQL&gt; \n create materialized view flatview refresh fast on statement as\n  select daterep,continent_name,country_name,cases from cases join countries using(country_id) join continents \n  using(continent_id) where cases&gt;0;\n\nselect daterep,continent_name,country_name,cases from cases join countries using(country_id) join continents using(continent_id) where cases&gt;0\n                                                                                                                                             *\nERROR at line 2:\nORA-12015: cannot create a fast refresh materialized view from a complex query\n\n<\/code><\/pre>\n<p>There are some limitations when we want fast refresh and we have a utility to help us understand what we have to change or add in our select clause.<\/p>\n<h3>explain_mview<\/h3>\n<p>I need to create the table where the messages will be written to by this utility:<\/p>\n<pre><code>\n@ ?\/rdbms\/admin\/utlxmv\n\nSQL&gt; \nset sqlformat ansiconsole\nSQL&gt; \nset pagesize 10000\n<\/code><\/pre>\n<p>This has created mv_capabilities_table and I can run dbms_mview.explain_mview() now.<\/p>\n<p>Here is the call, with the select part of the materialized view:<\/p>\n<pre><code>\nSQL&gt; \nexec dbms_mview.explain_mview('-\n  select daterep,continent_name,country_name,cases from cases join countries using(country_id) join continents using(continent_id) where cases&gt;0-\n  ');\n\nPL\/SQL procedure successfully completed.\n\nSQL&gt; \nselect possible \"?\",capability_name,related_text,msgtxt from mv_capabilities_table where capability_name like 'REFRESH_FAST%' order by seq;\n\n   ?                  CAPABILITY_NAME    RELATED_TEXT                                                                 MSGTXT\n____ ________________________________ _______________ ______________________________________________________________________\nN    REFRESH_FAST\nN    REFRESH_FAST_AFTER_INSERT                        inline view or subquery in FROM list not supported for this type MV\nN    REFRESH_FAST_AFTER_INSERT                        inline view or subquery in FROM list not supported for this type MV\nN    REFRESH_FAST_AFTER_INSERT                        view or subquery in from list\nN    REFRESH_FAST_AFTER_ONETAB_DML                    see the reason why REFRESH_FAST_AFTER_INSERT is disabled\nN    REFRESH_FAST_AFTER_ANY_DML                       see the reason why REFRESH_FAST_AFTER_ONETAB_DML is disabled\nN    REFRESH_FAST_PCT                                 PCT FAST REFRESH is not possible if query contains an inline view\n\nSQL&gt; \nrollback;\n\nRollback complete.\n<\/code><\/pre>\n<p>&#8220;inline view or subquery in FROM list not supported for this type MV&#8221; is actually very misleading. I use ANSI joins and they are translated to query blocks and this is not supported.<\/p>\n<h3>No ANSI joins<\/h3>\n<p>I rewrite it with the old join syntax:<\/p>\n<pre><code>\nSQL&gt; \nexec dbms_mview.explain_mview('-\n  select daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0-\n  ');\n\nPL\/SQL procedure successfully completed.\n\nSQL&gt; \nselect possible \"?\",capability_name,related_text,msgtxt from mv_capabilities_table where capability_name like 'REFRESH_FAST%' order by seq;\n\n   ?                  CAPABILITY_NAME       RELATED_TEXT                                                                      MSGTXT\n____ ________________________________ __________________ ___________________________________________________________________________\nN    REFRESH_FAST\nN    REFRESH_FAST_AFTER_INSERT        CONTINENTS         the SELECT list does not have the rowids of all the detail tables\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CASES         the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.COUNTRIES     the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CONTINENTS    the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_ONETAB_DML                       see the reason why REFRESH_FAST_AFTER_INSERT is disabled\nN    REFRESH_FAST_AFTER_ANY_DML                          see the reason why REFRESH_FAST_AFTER_ONETAB_DML is disabled\nN    REFRESH_FAST_PCT                                    PCT is not possible on any of the detail tables in the materialized view\n\nSQL&gt; \nrollback;\n\nRollback complete.\n<\/code><\/pre>\n<p>Now I need to add the ROWID of the table CONTINENTS in the materialized view.<\/p>\n<h3>ROWID for all tables<\/h3>\n<p>Yes, as I mentioned, the gap between indexes and materialized views is shorter. The REFRESH FAST ON STATEMENT requires access by rowid to update the materialized view, like when a statement updates an index.<\/p>\n<pre><code>\nSQL&gt; \nexec dbms_mview.explain_mview('-\n  select continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0-\n  ');\n\nPL\/SQL procedure successfully completed.\n\nSQL&gt; \nselect possible \"?\",capability_name,related_text,msgtxt from mv_capabilities_table where capability_name like 'REFRESH_FAST%' order by seq;\n\n   ?                  CAPABILITY_NAME       RELATED_TEXT                                                                      MSGTXT\n____ ________________________________ __________________ ___________________________________________________________________________\nN    REFRESH_FAST\nN    REFRESH_FAST_AFTER_INSERT        COUNTRIES          the SELECT list does not have the rowids of all the detail tables\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CASES         the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.COUNTRIES     the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CONTINENTS    the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_ONETAB_DML                       see the reason why REFRESH_FAST_AFTER_INSERT is disabled\nN    REFRESH_FAST_AFTER_ANY_DML                          see the reason why REFRESH_FAST_AFTER_ONETAB_DML is disabled\nN    REFRESH_FAST_PCT                                    PCT is not possible on any of the detail tables in the materialized view\nSQL&gt; \nrollback;\n\nRollback complete.\n<\/code><\/pre>\n<p>Now, the ROWID for COUNTRIES.<\/p>\n<p>I continue the and finally I&#8217;ve added ROWID for all tables involved:<\/p>\n<pre><code>\nSQL&gt; \nexec dbms_mview.explain_mview('-\n  select continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0-\n  ');\n\nPL\/SQL procedure successfully completed.\n\nSQL&gt; \nselect possible \"?\",capability_name,related_text,msgtxt from mv_capabilities_table where capability_name like 'REFRESH_FAST%' order by seq;\n\n   ?                  CAPABILITY_NAME       RELATED_TEXT                                                                      MSGTXT\n____ ________________________________ __________________ ___________________________________________________________________________\nN    REFRESH_FAST\nN    REFRESH_FAST_AFTER_INSERT        COUNTRIES          the SELECT list does not have the rowids of all the detail tables\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CASES         the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.COUNTRIES     the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CONTINENTS    the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_ONETAB_DML                       see the reason why REFRESH_FAST_AFTER_INSERT is disabled\nN    REFRESH_FAST_AFTER_ANY_DML                          see the reason why REFRESH_FAST_AFTER_ONETAB_DML is disabled\nN    REFRESH_FAST_PCT                                    PCT is not possible on any of the detail tables in the materialized view\n\nSQL&gt; \nrollback;\n\nRollback complete.\n\nSQL&gt; \nexec dbms_mview.explain_mview('-\n  select cases.rowid case_rowid,countries.rowid country_rowid,continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0-\n  ');\n\nPL\/SQL procedure successfully completed.\n\nSQL&gt; select possible \"?\",capability_name,related_text,msgtxt from mv_capabilities_table where capability_name like 'REFRESH_FAST%' order by seq;\n   ?                  CAPABILITY_NAME       RELATED_TEXT                                                                      MSGTXT\n____ ________________________________ __________________ ___________________________________________________________________________\nN    REFRESH_FAST\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CASES         the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.COUNTRIES     the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_INSERT        DEMO.CONTINENTS    the detail table does not have a materialized view log\nN    REFRESH_FAST_AFTER_ONETAB_DML                       see the reason why REFRESH_FAST_AFTER_INSERT is disabled\nN    REFRESH_FAST_AFTER_ANY_DML                          see the reason why REFRESH_FAST_AFTER_ONETAB_DML is disabled\nN    REFRESH_FAST_PCT                                    PCT is not possible on any of the detail tables in the materialized view\n\nSQL&gt; \nrollback;\n\nRollback complete.\n<\/code><\/pre>\n<p>Ok, now another message: &#8220;the detail table does not have a materialized view log&#8221;. But that&#8217;s exactly the purpose of statement-level refresh: being able to fast refresh without creating and maintaining materialized view logs, and without full-refreshing a table or a partition.<\/p>\n<p>This&#8217;t the limit of DBMS_MVIEW.EXPLAIN_MVIEW. Let&#8217;s try to create the materialized view now:<\/p>\n<pre><code>\nSQL&gt; \ncreate materialized view flatview refresh fast on statement as\n  select cases.rowid case_rowid,countries.rowid country_rowid,continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0;\n\nselect cases.rowid case_rowid,countries.rowid country_rowid,continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0\n                                                                                                                                                                                                                                                                                    *\nERROR at line 2:\nORA-32428: on-statement materialized join view error: Shape of MV is not\nsupported(composite PK)\n\nSQL&gt;\n<\/code><\/pre>\n<p>That&#8217;s clear. I had created the fact primary key on the compound foreign keys.<\/p>\n<h3>Surrogate key on fact table<\/h3>\n<p>This is not allowed by statement-level refresh, so let&#8217;s change that:<\/p>\n<pre><code>\nSQL&gt; \nalter table cases add (case_id number);\n\nTable altered.\n\nSQL&gt; \nupdate cases set case_id=rownum;\n\n21274 rows updated.\n\nSQL&gt; \nalter table cases drop primary key;\n\nTable altered.\n\nSQL&gt; \nalter table cases add primary key(case_id);\n\nTable altered.\n\nSQL&gt; \nalter table cases add unique(daterep,country_id);\nTable altered.\n<\/code><\/pre>\n<p>I have added a surrogate key and defined a unique key for the composite one.<\/p>\n<p>Now the creation is sucessful:<\/p>\n<pre><code>\nSQL&gt; \ncreate materialized view flatview refresh fast on statement as\n  select cases.rowid case_rowid,countries.rowid country_rowid,continents.rowid continent_rowid,daterep,continent_name,country_name,cases from cases , countries , continents where cases.country_id=countries.country_id and countries.continent_id=continents.continent_id and cases&gt;0;\n\nMaterialized view created.\n<\/code><\/pre>\n<p>Note that I tested later and I am able to create it with the ROWID from the fact table CASES only. But that&#8217;s not a good idea: in order to propagate any change to the underlying tables, the materialized view must have the ROWID, like an index. I consider as a bug the possibility to do it.<\/p>\n<p>Here are the columns stored in my materialized view:<\/p>\n<pre><code>\nSQL&gt; \ndesc flatview\n\n              Name    Null?            Type\n__________________ ________ _______________\nCASE_ROWID                  ROWID\nCOUNTRY_ROWID               ROWID\nCONTINENT_ROWID             ROWID\nDATEREP                     VARCHAR2(10)\nCONTINENT_NAME              VARCHAR2(30)\nCOUNTRY_NAME                VARCHAR2(60)\nCASES                       NUMBER\n<\/code><\/pre>\n<p>Storing the ROWID is not something we should recommend as some maintenance operations may change the physical location of rows. You will need to complete refresh the materialized view after an online move for example.<\/p>\n<h3>No-join query<\/h3>\n<p>I&#8217;ll show query rewrite in another blog post. For the moment, I&#8217;ll query this materialized view directly.<\/p>\n<p>Here is a query similar to the one in the previous post:<\/p>\n<pre><code>\nSQL&gt; \nselect continent_name,country_name,top_date,top_cases from (\n select continent_name,country_name,daterep,cases\n  ,first_value(daterep)over(partition by continent_name order by cases desc) top_date\n  ,first_value(cases)over(partition by continent_name order by cases desc)top_cases\n  ,row_number()over(partition by continent_name order by cases desc) r\n  from flatview\n )\n where r=1 order by top_cases\n;\n\n   CONTINENT_NAME                COUNTRY_NAME      TOP_DATE    TOP_CASES\n_________________ ___________________________ _____________ ____________\nOceania           Australia                   23\/03\/2020             611\nAfrica            South_Africa                05\/06\/2020            3267\nEurope            Russia                      12\/05\/2020           11656\nAsia              China                       13\/02\/2020           15141\nAmerica           United_States_of_America    26\/04\/2020           48529\n\n<\/code><\/pre>\n<p>I have replaced the country_id and continent_id by their name as I didn&#8217;t put them in my materialized view. And I repeated the window function everywhere if you want to run the same in versions lower than 20c.<\/p>\n<p>This materialized view is a table. I can partition it by hash to scatter the data. I can cluster on another column. I can add indexes. I have the full power of a SQL databases on it, without the need to join if you think that joins are slow. If you come from NoSQL you can see it like a DynamoDB global index. You can query it without joining, fetching all attributes with one call, and filtering on another key than the primary key. But here we have always strong consistency: the changes are replicated immediately, fully ACID. They will be committed or rolled back by the same transaction that did the change. They will be replicated synchronously or asynchronously with read-only replicas. <\/p>\n<h3>DML on base tables<\/h3>\n<p>Let&#8217;s do some changes here, lowering the covid-19 cases of CHN to 42%:<\/p>\n<pre><code>\nSQL&gt; \nalter session set sql_trace=true;\n\nSession altered.\n\nSQL&gt; \nupdate cases set cases=cases*0.42 where country_id=(select country_id from countries where country_code='CHN');\n\n157 rows updated.\n\nSQL&gt; \nalter session set sql_trace=false;\n\nSession altered.\n\n<\/code><\/pre>\n<p>I have set sql_trace because I want to have a look at the magic behind it.<\/p>\n<p>Now running my query on the materialized view:<\/p>\n<pre><code>\n\nSQL&gt; \nselect continent_name,country_name,top_date,top_cases from (\n select continent_name,country_name,daterep,cases\n  ,first_value(daterep)over(partition by continent_name order by cases desc) top_date\n  ,first_value(cases)over(partition by continent_name order by cases desc)top_cases\n  ,row_number()over(partition by continent_name order by cases desc) r\n  from flatview\n )\n where r=1 order by top_cases\n;\n\n   CONTINENT_NAME                COUNTRY_NAME      TOP_DATE    TOP_CASES\n_________________ ___________________________ _____________ ____________\nOceania           Australia                   23\/03\/2020             611\nAfrica            South_Africa                05\/06\/2020            3267\nAsia              India                       05\/06\/2020            9851\nEurope            Russia                      12\/05\/2020           11656\nAmerica           United_States_of_America    26\/04\/2020           48529\n\n<\/code><\/pre>\n<p>CHN is not the top one in Asia anymore with the 42% correction.<\/p>\n<p>The changes were immediately propagated to the materialized view like when indexes are updated, and we can see that in the trace:<\/p>\n<pre><code>\nSQL&gt; \ncolumn value new_value tracefile\nSQL&gt; \nselect value from v$diag_info where name='Default Trace File';\n                                                                     VALUE\n__________________________________________________________________________\n\/u01\/app\/oracle\/diag\/rdbms\/cdb1a_iad154\/CDB1A\/trace\/CDB1A_ora_49139.trc\n\n\nSQL&gt; \ncolumn value clear\nSQL&gt; \nhost tkprof &amp;tracefile trace.txt\n\nTKPROF: Release 20.0.0.0.0 - Development on Thu Jun 4 15:43:13 2020\n\nCopyright (c) 1982, 2020, Oracle and\/or its affiliates.  All rights reserved.\n\nSQL&gt; \nhost awk '\/\"FLATVIEW\/,\/^[*]\/' trace.txt\n\n<\/code><\/pre>\n<p>sql_trace instruments all executions with time and number of rows. tkprof aggregates those for analysis.<\/p>\n<p>The trace shows two statements on my materialized view: DELETE and INSERT.<\/p>\n<p>The first one is about removing the modified rows.<\/p>\n<pre><code>\nDELETE FROM \"DEMO\".\"FLATVIEW\"\nWHERE\n \"CASE_ROWID\" = :1\n\ncall     count       cpu    elapsed       disk      query    current        rows\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\nParse      158      0.00       0.00          0          0          0           0\nExecute    158      0.02       0.10         38        316        438         142\nFetch        0      0.00       0.00          0          0          0           0\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\ntotal      316      0.02       0.10         38        316        438         142\n\nMisses in library cache during parse: 1\nMisses in library cache during execute: 1\nOptimizer mode: ALL_ROWS\nParsing user id: 634     (recursive depth: 1)\nNumber of plan statistics captured: 3\n\nRows (1st) Rows (avg) Rows (max)  Row Source Operation\n---------- ---------- ----------  ---------------------------------------------------\n         0          0          0  DELETE  FLATVIEW (cr=2 pr=0 pw=0 time=2080 us starts=1)\n         1          0          1   INDEX UNIQUE SCAN I_OS$_FLATVIEW (cr=2 pr=0 pw=0 time=2055 us starts=1 cost=1 size=10 card=1)(object id 78728)\n\n<\/code><\/pre>\n<p>This has been done row-by-row but is optimized with an index on ROWID that has been created autonomously with my materialized view.<\/p>\n<p>The second one is inserting the modified rows:<\/p>\n<pre><code>\nINSERT INTO  \"DEMO\".\"FLATVIEW\" SELECT \"CASES\".ROWID \"CASE_ROWID\",\n  \"COUNTRIES\".ROWID \"COUNTRY_ROWID\",\"CONTINENTS\".ROWID \"CONTINENT_ROWID\",\n  \"CASES\".\"DATEREP\" \"DATEREP\",\"CONTINENTS\".\"CONTINENT_NAME\" \"CONTINENT_NAME\",\n  \"COUNTRIES\".\"COUNTRY_NAME\" \"COUNTRY_NAME\",\"CASES\".\"CASES\" \"CASES\" FROM\n  \"CONTINENTS\" \"CONTINENTS\",\"COUNTRIES\" \"COUNTRIES\", (SELECT \"CASES\".ROWID\n  \"ROWID\",\"CASES\".\"DATEREP\" \"DATEREP\",\"CASES\".\"CASES\" \"CASES\",\n  \"CASES\".\"COUNTRY_ID\" \"COUNTRY_ID\" FROM \"DEMO\".\"CASES\" \"CASES\" WHERE\n  \"CASES\".ROWID=(:Z)) \"CASES\" WHERE \"CASES\".\"COUNTRY_ID\"=\n  \"COUNTRIES\".\"COUNTRY_ID\" AND \"COUNTRIES\".\"CONTINENT_ID\"=\n  \"CONTINENTS\".\"CONTINENT_ID\" AND \"CASES\".\"CASES\"&gt;0\n\ncall     count       cpu    elapsed       disk      query    current        rows\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\nParse      158      0.00       0.00          0          0          0           0\nExecute    158      0.01       0.11          0        734        616         142\nFetch        0      0.00       0.00          0          0          0           0\n------- ------  -------- ---------- ---------- ---------- ----------  ----------\ntotal      316      0.02       0.12          0        734        616         142\n\n\nMisses in library cache during parse: 1\nMisses in library cache during execute: 1\nOptimizer mode: ALL_ROWS\nParsing user id: 634     (recursive depth: 1)\nNumber of plan statistics captured: 3\n\nRows (1st) Rows (avg) Rows (max)  Row Source Operation\n---------- ---------- ----------  ---------------------------------------------------\n         0          0          0  LOAD TABLE CONVENTIONAL  FLATVIEW (cr=5 pr=0 pw=0 time=235 us starts=1)\n         1          0          1   NESTED LOOPS  (cr=2 pr=0 pw=0 time=25 us starts=1 cost=3 size=52 card=1)\n         1          0          1    NESTED LOOPS  (cr=2 pr=0 pw=0 time=20 us starts=1 cost=2 size=42 card=1)\n         1          0          1     TABLE ACCESS BY USER ROWID CASES (cr=1 pr=0 pw=0 time=14 us starts=1 cost=1 size=22 card=1)\n         1          0          1     TABLE ACCESS BY INDEX ROWID COUNTRIES (cr=1 pr=0 pw=0 time=5 us starts=1 cost=1 size=20 card=1)\n         1          0          1      INDEX UNIQUE SCAN SYS_C009414 (cr=0 pr=0 pw=0 time=3 us starts=1 cost=0 size=0 card=1)(object id 78716)\n         1          0          1    TABLE ACCESS BY INDEX ROWID CONTINENTS (cr=1 pr=0 pw=0 time=2 us starts=1 cost=1 size=10 card=1)\n         1          0          1     INDEX UNIQUE SCAN SYS_C009412 (cr=0 pr=0 pw=0 time=1 us starts=1 cost=0 size=0 card=1)(object id 78715)\n\n<\/code><\/pre>\n<p>Again, a row-by-row insert apparently as the &#8220;execute count&#8221; is nearly the same as the &#8220;rows count&#8221;. 157 is the number of rows I have updated. <\/p>\n<p>You may think that this is a huge overhead, but those operations are optimized for a long time. The materialized view is refreshed and ready for optimal queries: no need to queue, stream, reorg, vacuum,&#8230; And I can imagine that if this feature is used, it will be optimized with bulk operations which would allow compression.<\/p>\n<h3>Truncate<\/h3>\n<p>This looks all good. But&#8230; what happens if I truncate the table?<\/p>\n<pre><code>\nSQL&gt; \ntruncate table cases;\n\nTable truncated.\n\nSQL&gt; \nselect continent_name,country_name,top_date,top_cases from (\n select continent_name,country_name,daterep,cases\n  ,first_value(daterep)over(partition by continent_name order by cases desc) top_date\n  ,first_value(cases)over(partition by continent_name order by cases desc)top_cases\n  ,row_number()over(partition by continent_name order by cases desc) r\n  from flatview\n )\n where r=1 order by top_cases\n;\n\n   CONTINENT_NAME                COUNTRY_NAME      TOP_DATE    TOP_CASES\n_________________ ___________________________ _____________ ____________\nOceania           Australia                   23\/03\/2020             611\nAfrica            South_Africa                05\/06\/2020            3267\nAsia              India                       05\/06\/2020            9851\nEurope            Russia                      12\/05\/2020           11656\nAmerica           United_States_of_America    26\/04\/2020           48529\n\n<\/code><\/pre>\n<p>Nothing changed. This is dangerous. You need to refresh it yourself. This may be a bug. What will happen if you insert data back? Note that, like with triggers, direct-path inserts will be transparently run as conventional inserts.<\/p>\n<pre><code>\nSQL&gt; \nexec dbms_mview.refresh('DEMO.FLATVIEW');\nSQL&gt; \ninsert into cases select rownum cases_id,daterep, geoid country_id,cases from covid where continentexp!='Other';\n\n21483 rows created.\n\nSQL&gt; \ncommit;\n\n   CONTINENT_NAME                COUNTRY_NAME      TOP_DATE    TOP_CASES\n_________________ ___________________________ _____________ ____________\nOceania           Australia                   23\/03\/2020             611\nAfrica            South_Africa                05\/06\/2020            3267\nEurope            Russia                      12\/05\/2020           11656\nAsia              China                       13\/02\/2020           15141\nAmerica           United_States_of_America    26\/04\/2020           48529\n\n<\/code><\/pre>\n<h3>Joins are not expensive<\/h3>\n<p>This feature is really good to pre-build the joins in a composition of tables, as a hierarchical key-value, or snowflake dimension fact table. You can partition, compress, order, filter, index,&#8230; as with any relational table. There no risk here with the denormalization as it is transparently maintained when you update the underlying tables.<\/p>\n<p>If you develop on a NoSQL database because you have heard that normalization was invented to reduce storage, which is not nexpensive anymore, that&#8217;s a myth (you can read this <a href=\"https:\/\/twitter.com\/FranckPachot\/status\/1253030043225395201\" rel=\"noopener noreferrer\" target=\"_blank\">long thread<\/a> to understand the origin of this myth). Normalization is about database integrity and <a href=\"https:\/\/twitter.com\/andy_pavlo\/status\/1253398920522317825?s=20\" rel=\"noopener noreferrer\" target=\"_blank\">separation lof logical and physical layers<\/a>. And that&#8217;s what Oracle Database implements with this feature: you update the logical view, tables are normalized for integrity, and the physical layer transparently maintains additional structures like indexes and materialized views to keep queries under single-digit milliseconds. Today you still need to think about indexes and materialized views to build. Some advisors may help. All those are the bricks for the future: an autonomous database where you define only the logical layer for your application and all those optimisations will be done in background.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>By Franck Pachot . This post is part of a series of small examples of recent features. I&#8217;m running this in the Oracle 20c preview in the Oracle Cloud. I have created a few tables in the previous post with a mini-snowflake scheme: a fact table CASES with the covid-19 cases per country and day. [&hellip;]<\/p>\n","protected":false},"author":27,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[955,229,59],"tags":[958,496,1489,1949,91,649,1980,96,209,458,1981,1982,1983],"type_dbi":[],"class_list":["post-14253","post","type-post","status-publish","format-standard","hentry","category-cloud","category-database-administration-monitoring","category-oracle","tag-12cr2","tag-bi","tag-dwh","tag-features","tag-index","tag-join","tag-on-statement","tag-oracle","tag-oracle-12c","tag-oracle-20c","tag-refresh","tag-statement-level","tag-synchronous"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.2 (Yoast SEO v27.2) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Oracle 12c - pre-built join index - dbi Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Oracle 12c - pre-built join index\" \/>\n<meta property=\"og:description\" content=\"By Franck Pachot . This post is part of a series of small examples of recent features. I&#8217;m running this in the Oracle 20c preview in the Oracle Cloud. I have created a few tables in the previous post with a mini-snowflake scheme: a fact table CASES with the covid-19 cases per country and day. [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\" \/>\n<meta property=\"og:site_name\" content=\"dbi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2020-06-05T19:09:24+00:00\" \/>\n<meta name=\"author\" content=\"Oracle Team\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Oracle Team\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"19 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\"},\"author\":{\"name\":\"Oracle Team\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee\"},\"headline\":\"Oracle 12c &#8211; pre-built join index\",\"datePublished\":\"2020-06-05T19:09:24+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\"},\"wordCount\":1684,\"commentCount\":0,\"keywords\":[\"12cR2\",\"BI\",\"DWH\",\"features\",\"index\",\"Join\",\"on-statement\",\"Oracle\",\"Oracle 12c\",\"Oracle 20c\",\"refresh\",\"statement-level\",\"synchronous\"],\"articleSection\":[\"Cloud\",\"Database Administration &amp; Monitoring\",\"Oracle\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\",\"url\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\",\"name\":\"Oracle 12c - pre-built join index - dbi Blog\",\"isPartOf\":{\"@id\":\"https:\/\/www.dbi-services.com\/blog\/#website\"},\"datePublished\":\"2020-06-05T19:09:24+00:00\",\"author\":{\"@id\":\"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\/\/www.dbi-services.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Oracle 12c &#8211; pre-built join index\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/#website\",\"url\":\"https:\/\/www.dbi-services.com\/blog\/\",\"name\":\"dbi Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.dbi-services.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee\",\"name\":\"Oracle Team\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g\",\"caption\":\"Oracle Team\"},\"url\":\"https:\/\/www.dbi-services.com\/blog\/author\/oracle-team\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Oracle 12c - pre-built join index - dbi Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/","og_locale":"en_US","og_type":"article","og_title":"Oracle 12c - pre-built join index","og_description":"By Franck Pachot . This post is part of a series of small examples of recent features. I&#8217;m running this in the Oracle 20c preview in the Oracle Cloud. I have created a few tables in the previous post with a mini-snowflake scheme: a fact table CASES with the covid-19 cases per country and day. [&hellip;]","og_url":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/","og_site_name":"dbi Blog","article_published_time":"2020-06-05T19:09:24+00:00","author":"Oracle Team","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Oracle Team","Est. reading time":"19 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#article","isPartOf":{"@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/"},"author":{"name":"Oracle Team","@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee"},"headline":"Oracle 12c &#8211; pre-built join index","datePublished":"2020-06-05T19:09:24+00:00","mainEntityOfPage":{"@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/"},"wordCount":1684,"commentCount":0,"keywords":["12cR2","BI","DWH","features","index","Join","on-statement","Oracle","Oracle 12c","Oracle 20c","refresh","statement-level","synchronous"],"articleSection":["Cloud","Database Administration &amp; Monitoring","Oracle"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/","url":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/","name":"Oracle 12c - pre-built join index - dbi Blog","isPartOf":{"@id":"https:\/\/www.dbi-services.com\/blog\/#website"},"datePublished":"2020-06-05T19:09:24+00:00","author":{"@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee"},"breadcrumb":{"@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.dbi-services.com\/blog\/oracle-index-on-joins\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/www.dbi-services.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Oracle 12c &#8211; pre-built join index"}]},{"@type":"WebSite","@id":"https:\/\/www.dbi-services.com\/blog\/#website","url":"https:\/\/www.dbi-services.com\/blog\/","name":"dbi Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.dbi-services.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/66ab87129f2d357f09971bc7936a77ee","name":"Oracle Team","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/f711f7cd2c9b09bf2627133755b569fb5be0694810cfd33033bdd095fedba86d?s=96&d=mm&r=g","caption":"Oracle Team"},"url":"https:\/\/www.dbi-services.com\/blog\/author\/oracle-team\/"}]}},"_links":{"self":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts\/14253","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/users\/27"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/comments?post=14253"}],"version-history":[{"count":0,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts\/14253\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/media?parent=14253"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/categories?post=14253"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/tags?post=14253"},{"taxonomy":"type","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/type_dbi?post=14253"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}