MemOptimized RowStore in Oracle Database 18c

The MemOptimized RowStore introduced in Oracle Database 18c is designed to improve the performance of simple queries accessing data via primary key columns only. An example of such a query is SELECT value FROM t WHERE key = :key where key is the only primary key column of table t. This feature is available for the following Oracle Database offerings only (see Licensing Information User Manual):

  • Oracle Database Enterprise Edition on Engineered Systems (EE-ES)
  • Oracle Database Cloud Service Enterprise Edition – Extreme Performance (DBCS EE-EP)
  • Oracle Database Exadata Cloud Service (ExaCS)

For this blog post I’ve used a Docker container running an Oracle Database 18c version 18.2.0.0.180417 on my MacBook Pro (Late 2016). The initialization parameter _exadata_feature_on=TRUE enabled technically the MemOptimized RowStore. This means that I expect the feature to work but with different performance metrics as on one of the officially supported environments.

Concept

The MemOptimized RowStore is conceptually best documented in Database Concepts. The idea is to store a heap-organized table completely in the memory within a subarea of the SGA. This subarea is named Memoptimized Pool and consists of the following two parts:

  • Memoptimize Buffer Area

This is a dedicated buffer cache for table blocks. 75% of the memoptimized pool is reserved for this buffer cache.

  • Hash Index

A hash index is a hash table/map as we know it from Java and other programming languages (associative array in PL/SQL). The primary key columns are used as key and a pointer to the block in the memoptimize buffer area is used as value. The hash index uses the other 25% of the memoptimized pool.

The size of the memoptimized pool is set by the initialization parameter MEMOPTIMIZE_POOL_SIZE. The default size is 0. Changing the value requires to restart of the database. The minimum size is 100M.

The following conditions must be met to use the MemOptimized RowStore:

  1. The table is marked as MEMOPTIMIZE FOR READ. See the memoptimize_read_clause for CREATE TABLE and ALTER TABLE statements.
  2. The table is heap-organized.
  3. The table has a primary key.
  4. The primary key is not an identity column.
  5. The table is not compressed.
  6. The table is not reference-partitioned.
  7. The table has at least one segment (use SEGMENT CREATION IMMEDIATE when creating tables).
  8. The table has been loaded in the memoptimized pool using dbms_memoptimize.populate.
  9. The table fits completely in the memoptimized pool.
  10. The query must be in the format SELECT <column_list> FROM <table> WHERE <primary_key_column> = <value>. The result columns must derive from the underlying table. Multiple primary key columns are supported, in this case, all primary key columns have to be defined in the where_clause. Additional predicates are not allowed.
  11. The initialization parameter STATISTICS_LEVEL must not be set to ALL.
  12. The optimizer hint GATHER_PLAN_STATISTICS must not be used.
  13. SQL trace must not be enabled.
  14. The query must not be executed within PL/SQL (neither static nor dynamic SQL is supported).
  15. The query must not be executed using the default database connection in a Java-stored procedure.
  16. The query must not be executed from a JDBC thin driver connection. You have to use OCI, otherwise the performance will be really bad, see the succeeding blog post MemOptimized RowStore in Oracle Database 18c with OCI. This might be a bug. [added on 2018-06-19].

If all conditions are met, then the row is fetched without a single logical I/O. In all other cases, you get either an error message or the query is executed the conventional way, using at least 3 logical I/Os (1 I/O for the root index block, 1 I/O for the index leaf block, 1 I/O for the table block).

Existing applications do not need to change their code to use the MemOptimized RowStore (besides some DDL).

But how can a row be fetched without an I/O? Technically it is a new kind of I/O which is no longer accounted for the wait event consistent gets. Instead, these operations are reported with new wait events:

  • memopt r lookups – counting every hash index lookup (regardless of the result)
  • memopt r hits– counting every successful hash index lookup (primary key found)
  • memopt r misses– counting every unsuccessful hash index lookup (primary key not found)

There are 65 wait events in v$statname for the MemOptimized RowStore. You find some descriptions in the Database Reference as well.

But why is this supposed to be faster than a single block access in the keep pool using a single-table hash cluster? The answer is given in the Introducing Oracle Database 18c whitepaper:

