Oracle 12c: Quando apenas o In-Memory não é o bastante

Essa é uma série de posts sobre a nova feature “Oracle In-Memory Column Store” presente na versão do Oracle 12c. O Oracle há muitos anos, tem fornecido a leitura de blocos diretamente da memória RAM através do buffer cache (v$bh). Ao longo desses anos a Oracle tem feito um grande trabalho, aperfeiçoando cada vez mais o algoritmo LRU afim de otimizar as buscas por blocos quentes e mantê-los cada vez por mais tempo em memória para um acesso muito mais rápido, servindo como um verdadeiro “cache” para o banco de dados.

Com a vinda do IMDB, a arquitetura e a performance muda completamente comparado a leitura no buffer cache da memória do database, a começar pela maneira como o Oracle armazena os dados em memória. Assim como o nome da feature sugere, o Oracle In-Memory Column Store, passa agora a armazenar os dados de forma colunar, totalmente otimizado para a leitura via SDRAM, trabalhando como um verdadeiro DBMS orientado a coluna: Column-oriented DBMS

Não é preciso dizer que a RAM é muito mais rápida que um acesso a disco (isso todo mundo já sabe), assim não é nem preciso comentar ou mostrar aqui, que com a utilização do IMDB o ganho de performance é absurdo. Mas … será que o IMDB vem para resolver todo e qualquer tipo de problema na performance do ambiente?! O que pretendo apresentar nessa série de posts é justamente casos como esses, evidências e provas na utilização da nova feature, que assim como qualquer outra, apresentar suas vantagens e desvantagens e melhores formas de se utilizar.

É incrível a quantidade de DBAs e empresas que acreditam só em   mágica, que com apenas um “alter table” todos os problemas de performance irão desaparecer. IMDB não é sinônimo de “desligar o cérebro do DBA”.

A partir de agora, vou apresentar um comportamento bastante incomum em alguns ambientes com o Oracle In memory. Através do utilitário Swingbench criei a tabela SOE.ORDER_ITEMS de 12G. Essa será a nossa tabela teste para o uso do IMDB. Aqui mais detalhes da tabela:

sys@ora01  SQL> @sizesoe.order_items

OWNER                     SEGMENT_NAME                   SEGMENT_TYPE             SIZE_MB  
------------------------- ------------------------------ ------------------ ------------- 
SOE                       ORDER_ITEMS                    TABLE                   12,398.0
-------------
sum                                                                              12,398.0

1 rowselected.

sys@ora01 SQL> -- Atualizando as estatísticas da tabela: 
sys@ora01 SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS('SOE', 'ORDER_ITEMS', 
               method_opt  => 'FOR TABLE FOR ALL COLUMNS SIZE AUTO', cascade =>true);

PL/SQL procedure successfullycompleted.

sys@ora01  SQL> @tbstatsoe.order_items

=============================================
===        O B J E C T   T A B L E        ===
=============================================

OWNER: SOE                              TABLE: ORDER_ITEMS

Name                             Null?    Type
-------------------------------  -------- ----------------------------
1      ORDER_ID                  NOT NULL NUMBER(12)
2      LINE_ITEM_ID              NOT NULL NUMBER(3)
3      PRODUCT_ID                NOT NULL NUMBER(6)
4      UNIT_PRICE                         NUMBER(8,2)
5      QUANTITY                           NUMBER(8)
6      DISPATCH_DATE                      DATE
7      RETURN_DATE                        DATE
8      GIFT_WRAP                          VARCHAR2(20)
9      CONDITION                          VARCHAR2(20)
10     SUPPLIER_ID                        NUMBER(6)
11     ESTIMATED_DELIVERY                 DATE