Key-value lookups then bypass the SQL execution layer and execute directly in the data access layer via an in-memory hash index.

And what are the expected performance gains? – I have not found any numbers in the documentation. Unfortunately, my tests are not conclusive in this area, since I’m running them in an unsupported environment. However, I’ve found an answer on the Ask TOM website, where Maria Colgan states the following:

the rowstore can be approximately 25% faster than a single-table hash cluster

Configure Database

Before we can use the MemOptimized RowStore we have to set the size of the memoptimized pool. In this case, I set the minimum size and restart the database.

Set Memoptimize Pool Size
ALTER SYSTEM SET memoptimize_pool_size = 100M SCOPE=SPFILE;
SHUTDOWN IMMEDIATE
STARTUP

Now the database has reserved 75 megabytes for the memoptimize buffer area and 25 megabytes for the hash index.

Create and Populate Table

Let’s create an empty table t4 with an memoptimize_read_clause.

Create Table t4
CREATE TABLE t4 (
   key    INTEGER            NOT NULL,
   value  VARCHAR2(30 CHAR)  NOT NULL,
   CONSTRAINT t4_pk PRIMARY KEY (key)
) 
SEGMENT CREATION IMMEDIATE
MEMOPTIMIZE FOR READ;

Please note, that the primary key definition on line 4 is required to avoid an ORA-62142: MEMOPTIMIZE FOR READ feature requires NOT DEFERRABLE PRIMARY KEY constraint on the table. And without the clause on line 6, we’d get an ORA-62156: MEMOPTIMIZE FOR READ feature not allowed on segment with deferred storage.

I use the following anonymous PL/SQL block to populate the table t4 with 100,000 rows and gather table statistics.

Populate Table t4
BEGIN
   dbms_random.seed(0);
   INSERT INTO t4 (key, value)
   SELECT rownum AS key, 
          dbms_random.string('x', round(dbms_random.value(5, 30), 0)) AS value
     FROM xmltable('1 to 100000');
   COMMIT;
   dbms_stats.gather_table_stats(ownname=>USER, tabname=>'T4');
END;
/

We may now run a query in an SQL client. I’ve used SQLcl because of the comprehensive statistics when using autotrace. The output is the result of the second execution.

Query t4 – Fallback to Conventional Index Access
SET LINESIZE 100
SET AUTOTRACE ON
SELECT * FROM t4 WHERE key = 42;

       KEY VALUE                         
---------- ------------------------------
        42 UKPBW05FQ1                    

Explain Plan
-----------------------------------------------------------

PLAN_TABLE_OUTPUT                                                                                   
----------------------------------------------------------------------------------------------------
Plan hash value: 1143490106                                                                         
                                                                                                    
------------------------------------------------------------------------------------------------    
| Id  | Operation                              | Name  | Rows  | Bytes | Cost (%CPU)| Time     |    
------------------------------------------------------------------------------------------------    
|   0 | SELECT STATEMENT                       |       |     1 |    24 |     2   (0)| 00:00:01 |    
|   1 |  TABLE ACCESS BY INDEX ROWID READ OPTIM| T4    |     1 |    24 |     2   (0)| 00:00:01 |    
|*  2 |   INDEX UNIQUE SCAN READ OPTIM         | T4_PK |     1 |       |     1   (0)| 00:00:01 |    
------------------------------------------------------------------------------------------------    
                                                                                                    
Predicate Information (identified by operation id):                                                 
---------------------------------------------------                                                 
                                                                                                    
   2 - access("KEY"=42)                                                                             

Statistics
-----------------------------------------------------------
               1  CPU used by this session
               1  CPU used when call started
               2  DB time
              42  Requests to/from client
              42  SQL*Net roundtrips to/from client
               3  buffer is not pinned count
             598  bytes received via SQL*Net from client
           83338  bytes sent via SQL*Net to client
               3  calls to get snapshot scn: kcmgss
               2  calls to kcmgcs
               3  consistent gets
               3  consistent gets examination
               3  consistent gets examination (fastpath)
               3  consistent gets from cache
               2  execute count
               1  index fetch by key
           24576  logical read bytes from cache
               1  memopt r lookups
               1  memopt r misses
              43  non-idle wait count
               2  opened cursors cumulative
               1  opened cursors current
               2  parse count (total)
               1  rows fetched via callback
               1  session cursor cache hits
               3  session logical reads
               1  sorts (memory)
            2011  sorts (rows)
               1  table fetch by rowid
              45  user calls

The execution plan on lines 20 and 21 shows the intention to use the MemOptimized RowStore (READ OPTIM). But the 3 consistent gets on line 41 indicates that a conventional index access has been used. We get the proof on lines 48 and 49. There was an access to the hash index (1 memopt r lookups), but no key with the value 42 has been found (1 memopt r misses). Hence the fallback to the conventional unique index access.

Populate MemOptimized RowStore

The following anonymous PL/SQL block populates the memoptimized pool for the table t4.

Populate Memoptimized Pool
BEGIN
   dbms_memoptimize.populate(schema_name=>USER, table_name=>'T4');
END;
/

It is important to note, that the memoptimized pool is populated in the background by a space management worker process. This call is just a request, with a own wait event memopt r populate tasks accepted.  Usually this is pretty fast, but to be sure you can check the relevant wait event before and after calling dbms_memoptimize.populate. Here’s an example:

Check Number of Rows Populated
SELECT n.name, s.value
  FROM v$sysstat s 
  JOIN v$statname n
    ON n.statistic# = s.statistic#
 WHERE n.name = 'memopt r rows populated';

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
memopt r rows populated                                              100000

100,000 rows are now in the memoptimized pool. Let’s query the table t4 again.

Query t4 – Using MemOptimized RowStore
SET LINESIZE 100
SET AUTOTRACE ON
SELECT * FROM t4 WHERE key = 42;

       KEY VALUE                         
---------- ------------------------------
        42 UKPBW05FQ1                    

Explain Plan
-----------------------------------------------------------

PLAN_TABLE_OUTPUT                                                                                   
----------------------------------------------------------------------------------------------------
Plan hash value: 1143490106                                                                         
                                                                                                    
------------------------------------------------------------------------------------------------    
| Id  | Operation                              | Name  | Rows  | Bytes | Cost (%CPU)| Time     |    
------------------------------------------------------------------------------------------------    
|   0 | SELECT STATEMENT                       |       |     1 |    24 |     2   (0)| 00:00:01 |    
|   1 |  TABLE ACCESS BY INDEX ROWID READ OPTIM| T4    |     1 |    24 |     2   (0)| 00:00:01 |    
|*  2 |   INDEX UNIQUE SCAN READ OPTIM         | T4_PK |     1 |       |     1   (0)| 00:00:01 |    
------------------------------------------------------------------------------------------------    
                                                                                                    
Predicate Information (identified by operation id):                                                 
---------------------------------------------------                                                 
                                                                                                    
   2 - access("KEY"=42)                                                                             

Statistics
-----------------------------------------------------------
              43  Requests to/from client
              43  SQL*Net roundtrips to/from client
             605  bytes received via SQL*Net from client
           83447  bytes sent via SQL*Net to client
               3  calls to get snapshot scn: kcmgss
               2  calls to kcmgcs
               2  execute count
               1  memopt r hits
               1  memopt r lookups
              44  non-idle wait count
               2  opened cursors cumulative
               1  opened cursors current
               2  parse count (total)
               1  session cursor cache count
               1  sorts (memory)
            2011  sorts (rows)
              46  user calls

As before, the execution plan on lines 20 and 21 shows the intention to use the MemOptimized RowStore (READ OPTIM). But in this case, there are no consistent gets. And on line 38 we have a successful hash index lookup (1 memopt r hits). A SQL query without logical I/Os, made possible by the MemOptimized RowStore.

Alternatives

What are your options, when your database does not provide a MemOptimized RowStore? I see the primarily the following alternatives:

  • Heap-organized table
  • Index-organized table
  • Single-table hash cluster

Let’s elaborate on them.

1. Heap-Organized Table