================================================================================
TableStatistics
================================================================================
TABLE_NAME               : ORDER_ITEMS
STATUS                   : VALID
LAST_ANALYZED            : 29/03/2016 22:08:46
PARTITIONED              : NO
TEMPORARY                : N
DEGREE                   : 1
BUFFER_POOL              : DEFAULT
CACHE                    : NO
CELL_FLASH_CACHE         : DEFAULT
COMPRESSION              : DISABLED
RESULT_CACHE             : DEFAULT
ROW_MOVEMENT             : DISABLED
LOGGING                  : YES
SAMPLE_SIZE              : 85789721
==================================================================================
Column Statistics
================================================================================================================================
ID#   NAME                         ANALYZED           NDV           DENSITY      # NULLS   # BUCKETS        SAMPLE     HISTOGRAM
================================================================================================================================
1     ORDER_ID               29-03-16 22:08      28434432        0.00000004            0           1      85789721          NONE
2     LINE_ITEM_ID           29-03-16 22:08             5        0.20000000            0           1      85789721          NONE
3     PRODUCT_ID             29-03-16 22:08           999        0.00100100            0           1      85789721          NONE
4     UNIT_PRICE             29-03-16 22:08          2000        0.00050000            0           1      85789721          NONE
5     QUANTITY               29-03-16 22:08            10        0.10000000            0           1      85789721          NONE
6     DISPATCH_DATE          29-03-16 22:08           124        0.00806452            0           1      85789721          NONE
7     RETURN_DATE            29-03-16 22:08          4507        0.00022188     84059125           1       1730596          NONE
8     GIFT_WRAP              29-03-16 22:08             6        0.16666667            0           1      85789721          NONE
9     CONDITION              29-03-16 22:08             3        0.33333333            0           1      85789721          NONE
10    SUPPLIER_ID            29-03-16 22:08           999        0.00100100            0           1      85789721          NONE
11    ESTIMATED_DELIVERY     29-03-16 22:08          4507        0.00022188            0           1      85789721          NONE
================================================================================================================================
IndexInformation
=====================================================================================================================================
INDEX_NAME                 COLUMNS                      ANALYZED           DIST_KEYS    DENSITY    NUM_ROWS  UNIQUE  VISIBLE   STATUS
=====================================================================================================================================
ITEM_ORDER_IX              ORDER_ID                     29-03-16 22:08      28434432  0.0000000    82834901      NO      YES       OK
ITEM_PRODUCT_IX            PRODUCT_ID                   29-03-16 22:08          999   0.0010010    79984685      NO       NO       OK
ORDER_ITEMS_PK             LINE_ITEM_ID ,ORDER_ID       29-03-16 22:08     85615194   0.0000000    85615194     YES      YES       OK

sys@ora01 SQL>

Veja que nesse momento, a tabela SOE.ORDER_ITEMS está com o InMemory desabilitado:

sys@ora01  SQL>selectowner, table_name, inmemoryfromdba_tableswhereowner='SOE' andtable_name='ORDER_ITEMS';

OWNER                     TABLE_NAME                     INMEMORY
------------------------  ------------------------------ --------
SOE                       ORDER_ITEMS                    DISABLED

1 rowselected.

Vamos executar uma consulta de encontro a tabela e analisar os resultados. Veja que estou forçando a consulta a ser executada em serial e obtendo o máximo de estatísticas para análise, isso através dos hints NO_PARALLEL e GATHER_PLAN_STATISTICS respectivamente.

sys@ora01  SQL> SET TIMING ON
sys@ora01  SQL> SELECT /*+ monitor gather_plan_statisticsno_parallel */ SUM(quantity)  
                FROM soe.order_items WHERE product_id=:p;

SUM(QUANTITY)
-------------
1376607

Elapsed: 00:00:43.51

1 rowselected.

sys@ora01 SQL> -- Buscando o sqlid da query acima 
sys@ora01  SQL> @lastsql

HASH_VALUE SQL_ID          CH#    PLAN_HASH  HASH_HEX   SQL_TEXT
---------- ------------- -----  ------------ ---------- -------------------------------------------------------
3284254310 97h2yq31w3gm6     0    2407813087  c3c1be66  SELECT /*+ MONITOR GATHER_PLAN_STATISTICS  NO_PARALLEL 
                                                        */ sum(QUANTITY) fromsoe.order_itemswhere PRODUCT_ID=:p

Vamos analisar agora, a consulta 97h2yq31w3gm6:

sys@ora01  SQL> SET HEADING OFF LINES 32767
sys@ora01  SQL> SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => '97h2yq31w3gm6',  
                report_level=>'ALL', type => 'TEXT') as report FROM dual;

SQL MonitoringReport