Heap-Organized Table t1
CREATE TABLE t1 (
   key    INTEGER            NOT NULL,
   value  VARCHAR2(30 CHAR)  NOT NULL,
   CONSTRAINT t1_pk PRIMARY KEY (key)
)
STORAGE (BUFFER_POOL KEEP);
ALTER INDEX t1_pk STORAGE (BUFFER_POOL KEEP);

This is very similar to table t4. The storage_clauses on lines 6 and 7 ensure that the table and index blocks are stored in the KEEP buffer pool.  This will reduce the physical I/Os when querying the table.

Accessing a single row requires 3 consistent gets as shown below.

Query t1
SET LINESIZE 100
SET AUTOTRACE ON
SELECT * FROM t1 WHERE key = 42;

       KEY VALUE                         
---------- ------------------------------
        42 UKPBW05FQ1                    

Explain Plan
-----------------------------------------------------------

PLAN_TABLE_OUTPUT                                                                                   
----------------------------------------------------------------------------------------------------
Plan hash value: 2347959165                                                                         
                                                                                                    
-------------------------------------------------------------------------------------               
| Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |               
-------------------------------------------------------------------------------------               
|   0 | SELECT STATEMENT            |       |     1 |    24 |     2   (0)| 00:00:01 |               
|   1 |  TABLE ACCESS BY INDEX ROWID| T1    |     1 |    24 |     2   (0)| 00:00:01 |               
|*  2 |   INDEX UNIQUE SCAN         | T1_PK |     1 |       |     1   (0)| 00:00:01 |               
-------------------------------------------------------------------------------------               
                                                                                                    
Predicate Information (identified by operation id):                                                 
---------------------------------------------------                                                 
                                                                                                    
   2 - access("KEY"=42)                                                                             

Statistics
-----------------------------------------------------------
              42  Requests to/from client
              42  SQL*Net roundtrips to/from client
               3  buffer is not pinned count
             598  bytes received via SQL*Net from client
           83426  bytes sent via SQL*Net to client
               2  calls to get snapshot scn: kcmgss
               2  calls to kcmgcs
               3  consistent gets
               3  consistent gets examination
               3  consistent gets examination (fastpath)
               3  consistent gets from cache
               1  cursor authentications
               2  execute count
               1  index fetch by key
           24576  logical read bytes from cache
              42  non-idle wait count
               2  opened cursors cumulative
               1  opened cursors current
               2  parse count (total)
               1  rows fetched via callback
               3  session logical reads
               1  sorts (memory)
            2011  sorts (rows)
               1  table fetch by rowid
              45  user calls

2. Index-Organized Table

Index-Organized Table t2
CREATE TABLE t2 (
   key    INTEGER            NOT NULL,
   value  VARCHAR2(30 CHAR)  NOT NULL,
   CONSTRAINT t2_pk PRIMARY KEY (key)
)
ORGANIZATION INDEX
STORAGE (BUFFER_POOL KEEP);

An index-organized table stores all its data within the index structure. This reduces the logical I/Os by one when accessing a single row via the primary key. We also use the KEEP buffer pool to minimize physical I/Os.

Accessing a single row requires 2 consistent gets as shown below.

Query t2
SET LINESIZE 100
SET AUTOTRACE ON
SELECT * FROM t2 WHERE key = 42;

       KEY VALUE                         
---------- ------------------------------
        42 UKPBW05FQ1                    

Explain Plan
-----------------------------------------------------------

PLAN_TABLE_OUTPUT                                                                                   
----------------------------------------------------------------------------------------------------
Plan hash value: 2827726509                                                                         
                                                                                                    
---------------------------------------------------------------------------                         
| Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |                         
---------------------------------------------------------------------------                         
|   0 | SELECT STATEMENT  |       |     1 |    24 |     1   (0)| 00:00:01 |                         
|*  1 |  INDEX UNIQUE SCAN| T2_PK |     1 |    24 |     1   (0)| 00:00:01 |                         
---------------------------------------------------------------------------                         
                                                                                                    
Predicate Information (identified by operation id):                                                 
---------------------------------------------------                                                 
                                                                                                    
   1 - access("KEY"=42)                                                                             

Statistics
-----------------------------------------------------------
              42  Requests to/from client
              42  SQL*Net roundtrips to/from client
               1  buffer is not pinned count
             598  bytes received via SQL*Net from client
           83426  bytes sent via SQL*Net to client
               2  calls to get snapshot scn: kcmgss
               2  calls to kcmgcs
               2  consistent gets
               2  consistent gets examination
               2  consistent gets examination (fastpath)
               2  consistent gets from cache
               1  cursor authentications
               2  execute count
               1  index fetch by key
           16384  logical read bytes from cache
              42  non-idle wait count
               2  opened cursors cumulative
               1  opened cursors current
               2  parse count (total)
               2  session logical reads
               1  sorts (memory)
            2011  sorts (rows)
              45  user calls

3. Single-Table Hash Cluster

Single-Table Hash Cluster
CREATE CLUSTER c3 (key INTEGER) 
   SIZE 256
   SINGLE TABLE HASHKEYS 100000
   STORAGE (BUFFER_POOL KEEP);

CREATE TABLE t3 (
   key    INTEGER            NOT NULL,
   value  VARCHAR2(30 CHAR)  NOT NULL,
   CONSTRAINT t3_pk PRIMARY KEY (key) -- to check uniqueness only
)
CLUSTER c3 (key);

A hash cluster is quite an old Oracle feature. I do not remember when it was introduced. It’s like it has always been around. The best option for primary key-based data retrieval, but a bit tricky to size. For sizing a hash cluster two parameters are important:

  • HASHKEYS

The HASHKEYS parameter (see line 3) defines the number of target buckets for the hash function. In this case, I chose 100,000. Without hash collisions, every key would be stored in its own bucket. But with this dataset, there are up to 5 keys which get stored in the same target bucket.

  • SIZE

The SIZE parameter (see line 2) defines the number of bytes initially reserved for a target bucket of a hash function. Since I know that there are up to 5 rows within a bucket and I want a bucket to be stored completely in a single block, I chose a size large enough for 5 rows. This leads to 256 bytes so that I can store 32 buckets in a single 8K block.

With these parameters, a cluster with 3,125 blocks will be created. It is probably a bit more. How much depends on the extent management configuration of the tablespace. This is optimal for our use case. But it is not optimal for full table scans, since we use 6-7 times more blocks than needed for a heap-organized table.

Accessing a single row in a correctly sized single-table hash cluster requires just 1 consistent get as shown below.

Query t3
SET LINESIZE 100
SET AUTOTRACE ON
SELECT * FROM t3 WHERE key = 42;

       KEY VALUE                         
---------- ------------------------------
        42 UKPBW05FQ1                    

Explain Plan
-----------------------------------------------------------

PLAN_TABLE_OUTPUT                                                                                   
----------------------------------------------------------------------------------------------------
Plan hash value: 180373899                                                                          
                                                                                                    
--------------------------------------------------------------------------                          
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |                          
--------------------------------------------------------------------------                          
|   0 | SELECT STATEMENT  |      |     1 |    24 |     1   (0)| 00:00:01 |                          
|*  1 |  TABLE ACCESS HASH| T3   |     1 |    24 |     1   (0)| 00:00:01 |                          
--------------------------------------------------------------------------                          
                                                                                                    
Predicate Information (identified by operation id):                                                 
---------------------------------------------------                                                 
                                                                                                    
   1 - access("KEY"=42)                                                                             

Statistics
-----------------------------------------------------------
               1  DB time
              42  Requests to/from client
              42  SQL*Net roundtrips to/from client
               1  buffer is not pinned count
             598  bytes received via SQL*Net from client
           83491  bytes sent via SQL*Net to client
               2  calls to get snapshot scn: kcmgss
               2  calls to kcmgcs
               1  cluster key scan block gets
               1  cluster key scans
               1  consistent gets
               1  consistent gets from cache
               1  consistent gets pin
               1  consistent gets pin (fastpath)
               2  execute count
            8192  logical read bytes from cache
              42  non-idle wait count
               2  opened cursors cumulative
               1  opened cursors current
               2  parse count (total)
               1  session cursor cache hits
               1  session logical reads
               1  sorts (memory)
            2011  sorts (rows)
              45  user calls