SQL Text
------------------------------
SELECT /*+  monitor gather_plan_statisticsno_parallel */ SUM(quantity) FROM soe.order_items  WHERE product_id=:p

Global Information
------------------------------
Status              :   DONE (ALL ROWS)
SQL ID              :   97h2yq31w3gm6
SQL Execution ID    :   16777216
Duration  :  44s
Module/Action   :  SQL*Plus/-
FetchCalls  :  1

Binds
=====================================
| Name |  Position |  Type  | Value |
=====================================
| :P    |        1 | NUMBER | 509   |
=====================================

Global Stats
================================================================
| Elapsed |   Cpu    |    IO    | Fetch | Buffer | Read | Read  |
| Time(s) |  Time(s) | Waits(s) | Calls |  Gets  | Reqs | Bytes |
================================================================
|      44 |    4.92  |       39 |     1 |    251K | 250K |   2GB |
================================================================

SQL Plan MonitoringDetails (Plan Hash Value=2407813087)
===========================================================================================================================================================================
| Id |               Operation               |      Name       |  Rows   | Cost |   Time   | Start | Execs |   Rows  | Read | Read  | Activity |      ActivityDetail      |
|    |                                       |                 | (Estim) |      | Active(s)| Active|       | (Actual)| Reqs | Bytes |   (%)    |        (# samples)       |
===========================================================================================================================================================================
|  0 | SELECT STATEMENT                      |                 |         |      |       39 |    +2 |     1 |       1 |      |       |          |                          |
|  1 |   SORT AGGREGATE                      |                 |       1 |      |       39 |    +2 |     1 |       1 |      |       |          |                          |
|  2 |   TABLE ACCESS BY INDEX ROWID BATCHED | ORDER_ITEMS     |   85876 |  697 |       40 |    +1 |     1 |    306K | 249K |   2GB |   100.00 | db file parallelread (40)|
|  3 |     INDEX RANGE SCAN                  | ITEM_PRODUCT_IX |   85876 |    2 |       39 |    +2 |     1 |    306K |  643 |   5MB |          |                          |
===========================================================================================================================================================================

1 rowselected.

sys@ora01  SQL> SET HEADING ON

Temos aqui o plano da nossa consulta de encontra a tabela ORDER_ITEMS. Veja que a consulta levou cerca de 44s para ser processada, sendo que, 39s foram gastos em I/O e 5s em CPU. A leitura total em disco foi cerca de 2G.

A partir de agora, iniciaremos o nosso teste com o Oracle In-memory 12c. O objetivo aqui, é colocar a nossa tabela ORDER_ITEMS em modo In-MemoryColumnStore e realizar novamente a mesma consulta e obter algumas conclusões. O primeiro passo, é popular a coluna na memória:

sys@ora01 SQL> ALTER TABLE soe.order_items INMEMORY;

Tablealtered.

sys@ora01 SQL> ALTER SESSION SET "_inmemory_populate_wait"=TRUE;

Sessionaltered.

sys@ora01 SQL> EXEC DBMS_INMEMORY.POPULATE('SOE','ORDER_ITEMS');

PL/SQL procedure successfullycompleted.

sys@ora01  SQL> COLUMN seg_inmem_size FORMAT 99,999,990.9 HEADING 'In-Memory|SIZE_MB'
sys@ora01  SQL> COLUMN seg_orig_size_megs FORMAT 99,999,990.9 HEADING  'ORIGINAL|SIZE_MB'
sys@ora01  SQL> COLUMN seg_megs_not_populated FORMAT 99,999,990.9 HEADING 'NOT  POPULATE|SIZE_MB'
sys@ora01  SQL> COLUMN populate_status HEADING 'POPULATE|STATUS'
sys@ora01  SQL> SELECT
2          inst_id
3          ,v.owner
4          ,v.segment_name
5          ,v.populate_status
6          ,v.inmemory_size/1024/1024  seg_inmem_size
7          ,v.bytes/(1024*1024)  seg_orig_size_megs
8          ,v.bytes_not_populated/(1024*1024)  seg_megs_not_populated
9          ,v.inmemory_priority
10         ,v.inmemory_compression
11  FROM v$im_segments v 
12  WHERE UPPER(segment_name) LIKE UPPER(CASE  WHEN INSTR('&1','.') > 0 
    THEN substr('&1',instr('&1','.')+1) ELSE '&1' END)
13 AND UPPER(owner) LIKE UPPER(CASE WHEN  INSTR('&1','.') > 0 THEN SUBSTR('&1',1,instr('&1','.')-1) ELSE user END)
14 / 

                                   POPULATE      In-Memory      ORIGINAL   NOT POPULATE   In-Memory    In-Memory
OWNER            SEGMENT_NAME      STATUS          SIZE_MB       SIZE_MB        SIZE_MB   PRIORITY   COMPRESSION
---------------- ----------------- --------- ------------- -------------  -------------   -------- -------------
SOE              ORDER_ITEMS       COMPLETED 3,554.1       12,398.0       0.0             NONE     FOR QUERY LOW

1 rowselected.

Como mostra a viewv$im_segments, veja que o status da nossa tabela está totalmente populada na área do InMemory, com um tamanho aproximadamente de 3500M.

Com a tabela na memória, iremos executar a mesma consulta e verificar os resultados, mas antes, vamos executar um flush no buffer cache/shared pool, para que os testes anteriores não sejam levados em consideração.

sys@ora01  SQL> ALTER SYSTEM FLUSH SHARED_POOL;

System altered.

sys@ora01  SQL> ALTER SYSTEM FLUSH BUFFER_CACHE;

System altered.

sys@ora01  SQL> SELECT /*+ monitor gather_plan_statisticsno_parallel */ SUM(quantity)  
                FROM soe.order_items WHERE product_id=:p;

SUM(QUANTITY)
-------------
1376607

Elapsed:  00:00:43.03

1 rowselected.

sys@ora01  SQL> @lastsql

HASH_VALUE  SQL_ID          CH#    PLAN_HASH HASH_HEX   SQL_TEXT
---------- ------------- -----  ------------ ---------- ----------------------------------------------------------
3284254310 97h2yq31w3gm6     0    2407813087  c3c1be66  SELECT /*+ monitor  gather_plan_statisticsno_parallel 
                                                        */ SUM(quantity) FROM soe.order_items WHERE  product_id=:p

sys@ora01  SQL> SET HEADING OFF LINES 32767
sys@ora01  SQL> SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => '97h2yq31w3gm6',  
                report_level=>'ALL', type => 'TEXT') as report FROM dual;

 