If you size the single-table hash cluster incorrectly, for example, by using SIZE 64 HASHKEYS 500 you end up with more than 150 consistent gets to access a single row resulting in bad performance.

Sizing a single-table hash cluster is really the key for best performance. However, for mixed workloads (PK access and other accesses to retrieve many rows) sizing becomes challenging and leads to a compromise. In such scenarios a heap-organized or index-organized table is easier to apply and may even be the better option.

Performance

Now let’s compare these four options using a PL/SQL and a Java program reading the table fully via 100,000 queries. Not a smart way to do it, but it should show the performance impact of the different data structures.


PL/SQL procedure

PL/SQL – 100,000 queries
CREATE OR REPLACE PROCEDURE p (in_table_name VARCHAR2) IS
   l_query VARCHAR2(1000 CHAR);
   l_value VARCHAR2(30 CHAR);
   l_start INTEGER;
   l_end   INTEGER;
BEGIN
   l_start := dbms_utility.get_time();
   l_query := 'SELECT value FROM ' || in_table_name || ' WHERE key = :key';
   FOR i IN 1..100000 LOOP
      EXECUTE IMMEDIATE l_query INTO l_value USING i;
   END LOOP;
   l_end := dbms_utility.get_time();
   dbms_output.put_line('read 100000 rows from ' || in_table_name || ' in ' ||
      to_char((l_end - l_start) / 100) || ' seconds.');
END p;
/


Java program

Java – 100,000 queries
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import oracle.jdbc.driver.OracleDriver;
public class J {
   private static boolean isRunningInDatabase() {
      return System.getProperty("oracle.jserver.version") != null;
   }
   public static void m(String tableName) throws SQLException {
      Connection conn;
      if (isRunningInDatabase()) {
         conn = new OracleDriver().defaultConnection();
      } else {
         conn = DriverManager.getConnection(
            "jdbc:oracle:thin:@//localhost:1521/odb.docker", "tvdca", "tvdca");
      }
      conn.setAutoCommit(false);
      long start = System.currentTimeMillis();
      String query = "SELECT value FROM " + tableName + " WHERE key = ?";      
      PreparedStatement ps = conn.prepareStatement(query);
      for (long i = 1; i <= 100000; i++) {
         ps.setLong(1, i);
         ResultSet rs = ps.executeQuery();
         while (rs.next()) {
            rs.getString("value");
         }
         rs.close();
      }
      ps.close();
      if (!isRunningInDatabase()) {
         conn.close();
      }
      long end = System.currentTimeMillis();
      System.out.println("read 100000 rows from " + tableName + " in " +
         String.valueOf((double) (end-start)/1000) + " seconds.");      
   }
   public static void main(String[] args) throws SQLException {
      m(args[0]);
   }
}


Both programs are doing the same work. They get a table name as a parameter and retrieve every row in the table via primary key access. The PL/SQL procedure runs within the database and the Java program outside of the database. The Java program needs to do 100,000 network round trips. For the PL/SQL program, these are just context switches between the PL/SQL and SQL engine. Therefore, the PL/SQL procedure calls are expected to be faster than the Java program executions.

Each program has been called five times for every table. The slowest and the fastest runtimes have been ignored. The average of the remaining three runtimes is used for the following chart.

The results look plausible for “t1 – heap-organized”, “t2 – index-organized” and “t3 – hash cluster”. But the runtimes for “t4 – memoptimized” are strange. For PL/SQL and Java. This requires further analysis.

blank

Analyzing PL/SQL Runtime for “t4 – memoptimized”

Let’s execute the PL/SQL procedure in a fresh session again.

Analyzing PL/SQL Runtime for t4 – memoptimized
SQL> connect tvdca/tvdca@odb
Connected.
SQL> set serveroutput on
SQL> exec p('t4')
read 100000 rows from t4 in 6.72 seconds.


PL/SQL procedure successfully completed.

SQL> SELECT n.name, s.sid, s.value
  2    FROM v$sesstat s 
  3    JOIN v$statname n
  4      ON n.statistic# = s.statistic#
  5   WHERE n.name in ('consistent gets','memopt r lookups', 'memopt r hits')
  6     AND s.value > 0
  7     AND s.sid = sys_context ('USERENV', 'SID')
  8   ORDER BY s.value desc;

NAME                                                                    SID      VALUE
---------------------------------------------------------------- ---------- ----------
consistent gets                                                         273     300025

Line 21 is interesting. 300,025 consistent gets. These are 300,000 more than expected. And there are no values for the wait events memopt r lookups and memopt r hits. This means Oracle uses a conventional access path instead of the MemOptimized RowStore. It’s the same execution plan as for “t1 – heap-organized” and the execution times are similar as well. As mentioned in the Concept chapter in the beginning, the MemOptimized RowStore cannot be used from PL/SQL.

Analyzing Java Runtime for “t4 – memoptimized”

To analyze the problem I’ve enabled SQL trace for the Java session. I was surprised by the runtime. Significantly faster with SQL trace enabled? The tkprof output revealed the reason. Here is an excerpt:

Tkprof Excerpt
SQL ID: 03z4487kpgfv3 Plan Hash: 1143490106

SELECT value 
FROM
 t4 WHERE key = :1 


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute 100000      0.65       7.10          0          0          0           0
Fetch   100000      0.95       7.36          0     300000          0      100000
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total   200001      1.60      14.47          0     300000          0      100000

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: ALL_ROWS
Parsing user id: 152  
Number of plan statistics captured: 1

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         1          1          1  TABLE ACCESS BY INDEX ROWID T4 (cr=3 pr=0 pw=0 time=599 us starts=1 cost=2 size=24 card=1)
         1          1          1   INDEX UNIQUE SCAN T4_PK (cr=2 pr=0 pw=0 time=562 us starts=1 cost=1 size=0 card=1)(object id 87514)

See the highlighted lines 12 and 25. We are back to a conventional access path as soon as we enable SQL trace. The runtime is similar to “t1 – heap-organized” plus some SQL trace overhead. SQL trace is a dead end.

Let’s try flame graphs. Luca Canali wrote an excellent blog post about flame graphs for Oracle. I’ve followed Luca’s instructions to produce some flame graphs. The PNGs are shown below. You may open the SVG variant via the link in a new browser tab.

The average runtime of “t4 – memoptimized” was 77.33 seconds and the average runtime of “t1 – heap-organized” was 48.21 seconds. That’s a difference of about 30 seconds. How can we find the functions in “t4 – memoptimized” which are contributing the most to this difference?

First, we assume the amount of sampled data is good enough to represent the load pattern. Second, we assume that we can calculate the runtime of a function based on the percentage shown in the flame graph. This allows the findings to be presented as follows:

Functiont1 Percentt4 Percentt1 Timet4 TimeDifference
opitsk99.53%99.37%47.9876.84-28.86

Third, we are looking for functions on a reasonable level in the call stack. In this case, it is not helpful to state that the opitsk function is slower in “t4 – memoptimized”. Reasonable means, that the identified function are sampled in different call stacks. This is avoiding double counting.

The following table lists 6 functions that account for more than two thirds of the total runtime of “t4 -memoptimized”. In total, they consume 36.66 seconds more than in “t1 – heap-organized”. In the tab pane above, there are flame graph variants named “marked”, highlighting these functions.

Functiont1 Percentt4 Percentt1 Timet4 TimeDifferenceNotes
opikndf2 (from optisk)17.64%30.22%8.5023.37-14.86
kksumc (from SELECT FETCH:)6.10%19.18%2.9414.83-11.89
kpoxihFetch0.00%4.86%0.003.76-3.76Fetch from memoptimize buffer area, n/a in t1
ksupop6.24%7.40%3.015.72-2.71
ksupucg3.00%4.24%1.453.28-1.83
kpoxihLookup0.00%2.07%0.001.60-1.60Hash index lookup, n/a in t1
Total32.98%67.97%15.9052.56-36.66

Someone with access to the source code of these functions could dig deeper, but I can’t. So I have to stop here. I don’t know how much my unsupported environment contributed to this bad performance. I just can hope it is a lot.