SQL MonitoringReport

SQL Text
------------------------------
SELECT /*+  monitor gather_plan_statisticsno_parallel */ SUM(quantity) FROM soe.order_items  WHERE product_id=:p

Global Information
------------------------------
Status              :   DONE (ALL ROWS)
SQL ID               :  97h2yq31w3gm6
SQL Execution ID    :   16777216
Duration  :  44s
Module/Action   :  SQL*Plus/-
FetchCalls  :  1

Binds
=====================================
| Name |  Position |  Type  | Value |
=====================================
| :P    |        1 | NUMBER | 509   |
=====================================

Global Stats
================================================================
| Elapsed |   Cpu    |    IO    | Fetch | Buffer | Read | Read  |
| Time(s) |  Time(s) | Waits(s) | Calls |  Gets  | Reqs | Bytes |
================================================================
|      44 |    4.92  |       39 |     1 |    251K | 250K |  2GB |
================================================================

SQL Plan MonitoringDetails (Plan Hash Value=2407813087)
===============================================================================================================================================================================
| Id |               Operation               |      Name       |  Rows  | Cost |   Time    | Start  | Execs |   Rows   | Read | Read  | Activity |      ActivityDetail       |
|    |                                       |                 | (Estim)|      | Active(s) | Active |       | (Actual) | Reqs | Bytes |   (%)    |        (# samples)        |
===============================================================================================================================================================================
|  0 | SELECT STATEMENT                      |                 |        |      |        39 |     +2 |     1 |        1 |      |       |          |                           |
|  1 |   SORT AGGREGATE                      |                 |      1 |      |        39 |     +2 |     1 |        1 |      |       |          |                           |
|  2 |   TABLE ACCESS BY INDEX ROWID BATCHED | ORDER_ITEMS     |  85876 |  697 |        40 |     +1 |     1 |     306K | 249K |   2GB |   100.00 | db file parallelread (40) |
|  3 |     INDEX RANGE SCAN                  | ITEM_PRODUCT_IX |  85876 |    2 |        39 |     +2 |     1 |     306K |  643 |   5MB |          |                           |
===============================================================================================================================================================================
1 rowselected.

sys@ora01  SQL> SET HEADING ON

Nada mudou em relação a primeira execução. Veja que interessante, mesmo com a minha tabela totalmente carregada na memória via IMDB a quantidade de leitura física da primeira execução com a segunda foi exatamente a mesma. O tempo total da consulta também não mudou, levamos cerca de ~ 44s para retornar os dados.

Se a ORDER_ITEMS está agora totalmente disponível via InMemoryColumnStore, porque o Oracle está fazendo a mesma quantidade de leitura em disco? Ele não deveria buscar os dados em memória e evitar essa grande quantidade de leitura em disco?

Como regra geral (assim como acontece no SmartScan do Exadata), para que o otimizador do Oracle decida ir vi InMemory, um acesso “FULL SCAN” deve ser feito na tabela. Observe no plano de execução da consulta acima, que o caminho escolhido pelo Oracle é através do INDEX ITEM_PRODUCT_IX (via INDEX RANGE SCAN).

===========================================================================================================================================================================
| Id |               Operation               |      Name       |  Rows   | Cost |   Time   | Start  | Execs |   Rows  | Read | Read  | Activity |      ActivityDetail     |
|    |                                       |                 | (Estim) |      | Active(s)| Active |       | (Actual)| Reqs | Bytes |   (%)    |        (# samples)      |
===========================================================================================================================================================================
|  2 |   TABLE ACCESS BY INDEX ROWID BATCHED | ORDER_ITEMS     |   85876 |  697 |       40 |     +1 |     1 |    306K | 249K |   2GB |  100.00  | db file parallelread (40)
|  3 |   INDEX RANGE SCAN                    | ITEM_PRODUCT_IX |   85876 |    2 |       39 |     +2 |     1 |    306K |  643

O Whitepaper do Oracle DatabaseIn-Memory 12c, é bem claro ao dizer que a busca dos dados via In-memory será feito apenas quando o acesso na tabela for via FULL SCAN. Assim jamais a nossa consulta acima irá consulta os dados presentes na nossa RAM, já que o otimizador está escolhendo o caminho através do index ITEM_PRODUCT_IX.

Podemos resolver essa situação adicionando o hint FULL na consulta, ou deixando o Index ITEM_PRODUCT_IX invisible. Veja só o resultado:

sys@ora01  SQL>alter index soe.ITEM_PRODUCT_IXinvisible;

Index altered.

sys@ora01  SQL> SELECT /*+ MONITOR GATHER_PLAN_STATISTICS NO_PARALLEL */ sum(QUANTITY)
           from soe.order_itemswhere PRODUCT_ID=:p;

SUM(QUANTITY)
-------------
1376607

Elapsed: 00:00:00.05

1 rowselected.

sys@ora01  SQL> SET HEADING OFF LINES 32767
sys@ora01  SQL> SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => '97h2yq31w3gm6',  
                report_level=>'ALL', type => 'TEXT') as report FROM dual;

SQL MonitoringReport

SQL Text
------------------------------
SELECT /*+  MONITOR GATHER_PLAN_STATISTICS NO_PARALLEL */ sum(QUANTITY)  
       from soe.order_itemswhere PRODUCT_ID=:p

Global Information
------------------------------
Status              :   DONE (ALL ROWS)
Instance  ID         :  1
Session  :  SYS  (1150:44245)
SQL ID              :   97h2yq31w3gm6
SQL Execution ID    :   16777217
Duration  :   .041563s
Module/Action   :  SQL*Plus/-
Service             :   SYS$USERS
Program  :sqlplus@Flavios-MacBook-Pro.local (TNS  V1-V3)
FetchCalls  :  1

Binds
======================================
| Name |  Position |  Type  | Value  |
======================================
| :P    |        1 | NUMBER | 509    |
======================================

Global Stats
===========================================================================
| Elapsed |   Cpu    |    IO    |   Other   | Fetch | Buffer | Read | Read |
| Time(s) |  Time(s) | Waits(s) | Waits(s) | Calls |   Gets  | Reqs | Bytes|
===========================================================================
|    0.04 |    0.04 |     0.00 |     0.00 |      1 |    249 |   18 | 144KB |
===========================================================================

SQL Plan MonitoringDetails (Plan Hash Value=3419397814)
======================================================================================================================================================
| Id |           Operation           |    Name     |  Rows   | Cost |   Time   | Start  | Execs |   Rows  | Read | Read  | Activity | ActivityDetail |
|    |                               |             | (Estim) |      | Active(s)| Active |       | (Actual)| Reqs | Bytes |   (%)    |   (# samples)  |
======================================================================================================================================================
|  0 | SELECT STATEMENT              |             |         |      |        1 |     +0 |     1 |       1 |      |       |          |                |
|  1 |   SORT AGGREGATE              |             |       1 |      |        1 |     +0 |     1 |       1 |      |       |          |                |
|  2 |    TABLE ACCESS INMEMORY FULL | ORDER_ITEMS |   85876 | 7880 |        1 |     +0 |     1 |    306K |   12 | 98304 |          |                |
======================================================================================================================================================

1 rowselected.

sys@ora01 SQL>

Aqui podemos notar o poder do INMEMORY, observe o tempo, caiu de 44s para apenas 0.4s. Veja também que agora a operação de consulta não foi mais via INDEX RANGE SCAN e sim via TABLE ACCESS INMEMORY FULL, o que mostra que a busca dos dados foi exclusivamente na memória para a tabela ORDER_ITEMS. Outro ponto importante é a quantidade de leitura física que de 2G caiu para 144KB. O tempo 0.4s foi exclusivamente o tempo da CPU em processar os dados  (CPU Time(s)). Chegamos aqui, com algumas conclusões:

  • Vou ter performance colocando todas as minhas tabelas em IMDB? É necessário testar, não tem outro caminho. Cada ambiente é única e existe muita variável no meio do caminho. Como os testes acima, mesmo ativando o IMDB, não vimos nenhuma melhora até analisar e corrigir.
  • Porque o Oracle decide um acesso via INDEX, sendo que meus dados estão na memória? Bom, isso vou explicar melhor em um próximo post, mas basicamente o Oracle considera o custo do INDEX menor, por isso acaba optando pela utilização do INDEX. Alguns parâmetros no banco também afetam essa decisão, vamos conferir isso melhor nos próximos posts.
  • Se o IMDB só funciona via FULL SCAN, devo deixar todos meus INDEXES invisíveis? NUNCA!!! Isso é um mito que surgiu com o Exadata. É importante lembra que tanto no IMDB como no Exadata, o INDEX não é um vilão. Um acesso via INDEX pode muitas vezes ser muito mais benéfico, já que o objetivo do INDEX é justamente economizar leitura em disco. A TI é um mundo lógico e deve ser sempre levado, como os gringos gostam de falar, com systematic approach. Não existe mágica. Nos próximos posts, vou mostrar melhor esse tema.
  • Vou precisar mudar minha aplicação para utilizar o IMDB? Como está na documentação Oracle, você não precisa mover uma vírgula da aplicação para utilizar o IMDB. Agora fazemos outra pergunta: Vou ter performance em todas as tabelas que coloquei no INMEMORY? Mais uma vez …. Depende! É necessário testar.
  • O IMDB realmente é performático? Sim, extremamente performático comparado com tudo que temos no mercado! Como disse no começo do post, não é necessário explicar a imensa diferença de performance da RAM contra o disco, todos já sabem. É necessário no entanto saber aproveitar e saber utilizar a feature da melhor forma. Como vimos nesse simples exemplo, uma consulta teste, caiu de 44s para 0.4s, e o mais importante, sem mudar uma vírgula na consulta.
  • Minha aplicação OLTP cheia de INDEX, vou ter algum ganho com o IMDB? Provavelmente. Mas uma vez: É necessário testar! Deve-se sempre avaliar, estatísticas, index, hints, etc …

Aguarde os próximos posts, tem muita coisa interessante sobre essa nova incrível feature do Oracle.

Leave a Comment