Updated on 2018-06-19: After some research I found the root cause. OCI is a prerequisite for getting good performance out of the MemOptimized RowStore. The succeeding blog post gives you more details about that.

Conclusion

If you access Oracle Databases with #NoPlsql applications, the MemOptimized RowStore might be an interesting feature to improve the performance when querying single rows from single tables via the primary key. If you access Oracle Databases with #SmartDB applications, you cannot use this feature, since it does not work when the SQL query is called from PL/SQL. That’s sad. However, it is disturbing that activating SQL trace or setting STATISTICS_LEVEL = 'ALL' deactivates the MemOptimized RowStore. I hope this will be fixed in a future release.

Updated on 2018-06-19, added OCI as a prerequisite and link to succeeding blog post.

5 Comments

  1. […] June, 10 2018 I blogged about the MemOptimized RowStore in Oracle 18c. If you haven’t read this post, it is a good idea to catch up now. I showed that accessing a […]

  2. […] February, 13 2019 Oracle Database 19c is available. I blogged about this feature here and here. Time for an update. So, what’s new in 19c regarding the MemOptimized […]

  3. blank Luis Santos says:

    Thanks a lot! It’s the best article I found up to now about MemOptimized RowStore.

  4. blank Luis Santos says:

    I’m trying to reproduce your tests. But I’m failing with the umcompreensible (at least to me) error:

    SQL> r
    1 CREATE TABLE t4 (
    2 key INTEGER NOT NULL,
    3 value VARCHAR2(30 CHAR) NOT NULL,
    4 CONSTRAINT t4_pk PRIMARY KEY (key)
    5 )
    6 SEGMENT CREATION IMMEDIATE
    7 MEMOPTIMIZE FOR READ
    8* tablespace USERS
    CREATE TABLE t4 (
    *
    ERROR at line 1:
    ORA-62141: MEMOPTIMIZE FOR READ feature cannot be used with COMPRESS option

    But USERS tablespace are not with COMPRESS clause, nor I’m using COMPRESS clause on create table.

    SQL> @printtbl8 "select * from dba_tablespaces where tablespace_name=''USERS''"
    TABLESPACE_NAME : USERS
    BLOCK_SIZE : 8192
    INITIAL_EXTENT : 65536
    NEXT_EXTENT :
    MIN_EXTENTS : 1
    MAX_EXTENTS : 2147483645
    MAX_SIZE : 2147483645
    PCT_INCREASE :
    MIN_EXTLEN : 65536
    STATUS : ONLINE
    CONTENTS : PERMANENT
    LOGGING : LOGGING
    FORCE_LOGGING : NO
    EXTENT_MANAGEMENT : LOCAL
    ALLOCATION_TYPE : SYSTEM
    PLUGGED_IN : NO
    SEGMENT_SPACE_MANAGEMENT : AUTO
    DEF_TAB_COMPRESSION : DISABLED
    RETENTION : NOT APPLY
    BIGFILE : NO
    PREDICATE_EVALUATION : HOST
    ENCRYPTED : NO
    COMPRESS_FOR :
    DEF_INMEMORY : DISABLED
    DEF_INMEMORY_PRIORITY :
    DEF_INMEMORY_DISTRIBUTE :
    DEF_INMEMORY_COMPRESSION :
    DEF_INMEMORY_DUPLICATE :
    SHARED : SHARED
    DEF_INDEX_COMPRESSION : DISABLED
    INDEX_COMPRESS_FOR :
    DEF_CELLMEMORY :
    DEF_INMEMORY_SERVICE :
    DEF_INMEMORY_SERVICE_NAME :
    LOST_WRITE_PROTECT : OFF
    CHUNK_TABLESPACE : N
    
    
    • Have you tried to use the NOCOMPRESS option explicitly?
      What is the result of select * from dba_ilmpolicies;?
      What is the result of select banner_full from v$version;?
      What is the result of select value from v$parameter where name = '_exadata_feature_on';?
      If you running this on a EE-ES, DBCS EE-EP or ExaCS system, then I suggest to contact Oracle support.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.