Rechercher sur arkzoyd.com

26 décembre 2009

OracleVM 2.2, mon Dell et moi

Il y a quelques mois, j'installais OracleVM sur mon laptop : un Dell E6500. Depuis, avec Easyteam, nos projets se multiplient et nous avons construit deux offres packagées qui, en 10 et 14 jours, permettent aux équipes de DBA ou d'administrateurs Système d'être opérationnelles et de déployer de nouveaux projets sur la plateforme de virtualisation d'Oracle.

Il faut dire qu'avec les architectures x86_64 6 cores et un mode de licences qui interdit l'utilisation de VMWare mais nécessite souvent de limiter le nombre de cores utilisés pour permettre aux entreprises de maîtriser leurs coûts, Oracle laisse peu de choix à ses utilisateurs. En outre, si Oracle VM n'est pas VMWare, la plateforme est, non seulement, vite rentabilisée mais peut également apporter une réelle valeur ajoutée à vos projets:
  • Installation automatique ou à partir de templates
  • Bascule Automatique
  • Qualité de Service
  • Clone
  • ...

Oracle VM 2.2

La dernière version d'Oracle VM est la version 2.2. Elle s'appuie sur Xen 3.4. Cette version offre un vaste ensemble de nouveautés et c'est désormais la version de référence pour les implémentations. Il faudra sans doute attendre les versions Oracle VM 3.0, Xen 4.0 et l'introduction de l'ensemble des fonctionnalités de Virtual Iron d'ici quelques mois pour changer cela.

Installer OracleVM 2.2 sur le Dell E6500

J'ai donc installé Oracle VM 2.2 sur mon laptop Dell E6500. La configuration reste essentiellement la même qu'avec la version 2.1. En ce qui me concerne, le véritable changement tient dans le support des drivers Intel® Rapid Recover Technology, ce qui évite de jongler dans le BIOS entre Ubuntu, Windows et le Domaine-0 d'OracleVM (Oracle EL 5.3) de mon laptop.

Configuration du Wifi

Pour utiliser OracleVM au quotidien sur un portable, configurer le Wifi est essentiel. Dans le cas du Dell E6500, il faut installer le firmware WiFi sur le Domaine 0; il s'agit du controleur Intel WiFi 5100 :
# lspci |grep -i wireless
0c:00.0 Network controller: Intel Corporation Wireless WiFi Link 5100
Et le domaine 0 fonctionne normalement avec un noyau basé sur Oracle EL 5.3 32 bits compilé pour Xen. Vous pouvez donc télécharger les drivers depuis un référentiel Redhat ou CentOS comme celui-ci. Pour installer le driver, utilisez la commande rpm comme ci-dessous:
rpm -ivh iwl5000-firmware-5.4.A.11-2.el5.rf.noarch.rpm
. Une fois le package installé, désactivez les services de réseaux "server" pour NetworkManager:
chkconfig network off
chkconfig wpa_supplicant off
chkconfig NetworkManager on
Vous pouvez bien sur arrêter les services et les redémarrer. Le moyen le plus simple pour tester votre configuration est de redémarrer votre laptop et de vérifier que le NetworkManager s'affiche correctement dans votre barre de menu:

Sélectionnez et configurez ensuite le réseau WiFi de votre choix!
Références:

22 décembre 2009

Oracle Database 11g Release 2 sur Aix/Power et HP-UX/Itanium

Oracle 11g Release 2 est disponible pour HP-UX Itanium et Aix Power. Vous pouvez télécharger les distributions sur Oracle Technology Network.

21 décembre 2009

Exemple d'utilisation d'Oracle GoldenGate

GoldenGate est le nouvel outil Oracle pour apporter l'intégration des données temps réelle aux masses. Et de fait, le sujet est "red hot" ! Loin de moi l'ambition de couvrir un sujet aussi intéressant que les data warehouses temps réel, les mises à jour à chaud ou la continuité, mais de fait, le champ des applications est très large et l'offre, désormais partie Intégrante de Fusion Middleware Integration, complète parfaitement ODI.

Parmi les avantages d'Oracle GoldenGate, on peut citer:
  • Sa capacité à exploiter des données en provenance et à destination de tous les SGBDR du marché de MySQL à Teradata en passant par SQL*Server, DB2 Mainframe ou Oracle SE1
  • Une grande simplicité de mise en oeuvre comme vous allez pouvoir le découvrir
  • Un vaste ensemble d'outils pour gérer la supervision ou le rapprochement des données

Cet article est une très succincte introduction technique à GoldenGate, il n'a pas d'autre ambition que de vous aider à construire votre GoldenGate "Hello World".

Architecture

Vous l'aurez compris GoldenGate est une architecture distribuée avec des composants sur des serveurs de types différents et interagissant avec des bases de données différentes. Dans notre exemple, nous allons utiliser une seule instance GoldenGate pour répliquer dans un sens uniquement les données de la table SCOTT.DEPT dans la table DEMO.DEPT; la solution finale que nous obtiendrons ressemblera à celle ci-dessous:
Les composants en jeu sont les suivants:
  • L'extract collecte les changements dans les redologs et archivelogs de la base de données Oracle et les envoie au Manager. A noter que ce process doit se connecter à la base de données pour non seulement collecter l'emplacement des logs mais également pour connaitre les identifiants des objets dans les logs et pour extraire certaines données (types LOB, UDT...) de la base de données.
  • Le manager stocke les vecteurs de changement dans les fichiers TRAIL. Vous pouvez visialiser ces fichiers avec l'utilitaire GoldenGate logdump
  • Le replicat récupère les changements dans les fichiers TRAIL et les applique dans la base de données

A fond sur Koala

Puisque mon portable fonctionne sur Karmic Koala, j'ai décidé de tester GoldenGate avec Oracle 11g Release 2 (aka 11gR2) sur Ubuntu 9.10. Bien sur, ce n'est pas supporté mais, il ne s'agit que de tests et je n'ai pas rencontré de problèmes jusqu'à présent; ne faites pas ça en production ;-). J'ai téléchargé et installé GoldenGate depuis Oracle E-Delivery puisque la version Linux x86 n'est pas disponible sur Oracle Technology Network. J'ai choisi la distribution ci-dessous :
  • Select a product Pack: "Oracle Fusion Middleware"
  • Platform: "Linux x86"
  • Description: "Oracle GoldenGate on Oracle Media Pack for Linux x86"
  • Name: "Oracle GoldenGate V10.4.0.x for Oracle 11g on RedHat 5.0"
Pour effectuer l'installation, il suffit de dézipper et dé-tar-er le fichier dans le répertoire de votre choix, positionner l'environnement pour accéder à la base de données Oracle et c'est tout; n'importe qui peut installer Oracle sur Ubuntu de toute façon :
. oraenv
ORACLE_SID = [WHITE] ? BLACK
The Oracle base for ORACLE_HOME=/u01/app/oracle/product/11.2.0/db_1 is /u01/app/oracle
Et voilà, c'est fini; vous pouvez lancer l'interface ligne de commande de GoldenGate:
./ggsci 

Oracle GoldenGate Command Interpreter for Oracle
Version 10.4.0.19 Build 002
Linux, x86, 32bit (optimized), Oracle 11 on Sep 29 2009 08:50:50

Copyright (C) 1995, 2009, Oracle and/or its affiliates. All rights reserved.


exit

Préparer la base de données

Il y a plusieurs pré-requis à vérifier sur la base de données utilisée pour effectuer la capture. Elle doit être en archivelog et GoldenGate doit pouvoir accéder aux journaux :
sqlplus / as sysdba

archive log list
Database log mode Archive Mode
Automatic archival Enabled
Archive destination /u01/app/oracle/oradata/BLACK/archivelogs
Oldest online log sequence 1
Next log sequence to archive 3
Current log sequence 3

Il faut positionner NLS_LANG pour qu'il corresponde au paramétrage de la la base de données :
sqlplus / as sysdba

select parameter, value
from nls_database_parameters
where parameter in ('NLS_LANGUAGE',
'NLS_TERRITORY',
'NLS_CHARACTERSET',
'NLS_LENGTH_SEMANTICS');

parameter VALUE
-------------------- -------------
NLS_LANGUAGE AMERICAN
NLS_TERRITORY AMERICA
NLS_CHARACTERSET WE8MSWIN1252
NLS_LENGTH_SEMANTICS BYTE


exit

export NLS_LANG=AMERICAN_AMERICA.WE8MSWIN1252
La configuration s'appuie sur un utilisateur avec un niveau de privilège élevé. Il doit en effet pouvoir récupérer les identifiants des objets dans les redologs ou effectuer des requêtes "flashback" pour récupérer les valeurs des UDT ou des LOB qui va chercher directement sur la base de données source. Il doit être capable d'activer les groupes de journalisations supplémentaires sur les tables :
sqlplus / as sysdba

create user gg
identified by gg
default tablespace users
temporary tablespace temp;

grant create session, resource, dba to gg;
La base de données doit également avoir le niveau minimum de "supplemental log" activé :
alter database add supplemental log data;

alter system switch logfile;
Dans mon cas, c'était tout! Évidemment, selon ce que vous cherchez à faire, il se peut que vous ayez besoin de plus de paramétrages sur la base de données source ou la base de données cible. Il se peut que vous ayez besoin de tables pour auditer les ordres DDL ou pour stocker les checkpoints sur la cible. Pour une liste complète de ce qu'il est nécessaire d'effectuer, référez-vous à la documentation GoldenGate et plus particulièrement à "Oracle Installation and Setup Guide".

Le schéma DEMO

Comme discuté, cet article ne vise pas à effectuer une quelconque configuration avancée. Je veux juste répliquer la table scott.dept dans le schéma demo. Pour rendre l'exemple encore plus simple, je vais considérer que personne n'accède la table pendant la configuration et je ne ferai pas attention aux transactions en cours sur la table ou si les objets ne sont pas synchronisés. Je vous propose de simplement créer la table demo.dept et de charger les données de scott.dept dedans :
create user demo identified by demo
default tablespace users
temporary tablespace temp
quota unlimited on users;

grant connect, resource to demo;

create table demo.dept
( deptno number(2,0),
dname varchar2(14),
loc varchar2(13),
constraint pk_dept primary key(deptno))
tablespace users;

insert into demo.dept
select * from scott.dept;

commit;

Configurer le Manager GoldenGate

Une fois la base de données configurée, reportez-vous au document "Administration Guide" pour continuer et connaitre les détails de la configuration de GoldenGate. D'abord, il faut créer tous les répertoires pour stocker les éléments de configuration, les journaux et les fichiers trails. Vous pouvez, là encore, choisir des destinations alternatives mais ce n'est pas vraiment notre préoccupation du moment:
./ggsci 

create subdirs

Creating subdirectories under current directory /gg

Parameter files /gg/dirprm: created
Report files /gg/dirrpt: created
Checkpoint files /gg/dirchk: created
Process status files /gg/dirpcs: created
SQL script files /gg/dirsql: created
Database definitions files /gg/dirdef: created
Extract data files /gg/dirdat: created
Temporary files /gg/dirtmp: created
Veridata files /gg/dirver: created
Veridata Lock files /gg/dirver/lock: created
Veridata Out-Of-Sync files /gg/dirver/oos: created
Veridata Out-Of-Sync XML files /gg/dirver/oosxml: created
Veridata Parameter files /gg/dirver/params: created
Veridata Report files /gg/dirver/report: created
Veridata Status files /gg/dirver/status: created
Veridata Trace files /gg/dirver/trace: created
Stdout files /gg/dirout: created

Une fois l'opération précédente effectuée, éditez le fichier de configuration du manager et positionnez le paramètre port. le fichier de configuration s'appelle MGR ; une fois l'opération effectuée, vous pouvez démarrer le manager :
edit params mgr

view params mgr
port 7809

start manager

status manager
Manager is running (IP port arkzoyd.7809).

Ajouter un group de journalisation supplémentaire à la table source

Comme pour Oracle Streams (pas de secret!), GoldenGate a besoin d'identifier la ligne sur laquelle les changements sont appliqués sans utiliser le ROWID. Il existe des outils génériques pour effectuer et valider ces opérations depuis ggsci:
dblogin userid gg, password gg
Successfully logged into database.

add trandata scott.dept
Logging of supplemental redo data enabled for table SCOTT.DEPT.

info trandata scott.dept
Logging of supplemental redo log data is enabled for table SCOTT.DEPT

Fichiers paramètres

J'ai appelé mon extract scott. Il capture les changements DML réalisés sur la table SCOTT.DEPT et les envoie dans le fichier TRAIL distant, qui dans mon cas particulier est géré sur la même instance que celle de l'extract. J'ai nommé mon réplicat demo. Les fichiers de paramètres des groupes scott et demo doivent ressembler à ceux ci-dessous :
help edit params

edit params scott

view params scott

extract scott
userid gg, password gg
rmthost localhost mgrport 7809
rmttrail SC
table SCOTT.DEPT;


edit params demo

view params demo

replicat demo
assumetargetdefs
userid gg, password gg
map SCOTT.DEPT, target DEMO.DEPT;
Note:
Avec la base de données Oracle, il faut utiliser des guillemets pour gérer des noms de table avec des minuscules. Toutefois, ce n'est pas le cas de tous les moteurs de bases de données et GoldenGate est donc très sensible à la casse de votre paramétrage. Pour éviter les problèmes j'utilise des valeurs de paramètres en majuscule à moins que je veuille spécifiquement que ceux-ci soient en minuscule.

Extract et Replicat

Une fois les fichiers de paramètres définis, ajoutez les extract, replicat et trail depuis ggsci:
add extract scott, tranlog, begin now
EXTRACT added.

add rmttrail SC, extract scott
RMTTRAIL added.

add replicat demo, exttrail SC, nodbcheckpoint, begin now
REPLICAT added.
Et démarrer la configuration:
start er *
Sending START request to MANAGER ...
EXTRACT SCOTT starting

Sending START request to MANAGER ...
REPLICAT DEMO starting


info all

Program Status Group Lag Time Since Chkpt
MANAGER RUNNING
EXTRACT RUNNING SCOTT 00:00:00 00:00:02
REPLICAT RUNNING DEMO 00:00:00 00:00:08


view ggsevt

exit

Mes changements sont-ils répliqués ?

Une fois la configuration réalisée, vous pouvez tester la réplication avec les ordres SQL ci-dessous :
sqlplus / as sysdba

update scott.dept
set dname='OPERATIONS2'
where deptno=40;

commit;

select dname from demo.dept
where deptno=40;

DNAME
--------------
OPERATIONS2


update scott.dept
set dname='OPERATIONS'
where deptno=40;

commit;

select dname from demo.dept
where deptno=40;

DNAME
--------------
OPERATIONS


exit

Supprimer votre configuration

Evidemment, il s'agit simplement d'un début et vous irez bientôt beaucoup plus loin. Pour éviter toute erreur dans vos prochains tests, supprimer la configuration une fois que vous en avez fait le tour :
./ggsci

stop er *
Sending STOP request to EXTRACT SCOTT ...
Request processed.

Sending STOP request to REPLICAT DEMO ...
Request processed.


delete er *
Are you sure you want to delete all groups? y
Deleted EXTRACT SCOTT.
Deleted REPLICAT DEMO.


stop manager
Manager process is required by other GGS processes.
Are you sure you want to stop it (y/n)? y

Sending STOP request to MANAGER ...
Request processed.
Manager stopped.


exit
Ensuite, je supprime les utilisateurs demo et gg :
sqlplus / as sysdba

drop user gg cascade;
drop user demo cascade;

exit

19 décembre 2009

Oracle Application Express 4.0 (aka APEX)

Une version Early Adopter d'Oracle Application Express 4.0 (aka APEX) est disponible sur Amazon Elastic Cloud Computing. Pour ouvrir un compte, cliquez sur l'URL http://tryapexnow.com/ et répondez au questionnaire.

Abonnez-vous aussi aux blogs de la communauté APEX (Allez les frenchies!); nul doute qu'on en saura bientôt plus sur les nouvelles fontionnalités comme les plugins ou autres régions imbriquées.

16 décembre 2009

Oracle GoldenGate est disponible sur E-Delivery

Une petite note pour pointer le fait qu'Oracle GoldenGate est disponible pour la plupart des plateformes et non pas simplement Linux x86_64 comme Oracle Technology Network peut le laisser penser.

Curiosité : c'est sur Oracle E-Delivery à "Fusion Middleware" -> "Oracle GoldenGate on Oracle Media Pack for XXX" que vous trouverez votre bonheur et pas sur la page de téléchargement du produit. C'est vrai que c'était écrit ! Merci à Alex F. pour l'info...

11 décembre 2009

Oracle 11gR2 et un cas non documenté d'"Adaptive Cursor Sharing"

Si vous avez lu la documentation de la base de données Oracle 11.1, vous connaissez la fonctionnalité dite "Intelligent" ou "Adaptive" Cursor Sharing. Selon cette documentation, Oracle peut désormais adapter son comportement et recalculer un plan d'exécution lorsqu'il le juge nécessaire. Cela permet d'éviter en partie les effets négatifs du bind peeking avec des plans de requêtes utilisant des variables bind. Vous trouverez, à ce sujet, un article très intéressant intitulé "Update on Adaptive Cursor Sharing" sur le blog consacré au CBO.

Quoiqu'il en soit et comme souvent avec la base de données, cette fonctionnalité est plus subtile qu'il n'y parait au premier abord. J'ai découvert un cas que vous devriez pourvoir reproduire où la requête est reparsée bien qu'elle soit strictement identique, dans le même environnement et n'utilise aucune variable bind.

Mon schéma de démonstration

Le fonctionnement de cette requête est peut-être lié aux caractériques de mon ordinateur portable; il n'est pas garanti que vous arriviez effectivement à le reproduire. J'utilise une base de données Oracle 11.2.0.1 Enterprise Edition sur Linux 32bits. Les paramètres à positionner en particulier sont les suivants:
  • sga_target=260M
  • pga_aggregate_target=180M
  • filesystemio_options=setall
J'utilise un schéma DEMO ainsi qu'un ensemble de tables, données, statistiques et indices. Vous pouvez télécharger le script pour créer tout l'environnement depuis l'URL suivante.

La requête

Exécutez la requête ci-dessous; la première fois, vous devriez découvrir que le plan a une valeur de hash qui est la 1851413986:
-- Pour changer le comportement de cet exemple, positionnez ce paramètre à "none":
-- alter session set "_optimizer_extended_cursor_sharing_rel"=none;
-- Sinon (pour reproduire mon cas d'utilisation), laissez le paramètre à "simple" :
-- alter session set "_optimizer_extended_cursor_sharing_rel"=simple;

set timing on
select /* GG */ t.year_id, sum(f.metric1)
from fact f, time t, dim2 d2, dim3 d3, dim4 d4
where f.time_id=t.time_id
and f.dim2_id=d2.dim2_id
and f.dim3_id1=d3.dim3_id1
and f.dim3_id2=d3.dim3_id2
and f.dim4_id=d4.dim4_id
and d2.dim2_lib='Value 5'
and d3.dim3_lib='Value (2,2)'
and d4.dim4_l2='L2.1'
and attr2='ZZ4'
and t.time_id=trunc(t.time_id,'W')
group by t.year_id
order by t.year_id;

YEAR_ID SUM(F.METRIC1)
--------- --------------
01-JAN-09 38490

Elapsed: 00:00:06.10

select *
from table(dbms_xplan.display_cursor(format=>'basic note'));

PLAN_TABLE_OUTPUT
-----------------
EXPLAINED SQL STATEMENT:
------------------------
select /* GG_2 */ t.year_id, sum(f.metric1) from fact f, time t, dim2
d2, dim3 d3, dim4 d4 where f.time_id=t.time_id and
f.dim2_id=d2.dim2_id and f.dim3_id1=d3.dim3_id1 and
f.dim3_id2=d3.dim3_id2 and f.dim4_id=d4.dim4_id and
d2.dim2_lib='Value 5' and d3.dim3_lib='Value (2,2)' and
d4.dim4_l2='L2.1' and attr2='ZZ4' and
t.time_id=trunc(t.time_id,'W') group by t.year_id order by t.year_id

Plan hash value: 1851413986

-----------------------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | NESTED LOOPS | |
| 3 | NESTED LOOPS | |
| 4 | HASH JOIN | |
| 5 | PART JOIN FILTER CREATE | :BF0000 |
| 6 | NESTED LOOPS | |
| 7 | NESTED LOOPS | |
| 8 | MERGE JOIN CARTESIAN | |
| 9 | PARTITION RANGE ALL | |
| 10 | TABLE ACCESS FULL | TIME |
| 11 | BUFFER SORT | |
| 12 | TABLE ACCESS FULL | DIM3 |
| 13 | PARTITION RANGE ITERATOR | |
| 14 | PARTITION HASH ALL | |
| 15 | BITMAP CONVERSION TO ROWIDS | |
| 16 | BITMAP AND | |
| 17 | BITMAP INDEX SINGLE VALUE | FACT_TIME_IDX |
| 18 | BITMAP INDEX SINGLE VALUE | FACT_DIM3_IDX |
| 19 | TABLE ACCESS BY LOCAL INDEX ROWID| FACT |
| 20 | PARTITION HASH JOIN-FILTER | |
| 21 | TABLE ACCESS FULL | DIM2 |
| 22 | INDEX UNIQUE SCAN | DIM4_PK |
| 23 | TABLE ACCESS BY INDEX ROWID | DIM4 |
-----------------------------------------------------------------
Après avoir exécuté la requête une première fois, exécutez la une seconde fois avec exactement le même texte et dans la même session. Dans mon cas, le plan change et prend désormais la valeur de hash 1094455219. Bien que le plan soit meilleur dans ce second cas, je m'attendais a priori a ce que le premier plan soit réutilisé :
set timing on
select /* GG */ t.year_id, sum(f.metric1)
from fact f, time t, dim2 d2, dim3 d3, dim4 d4
where f.time_id=t.time_id
and f.dim2_id=d2.dim2_id
and f.dim3_id1=d3.dim3_id1
and f.dim3_id2=d3.dim3_id2
and f.dim4_id=d4.dim4_id
and d2.dim2_lib='Value 5'
and d3.dim3_lib='Value (2,2)'
and d4.dim4_l2='L2.1'
and attr2='ZZ4'
and t.time_id=trunc(t.time_id,'W')
group by t.year_id
order by t.year_id;

YEAR_ID SUM(F.METRIC1)
--------- --------------
01-JAN-09 38490

Elapsed: 00:00:00.18

select *
from table(dbms_xplan.display_cursor(format=>'basic note'));

PLAN_TABLE_OUTPUT
-----------------
EXPLAINED SQL STATEMENT:
------------------------
select /* GG_2 */ t.year_id, sum(f.metric1) from fact f, time t, dim2
d2, dim3 d3, dim4 d4 where f.time_id=t.time_id and
f.dim2_id=d2.dim2_id and f.dim3_id1=d3.dim3_id1 and
f.dim3_id2=d3.dim3_id2 and f.dim4_id=d4.dim4_id and
d2.dim2_lib='Value 5' and d3.dim3_lib='Value (2,2)' and
d4.dim4_l2='L2.1' and attr2='ZZ4' and
t.time_id=trunc(t.time_id,'W') group by t.year_id order by t.year_id

Plan hash value: 1094455219

--------------------------------------------------------------
| Id | Operation | Name |
--------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | HASH JOIN | |
| 3 | NESTED LOOPS | |
| 4 | NESTED LOOPS | |
| 5 | MERGE JOIN CARTESIAN | |
| 6 | MERGE JOIN CARTESIAN | |
| 7 | PARTITION HASH ALL | |
| 8 | TABLE ACCESS FULL | DIM2 |
| 9 | BUFFER SORT | |
| 10 | TABLE ACCESS FULL | DIM3 |
| 11 | BUFFER SORT | |
| 12 | PARTITION RANGE ALL | |
| 13 | TABLE ACCESS FULL | TIME |
| 14 | PARTITION RANGE ITERATOR | |
| 15 | PARTITION HASH ITERATOR | |
| 16 | BITMAP CONVERSION TO ROWIDS | |
| 17 | BITMAP AND | |
| 18 | BITMAP INDEX SINGLE VALUE | FACT_TIME_IDX |
| 19 | BITMAP INDEX SINGLE VALUE | FACT_DIM3_IDX |
| 20 | BITMAP INDEX SINGLE VALUE | FACT_DIM2_IDX |
| 21 | TABLE ACCESS BY LOCAL INDEX ROWID| FACT |
| 22 | TABLE ACCESS FULL | DIM4 |
--------------------------------------------------------------

Note
-----
- cardinality feedback used for this statement
Comme vous le découvrez à travers la note du plan, le calcul est désormais impacté par des informations d'exécution. Je n'ai rien trouvé sur cette note sur le site du support ou les forums ; En outre, si vous collectez la trace 10053, vous découvrirez, non seulement que le curseur est reparsé mais également que des hints opt_estimate sont utilisés pour corriger les estimations de cardinalité du plan; intéressant ?

Conclusion

D'après mes tests, la valeur du paramètre caché _optimizer_extended_cursor_sharing_rel impacte ce fonctionnement. Toutefois, les informations sur lesquelles s'appuie l'optimiseur pour décider qu'il est plus intelligent de changer le plan ne semblent pas évidentes à trouver. En plus, la colonne IS_SHAREABLE de la vue V$SQL suggère a priori que le plan pourrait être réutilisé. Si vous interrogez la vue V$SQL_SHARED_CURSOR lorsque les 2 curseurs enfants sont dans la shared pool, vous ne verrez aucune différence ! Je suis vraiment curieux de découvrir pourquoi et comment... Quelqu'un a-t-il une idée ?

10 décembre 2009

Mon petit Data Mart (Partie 5) : Star Transformation et Bitmap Join Index

Dans les articles précédents consacrés à "mon petit Data Mart", nous nous sommes concentrés sur le partitioning et les mécanismes de ré-écritures de requêtes qui permettent d'utiliser des agrégats lorsque ceux-ci sont stockés dans des vues matérialisées. C'est assez basic mais ça permettra à quelques-uns, je le souhaite, de mettre du concret sur ce qui est présenté dans la documentation.

C'est un substance ce que je vous propose de faire, une fois encore, avec 2 exemples d'utilisation d'une "star transformation" puis de "bitmap join index". Si ce que vous cherchez est l'explication de ces 2 mécanismes, je ne saurais trop vous conseiller de lire les sections à propos des requêtes en étoile et des bitmap join indexes dans le "Data Warehousing Guide 11.2". Lisez également ces 2 articles de Jonathan Lewis, un peu ancien mais toujours aussi pertinents :

Schéma d'exemple

La star transformation et les index bitmap join permettent d'accélérer les accès aux données de détail lorsque celles-ci sont nécessaires. Nous allons donc re-créer un schema d'exemple, sans vue matérialisée cette fois-ci, pour vous permettre d'illustrer des plans et des temps de réponses associés.
Note:
J'ai testé cet exemple sur Oracle 11.2.0.1 sur Linux x86 32bits mais il devrait fonctionner avec Oracle 10g.
Le script ci-dessous crée les tables, données, statistiques et les indices pour nos requêtes :
drop table time purge;
drop table dim2 purge;
drop table dim3 purge;
drop table dim4 purge;
drop table d1 purge;
drop table d3 purge;
drop table d2 purge;
drop table d4 purge;
drop table fact purge;

create table fact
(d1 number not null,
d2 number not null,
d3 number not null,
d4 number not null,
x number);

begin
for i in 1..100 loop
insert into fact(d1, d2, d3, d4, x)
select d1, d2, d3, i, d1+d2+d3+i
from (select rownum d1 from dual
connect by level<=100) d1,
(select rownum d2 from dual
connect by level<=100) d2,
(select rownum d3 from dual
connect by level<=100) d3;
commit;
end loop;
end;
/

select count(*) from fact;

exec dbms_stats.gather_table_stats(user, 'FACT', -
method_opt=>'for all columns size 254')

create bitmap index fact_d1_bix on fact(d1);
create bitmap index fact_d2_bix on fact(d2);
create bitmap index fact_d3_bix on fact(d3);
create bitmap index fact_d4_bix on fact(d4);

create table d1
(d1 number,
d1_l number);
insert into d1
select rownum d1, mod(rownum,10)
from dual
connect by level<=100000;

exec dbms_stats.gather_table_stats(user, 'D1', -
method_opt=>'for all columns size 254')
alter table d1 add constraint d1_pk primary key(d1);

create table d2
(d2 number,
d2_l number);
insert into d2
select rownum d2, mod(rownum,10)
from dual
connect by level<=100000;

exec dbms_stats.gather_table_stats(user, 'D2', -
method_opt=>'for all columns size 254')
alter table d2 add constraint d2_pk primary key(d2);

create table d3
(d3 number,
d3_l number);
insert into d3
select rownum d3, mod(rownum,10)
from dual
connect by level<=100000;

exec dbms_stats.gather_table_stats(user, 'D3', -
method_opt=>'for all columns size 254')
alter table d3 add constraint d3_pk primary key(d3);

create table d4
(d4 number,
d4_l number);
insert into d4
select rownum d4, mod(rownum,10)
from dual
connect by level<=100000;

exec dbms_stats.gather_table_stats(user, 'D4', -
method_opt=>'for all columns size 254')
alter table d4 add constraint d4_pk primary key(d4);

Une requête simple

Voici une requête simple qui consiste à compter le nombre de lignes correspondant à un cluster de données dans notre table de fait segmenté selon certaines valeurs prises dans les tables de dimension:
alter session set star_transformation_enabled=false;
set timing on
select /*GG1*/ count(*)
from fact f, d1, d2, d3, d4
where f.d1=d1.d1
and f.d2=d2.d2
and f.d3=d3.d3
and f.d4=d4.d4
and d2_l=1
and d1_l=1
and d3_l=1
and d4_l=1;

COUNT(*)
----------
10000

Elapsed: 00:00:43.54

select * from table(dbms_xplan.display_cursor(format=>'basic note'));

EXPLAINED SQL STATEMENT:
------------------------
select /*GG1*/ count(*) from fact f, d1, d2, d3, d4 where f.d1=d1.d1
and f.d2=d2.d2 and f.d3=d3.d3 and f.d4=d4.d4 and d2_l=1
and d1_l=1 and d3_l=1 and d4_l=1

Plan hash value: 4147546651

---------------------------------------
| Id | Operation | Name |
---------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT AGGREGATE | |
| 2 | HASH JOIN | |
| 3 | TABLE ACCESS FULL | D1 |
| 4 | HASH JOIN | |
| 5 | TABLE ACCESS FULL | D3 |
| 6 | HASH JOIN | |
| 7 | TABLE ACCESS FULL | D2 |
| 8 | HASH JOIN | |
| 9 | TABLE ACCESS FULL| D4 |
| 10 | TABLE ACCESS FULL| FACT |
---------------------------------------
Note:
La valeur par défaut du paramètre star_transformation_enabled en 11.2 est false

La même requête avec une star transformation

Nous allons maintenant exécutez la même requête avec une star transformation. Vous remarquerez en particulier les opérations BITMAP KEY ITERATION puis BITMAP MERGE entre les tables de dimension et les index bitmap ainsi que l'opération de BITMAP AND entre tous les segments bitmap résultant des étapes préalables; le temps de réponse est significativement amélioré (sur mon laptop) :
alter session set star_transformation_enabled=true;
set timing on
select /*GG2*/ count(*)
from fact f, d1, d2, d3, d4
where f.d1=d1.d1
and f.d2=d2.d2
and f.d3=d3.d3
and f.d4=d4.d4
and d2_l=1
and d1_l=1
and d3_l=1
and d4_l=1;

COUNT(*)
----------
10000

Elapsed: 00:00:02.02

select * from table(dbms_xplan.display_cursor(format=>'basic note'));

EXPLAINED SQL STATEMENT:
------------------------
select /*GG1*/ count(*) from fact f, d1, d2, d3, d4 where f.d1=d1.d1
and f.d2=d2.d2 and f.d3=d3.d3 and f.d4=d4.d4 and d2_l=1
and d1_l=1 and d3_l=1 and d4_l=1

Plan hash value: 2278764636

----------------------------------------------------
| Id | Operation | Name |
----------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT AGGREGATE | |
| 2 | BITMAP CONVERSION COUNT | |
| 3 | BITMAP AND | |
| 4 | BITMAP MERGE | |
| 5 | BITMAP KEY ITERATION | |
| 6 | TABLE ACCESS FULL | D4 |
| 7 | BITMAP INDEX RANGE SCAN| FACT_D4_BIX |
| 8 | BITMAP MERGE | |
| 9 | BITMAP KEY ITERATION | |
| 10 | TABLE ACCESS FULL | D2 |
| 11 | BITMAP INDEX RANGE SCAN| FACT_D2_BIX |
| 12 | BITMAP MERGE | |
| 13 | BITMAP KEY ITERATION | |
| 14 | TABLE ACCESS FULL | D1 |
| 15 | BITMAP INDEX RANGE SCAN| FACT_D1_BIX |
| 16 | BITMAP MERGE | |
| 17 | BITMAP KEY ITERATION | |
| 18 | TABLE ACCESS FULL | D3 |
| 19 | BITMAP INDEX RANGE SCAN| FACT_D3_BIX |
----------------------------------------------------

Note
-----
- star transformation used for this statement

La même requête avec des Bitmap Join Indexes

Nous allons maintenant prendre du temps pour calculer des bitmap join indexes entre la table de faits et les colonnes des dimensions utilisées dans la clause where de notre requête :
drop index fact_d1_bix;
drop index fact_d2_bix;
drop index fact_d3_bix;
drop index fact_d4_bix;

create bitmap index fact_d1_bjix
on fact(d1.d1_l)
from d1, fact f
where d1.d1=f.d1;

create bitmap index fact_d2_bjix
on fact(d2.d2_l)
from d2, fact f
where d2.d2=f.d2;

create bitmap index fact_d3_bjix
on fact(d3.d3_l)
from d3, fact f
where d3.d3=f.d3;

create bitmap index fact_d4_bjix
on fact(d4.d4_l)
from d4, fact f
where d4.d4=f.d4;
La requête, sans star transformation, a un plan et des temps d'exécution significativement améliorés :
alter session set star_transformation_enabled=false;
set timing on
select /*GG2*/ count(*)
from fact f, d1, d2, d3, d4
where f.d1=d1.d1
and f.d2=d2.d2
and f.d3=d3.d3
and f.d4=d4.d4
and d2_l=1
and d1_l=1
and d3_l=1
and d4_l=1;

COUNT(*)
----------
10000

Elapsed: 00:00:00.63

select * from table(dbms_xplan.display_cursor(format=>'basic note'));

EXPLAINED SQL STATEMENT:
------------------------
select /*GG2*/ count(*) from fact f, d1, d2, d3, d4 where f.d1=d1.d1
and f.d2=d2.d2 and f.d3=d3.d3 and f.d4=d4.d4 and d2_l=1
and d1_l=1 and d3_l=1 and d4_l=1

Plan hash value: 2920980726

-----------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT AGGREGATE | |
| 2 | BITMAP CONVERSION COUNT | |
| 3 | BITMAP AND | |
| 4 | BITMAP INDEX SINGLE VALUE| FACT_D4_BJIX |
| 5 | BITMAP INDEX SINGLE VALUE| FACT_D1_BJIX |
| 6 | BITMAP INDEX SINGLE VALUE| FACT_D2_BJIX |
| 7 | BITMAP INDEX SINGLE VALUE| FACT_D3_BJIX |
-----------------------------------------------------

Conclusion

Evidemment, le résultat dépend du travail préalable réalisé lors des chargements. Ces outils peuvent néanmoins permettre de changer de plusieurs facteurs d'échelle les temps de réponse de vos requêtes.

07 décembre 2009

Mon petit Data Mart (Partie 4) : Clés étrangères et Query Rewrite

Cet article est le 4ème d'une série qui vise à explorer de manière itérative certains aspects d'un système d'information décisionnel avec Oracle Database 11.2. Voici une liste des articles précédents :.Dans ce nouvel article, nous allons voir comment les contraintes référentielles et les contraintes "not null" peuvent aider à améliorer les temps de réponse, même lorsqu'elles sont désactivées.

Contexte

Dans l'article précédent, nous nous étions arrêté sur le constat décevant que notre vue matérialisée n'était pas utilisée lorsque nous faisions une agrégation sur la-dite dimension mais sans faire la jointure avec la table correspondante; voici une requête typique qui illustre cette problématique :
select dim2_id id, sum(METRIC1)
from fact
group by dim2_id
order by 1;

ID SUM(METRIC1)
-- ------------
1 1040563094
2 1040793118
3 1041107486
4 1041441961
5 1041776436
6 1042120956
7 1041943136
8 1042713564
9 1042929508
10 1043016876

Elapsed: 00:00:19.14

select * from
table(dbms_xplan.display_cursor(format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select DIM2_ID, sum(METRIC1) from fact group by DIM2_ID order by 1

Plan hash value: 3822482429

-------------------------------------
| Id | Operation | Name |
-------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | PARTITION RANGE ALL| |
| 3 | PARTITION HASH ALL| |
| 4 | TABLE ACCESS FULL| FACT |
-------------------------------------

Foreign Key et Not Null

Et de fait... Rien ne garantit jusqu'à présent que les valeurs de la colonne time_id dans la table fact soient bien incluses dans la table de dimension que nous avons nommée time ou même qu'elles ne soient pas null. Or, dans l'un ou l'autre des cas, l'agrégation de toutes les valeurs sur cet axe sera différent selon qu'on joint à la table ou non. Pour garantir que c'est le cas, il suffit donc de créer une contrainte référentielle entre les colonnes time_id de fact et de time. Nous ajouterons ensuite une contrainte "not null" sur fact.time_id; notre requête pourra alors utiliser la vue matérialisée :
alter table fact 
add constraint fact_time_fk foreign key(time_id)
references time(time_id);

alter table fact modify (time_id not null);

set timing on
set num 11

select dim2_id id, sum(METRIC1)
from fact
group by dim2_id
order by 1;

ID SUM(METRIC1)
-- ------------
1 1040563094
2 1040793118
3 1041107486
4 1041441961
5 1041776436
6 1042120956
7 1041943136
8 1042713564
9 1042929508
10 1043016876

Elapsed: 00:00:00.02


select * from
table(dbms_xplan.display_cursor(format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select DIM2_ID, sum(METRIC1) from fact group by DIM2_ID order by 1

------------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | PARTITION RANGE ALL | |
| 3 | PARTITION HASH ALL | |
| 4 | MAT_VIEW REWRITE ACCESS FULL| AG_FACT_BY_MONTH |
------------------------------------------------------------

Désactiver les contraintes

La requête est donc ré-écrite. Cependant, maintenir les contraintes peut être très contraignant pour nos besoins; nous pouvons donc les désactiver et, avec l'option rely, proposer au CBO de les utiliser malgré tout; la vue matérialisée sera dans ce cas toujours utilisée et ceux malgré la désactivation des contraintes :
col constraint_name format a15
select constraint_name, constraint_type
from user_constraints
where table_name='FACT';

CONSTRAINT_NAME C
--------------- -
FACT_TIME_FK R
SYS_C0011620 C


alter table fact modify constraint time_fk rely disable;

alter session set query_rewrite_integrity=enforced;

select dim2_id id, sum(METRIC1)
from fact
group by dim2_id
order by 1;

ID SUM(METRIC1)
-- ------------
1 1040563094
2 1040793118
3 1041107486
4 1041441961
5 1041776436
6 1042120956
7 1041943136
8 1042713564
9 1042929508
10 1043016876

Elapsed: 00:00:00.19


select * from
table(dbms_xplan.display_cursor(format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select DIM2_ID, sum(METRIC1) from fact group by DIM2_ID order by 1

------------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | PARTITION RANGE ALL | |
| 3 | PARTITION HASH ALL | |
| 4 | MAT_VIEW REWRITE ACCESS FULL| AG_FACT_BY_MONTH |
------------------------------------------------------------
Note:
Vous remarquerez que l'on a pas à dire query_rewrite_integrity=trusted et que ça fonctionne, même si le paramètre est laissé à enforced.

Et ensuite ?

Notre petit système décisionnel commence à donner des temps de réponses intéressants dans de nombreux cas. Toutefois, lorsqu'on en vient à jouer avec les attributs ou de multiples dimensions au niveau du détail, c'est une autre histoire. Comment encore aller plus loin? c'est en substance le thème que nous aborderons dans le prochain article.

05 décembre 2009

Mon petit Data Mart (Partie 3) : Dimensions et Query Rewrite

Dans les 2 articles précédents Mon petit Data Mart (Partie 1) : Dimensions, Partitions et Données et Mon petit Data Mart (Partie 2) : Vues Matérialisées et Query Rewrite, nous avons commencé à construire un SI décisionnel avec Oracle Database 11.2 et montré les capacités de ré-écriture du SQL en tirant parti des vues matérialisées... y compris lorsque les données ne sont pas rafraichies!

Continuons d'ajouter aux fonctionnalités de notre modèle en créant des contraintes sur lesquelles l'optimiseur Oracle peut s'appuyer pour résoudre nos requêtes; nous allons en particulier explorer les "dimensions" qui permettent d'assurer le lien entre des niveaux d'agrégats décrits dans une table de dimensions.

Une vue matérialisée inutilisée

Comme présenté à la fin de l'article précédent, malgré le fait que la somme de la colonne metric1 soit calculée au niveau mois dans la vue matérialisée, si vous interrogez la table fact en faisant une agrégation au niveau de l'année, la vue matérialisée n'est pas utilisée :
set timing on
set num 11

select t.year_id year,
sum (f.metric1) metric1
from fact f, time t
where f.time_id=t.time_id
group by t.year_id
order by t.year_id;

YEAR METRIC1
--------- -----------
01-JAN-09 10248074782
01-JAN-10 170331353

Elapsed: 00:00:19.13

Contrainte "Dimension"

Pour permettre l'utilisation de la vue matérialisée, nous pouvons ajouter une dimension comme ci-dessous :
drop dimension time_dim;

create dimension time_dim
level day is time.time_id
level month is time.month_id
level quarter is time.quarter_id
level year is time.year_id
hierarchy y_rollup (
day child of
month child of
quarter child of
year)
attribute day determines (date_lib)
attribute month determines (month_lib)
attribute month determines (quarter_lib)
attribute month determines (year_lib);
Une dimension est une contrainte qui n'est pas vérifiée par Oracle. Pour prendre en compte la contrainte, il faut donc dire à l'optimiseur que nous faisons confiance à la contrainte, même si Oracle ne l'a pas vérifiée avec la commande ci-dessous :
alter session set query_rewrite_integrity=trusted;
La requête peut alors être exécutée; elle utilisera la vue matérialisée :
select t.year_id year,
sum (f.metric1) metric1
from fact f, time t
where f.time_id=t.time_id
group by t.year_id
order by t.year_id;

YEAR METRIC1
--------- -----------
01-JAN-09 10248074782
01-JAN-10 170331353

Elapsed: 00:00:00.10


select * from table(
dbms_xplan.display_cursor(format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select t.year_id year, sum (f.metric1) metric1 from fact f,
time t where f.time_id=t.time_id group by t.year_id order by
t.year_id

Plan hash value: 747418634

-------------------------------------------------------------
| Id | Operation | Name |
-------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | HASH JOIN | |
| 3 | PART JOIN FILTER CREATE | :BF0000 |
| 4 | VIEW | |
| 5 | HASH UNIQUE | |
| 6 | PARTITION RANGE ALL | |
| 7 | TABLE ACCESS FULL | TIME |
| 8 | PARTITION RANGE JOIN-FILTER | |
| 9 | PARTITION HASH ALL | |
| 10 | MAT_VIEW REWRITE ACCESS FULL| AG_FACT_BY_MONTH |
-------------------------------------------------------------

Dimension non valide

Comme vous pouvez vous en rendre compte ci-dessous, la requête n'utilise pas la vue matérialisée si Oracle doit valider l'intégrité de la base de données :
alter session set query_rewrite_integrity=enforced;

select t.year_id year,
sum (f.metric1) metric1
from fact f, time t
where f.time_id=t.time_id
group by t.year_id
order by t.year_id;

YEAR METRIC1
--------- -----------
01-JAN-09 10248074782
01-JAN-10 170331353

Elapsed: 00:00:18.99
Mais que se passe-t-il si une dimension n'est pas consistante ? Dans ce cas, le résultat sera faux comme vous pourrez le découvrir dans les 2 requêtes ci-dessous après avoir modifié les données de la table time :
update time set year_id=to_date('01/01/2010', 'DD/MM/YYYY'),
year_lib='2010'
where TIME_ID=to_date('31/12/2009', 'DD/MM/YYYY');

commit;

exec dbms_mview.refresh('AG_FACT_BY_MONTH','complete')

select t.year_id year,
sum (f.metric1) metric1
from fact f, time t
where f.time_id=t.time_id
group by t.year_id
order by t.year_id;

YEAR METRIC1
--------- -----------
01-JAN-09 10248074782
01-JAN-10 1037840410

Elapsed: 00:00:00.05

select t.year_id year,
sum (f.metric1) metric1
from fact f, time t
where f.time_id=t.time_id
group by t.year_id
order by t.year_id;

YEAR METRIC1
--------- -----------
01-JAN-09 10219840501
01-JAN-10 198565634

Elapsed: 00:00:19.14

Valider la cohérence de la dimension

Oracle offre une procédure DBMS_OLAP.VALIDATE_DIMENSION pour valider votre dimension. Pour l'utiliser, nous pouvons procéder comme suit :
truncate table dimension_exceptions;

exec dbms_olap.validate_dimension ('TIME_DIM', user, -
false, true);

col year_lib format a4
select time_id, year_id, year_lib from time
where rowid in (select bad_rowid
from dimension_exceptions)
order by 1;

TIME_ID YEAR_ID YEAR
--------- --------- ----
01-OCT-09 01-JAN-09 2009
02-OCT-09 01-JAN-09 2009
[...]
30-DEC-09 01-JAN-09 2009
31-DEC-09 01-JAN-10 2010
La dernière requête affiche l'ensemble des lignes qui ne vérifient pas la contrainte de dimension, à savoir l'ensemble des lignes du 3ème trimestre 2009. Ci-dessous, voici comment corriger cette dimension:
update time set year_id=to_date('01/01/2009', 'DD/MM/YYYY'),
year_lib='2009'
where TIME_ID=to_date('31/12/2009', 'DD/MM/YYYY');

commit;

exec dbms_mview.refresh('AG_FACT_BY_MONTH','complete')

truncate table dimension_exceptions;

exec dbms_olap.validate_dimension ('TIME_DIM', user, -
false, true);

col year_lib format a4
select time_id, year_id, year_lib
from time
where rowid in
(select bad_rowid
from dimension_exceptions)
order by 1;

no rows selected
Note
Malgré le fait que la dimension ait maintenant été validée, il est toujours nécessaire d'utiliser le paramètre query_rewrite_integrity=trusted pour que la vue soit utilisée.

Et ensuite ?

Si l'utilisation des différents niveaux de la dimension peuvent maintenant être utilisés, il reste encore certains cas où la vue matérialisée pourrait être utilisée mais ne l'est pas. Voici un autre de ces cas :
select DIM2_ID id, sum(METRIC1)
from fact
group by DIM2_ID
order by 1;

ID SUM(METRIC1)
-- ------------
1 1040563094
2 1040793118
3 1041107486
4 1041441961
5 1041776436
6 1042120956
7 1041943136
8 1042713564
9 1042929508
10 1043016876

Elapsed: 00:00:19.14


select * from
table(dbms_xplan.display_cursor(format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select DIM2_ID, sum(METRIC1) from fact group by DIM2_ID order by 1

Plan hash value: 3822482429

-------------------------------------
| Id | Operation | Name |
-------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | PARTITION RANGE ALL| |
| 3 | PARTITION HASH ALL| |
| 4 | TABLE ACCESS FULL| FACT |
-------------------------------------
Vous trouverez donc dans le prochain article comment les contraintes référentielles et not null peuvent, même inactives, permettre la ré-écriture de certaines autres requêtes.

Mon petit Data Mart (Partie 2) : Vues Matérialisées et Query Rewrite

Si vous avez suivi l'article précédent intitulé "Mon petit Data Mart (Partie 1) : Dimensions, Partitions et Données", vous disposez maintenant d'une base de données 11g Release 2 avec une table de fait et 4 tables de dimensions. Nous allons maintenant utiliser ce schéma pour illustrer l'utilisation et l'intérêt des vues matérialisées et du query rewrite...

Un calcul de somme

Commençons par une requête simple qui permet de calculer la somme des colonnes metric1 groupées par an. Comme vous pouvez le voir, dans mon cas ci-dessous, le temps d'exécution (en 11.2.0.1 sur mon laptop sous Linux), il faut à peu près 20 secondes pour exécuter la requête :
alter session set query_rewrite_enabled=true;
alter session set query_rewrite_integrity=enforced;
set timing on
set num 12
select trunc(t.month_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by trunc(t.month_id,'YYYY')
order by 1;

YEAR M1
--------- ------------
01-JAN-09 10248074782
01-JAN-10 170331336

Elapsed: 00:00:20.01

Vue matérialisée et query rewrite

Pour vous montrer l'intérêt d'une vue matérialisée, vous allons créée une vue (sans toutefois que celle-ci soit rafraichie automatiquement) et nous arranger pour que la requête puisse l'utiliser. Dans notre cas, nous allons simplement aggréger la table FACT au niveau mois de l'axe temps (sans toucher les autres niveaux). Voilà la vue matérialisée associée:

drop materialized view ag_fact_by_month;

create materialized view ag_fact_by_month
partition by range(month_id) interval(NUMTOYMINTERVAL(1,'MONTH'))
subpartition by hash(dim2_id) subpartitions 2
(partition p0 values less than (to_date('01/01/2009','DD/MM/YYYY')))
nologging
enable query rewrite
as select t.month_id,
f.dim2_id,
f.dim3_id1,
f.dim3_id2,
f.dim4_id,
sum(f.metric1) metric1_sum,
count(f.metric1) metric1_count,
avg(f.metric1) metric1_avg,
sum(f.metric2) metric2_sum,
count(f.metric2) metric2_count,
avg(f.metric2) metric2_avg
from fact f, time t
where f.time_id=t.time_id
group by t.month_id, f.dim2_id,
f.dim3_id1, f.dim3_id2, f.dim4_id;

Elapsed: 00:00:35.62

select count(*)
from ag_fact_by_month;

COUNT(*)
--------
15600
Si vous relancez la requête précédente, vous constaterez que les temps d'exécution de la requête sont radicalement transformés et, que le plan d'exécution utilise notre vue matérialisée :
alter session set query_rewrite_enabled=true;
alter session set query_rewrite_integrity=enforced;
set timing on
set num 12
select trunc(t.month_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by trunc(t.month_id,'YYYY')
order by 1;

YEAR M1
--------- ------------
01-JAN-09 10248074782
01-JAN-10 170331336

Elapsed: 00:00:00.03


select *
from table(dbms_xplan.display_cursor(
format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select trunc(t.month_id,'YYYY') year, sum(f.metric1) m1 from
fact f, time t where t.time_id=f.time_id group by
trunc(t.month_id,'YYYY') order by 1

Plan hash value: 2405448810

-----------------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT ORDER BY | |
| 2 | HASH GROUP BY | |
| 3 | MAT_VIEW REWRITE ACCESS FULL| AG_FACT_BY_MONTH |
-----------------------------------------------------------

Consistence et Query Rewrite

Lorsque vous modifiez les données de la table de faits, la vue matérialisée n'est plus utilisable dans les conditions normales. L'exemple qui suit illustre les 2 méthodes qui permettent d'utiliser la vue matérialisée de nouveau à savoir (1) l'utilisation du paramètre query_rewrite_integrity=stale_tolerated ou (2) le refresh de la vue matérialisée :
insert into fact
values (to_date('06/01/2010','DD/MM/YYYY'),
2, 2, 1, 'AXIS 2',
'XX', 'XS3', 7,
to_date('11/01/2010','DD/MM/YYYY'),
17, 22);

commit;

set timing on
set num 12
select trunc(t.month_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by trunc(t.month_id,'YYYY')
order by 1;

YEAR M1
--------- ------------
01-JAN-09 10248074782
01-JAN-10 170331353

Elapsed: 00:00:19.88


alter session set query_rewrite_integrity=stale_tolerated;

select trunc(t.month_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by trunc(t.month_id,'YYYY')
order by 1;

YEAR M1
--------- ------------
01-JAN-09 10248074782
01-JAN-10 170331336

Elapsed: 00:00:00.02


exec dbms_mview.refresh('ag_fact_by_month','complete')
alter session set query_rewrite_integrity=enforced;
select trunc(t.month_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by trunc(t.month_id,'YYYY')
order by 1;

YEAR M1
--------- ------------
01-JAN-09 10248074782
01-JAN-10 170331353

Elapsed: 00:00:00.02

Et ensuite ?

Si l'utilisation du niveau aggrégé (i.e. time.month_id) passe par la vue matérialisée, il n'en est rien si vous interrogez un autre niveau (e.g. time.year_id), comme vous pouvez vous en rendre compte ci-dessous :
select to_char(t.year_id,'YYYY') year,
sum(f.metric1) m1
from fact f, time t
where t.time_id=f.time_id
group by to_char(t.year_id,'YYYY')
order by 1;

YEAR M1
---- ------------
2009 10248074782
2010 170331353

Elapsed: 00:00:20.94


select *
from table(dbms_xplan.display_cursor(
format=>'basic'));

EXPLAINED SQL STATEMENT:
------------------------
select to_char(t.year_id,'YYYY') year, sum(f.metric1) m1 from
fact f, time t where t.time_id=f.time_id group by
to_char(t.year_id,'YYYY') order by 1

Plan hash value: 2313903028

--------------------------------------
| Id | Operation | Name |
--------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | SORT GROUP BY | |
| 2 | PARTITION RANGE ALL | |
| 3 | HASH JOIN | |
| 4 | TABLE ACCESS FULL | TIME |
| 5 | PARTITION HASH ALL| |
| 6 | TABLE ACCESS FULL| FACT |
--------------------------------------
Dans notre prochain article, nous explorerons l'utilisation des dimensions pour répondre à ce besoin.

Mon petit Data Mart (Partie 1) : Dimensions, Partitions et Données

Statistiques, partitioning, vues matérialisées, query rewrite, dimensions, indices bitmap et bitmap join, parallelisme, Olap ou Exadata Storage Server, les fonctionnalités Oracle Database 11g Release 2 liées au décisionnel sont nombreuses et pas toujours simples à tester. Bien sur, il y a le schéma SH sur lequel vous pouvez vous appuyer pour une bonne partie de vos tests. Toutefois, SH implémente déjà toute ces fonctionnalités et je vous propose plutôt la démarche inverse, à savoir de partir de rien et de construire, étape par étape, votre système décisionnel.

Dans ce premier article, intitulé "Dimensions, Partitions et Données", vous trouverez la première étape de la construction d'un système décisionnel qui s'appuie sur Oracle 11g, un partitionnement composite par interval et hash et génère des données de manière désordonnées. Contrairement à SH, les noms des tables ne sont pas toujours très opportuns, mais l'important n'est-il pas d'avoir un exemple pour effectuer ses tests?

Table et Partitionnement Composite par Interval/Hash

Dans un premier temps, nous allons créer une table de faits qui contient les informations de détail que l'on veut exploiter. La table pointera au niveau de détail sur 4 dimensions dont la première est une dimension de type date et la 3ème a une clé utilisant 2 colonnes. Une fois la table créée, nous pourrons générer un jeu de données et prendre les statistiques associées :
drop table fact purge;

create table fact(time_id date,
dim2_id number,
dim3_id1 number,
dim3_id2 number,
dim4_id varchar2(15),
attr1 varchar2(2),
attr2 varchar2(5),
attr3 number,
attr4 date,
metric1 number,
metric2 number)
partition by range(time_id) interval(NUMTOYMINTERVAL(1,'MONTH'))
subpartition by hash(dim2_id) subpartitions 2
(partition P0 values less than (to_date('01/01/2009','DD/MM/YYYY')))
nologging;

insert /*+append */ into fact
select t.time_id,
d2.dim2_id,
d3.dim3_id1,
d3.dim3_id2,
d4.dim4_id,
to_char(mod(t_val,9)),
'ZZ'||to_char(mod(t_val+d3_val,127)),
mod(t_val+d3_val+d2_val,1034),
to_date('01/01/2009','DD/MM/YYYY')
+ mod(t_val+d3_val+d2_val-d4_val,365),
mod(t_val*d3_val*d2_val+d4_val,2009),
mod(t_val*d3_val*d2_val*d4_val,40003)
from (select rownum t_val,
to_date('01/01/2009','DD/MM/YYYY')
+ floor(rownum/27) time_id
from dual
connect by level <= 10000) t,
(select rownum d2_val,
rownum dim2_id
from dual
connect by level <= 10) d2,
(select mod(rownum,7) d3_val,
mod(rownum,3) dim3_id1,
floor(rownum/3) dim3_id2
from dual
connect by level <= 30) d3,
(select rownum d4_val,
'AXIS '||to_char(rownum) dim4_id
from dual
connect by level <= 4) d4;

commit;

exec dbms_stats.gather_table_stats(user, -
'FACT', cascade=>true, -
method_opt=>'for all columns size 254');

Dimension temporelle

La première dimension est une date. Nous allons créer et alimenter une table qui contient l'ensemble des jours stockés dans la table de faits ainsi que les différents niveaux que nous pourrons utiliser pour nos aggrégations (mois, trimestres, années) :
drop table time purge;

create table time (time_id date,
date_lib varchar2(25),
month_id date,
month_lib varchar2(25),
quarter_id date,
quarter_lib varchar2(25),
year_id date,
year_lib varchar2(25))
partition by range(time_id) interval(NUMTOYMINTERVAL(1,'MONTH'))
(partition P0 values less than (to_date('01/01/2009','DD/MM/YYYY')))
nologging;

insert /*+ append */ into time
select time_id,
to_char(time_id,'YYYYMMDD'),
trunc(time_id, 'MM'),
to_char(trunc(time_id, 'MM'),'YYYYMMDD'),
trunc(time_id, 'Q'),
to_char(trunc(time_id, 'Q'),'YYYYMM'),
trunc(time_id, 'YYYY'),
to_char(trunc(time_id, 'YYYY'),'YYYY')
from (select to_date('01/01/2009','DD/MM/YYYY')+rownum-1 time_id
from dual
connect by level <= 371) t;

commit;

exec dbms_stats.gather_table_stats(user, -
'TIME', cascade=>true, -
method_opt=>'for all columns size 254');

create unique index time_pk
on time (time_id) local;

alter table time
add constraint time_pk
primary key (time_id)
using index time_pk;

Seconde dimension

La seconde dimension est utilisée comme clé de hash du sous partitionnement de la table de fait. Nous allons créer et remplir la dimension associée :
drop table dim2 purge;

create table dim2 (dim2_id number,
dim2_lib varchar2(25),
dim2_l1 number,
l1_lib varchar2(25))
partition by hash(dim2_id) partitions 2
nologging;

insert /*+ append */ into dim2
(select rownum,
'Value '||to_char(rownum),
mod(rownum ,3),
'Aggregate '||to_char(mod(rownum ,3))
from dual
connect by level <= 10);

commit;

exec dbms_stats.gather_table_stats(user, -
'DIM2', cascade=>true, -
method_opt=>'for all columns size 254');

create unique index dim2_pk
on dim2 (dim2_id) local;

alter table dim2
add constraint dim2_pk
primary key (dim2_id)
using index dim2_pk;

3ème dimension multi-colonnes

La clé primaire de la troisième dimension est constituée de 2 colonnes. Nous allons créer et remplir la dimension associée :
drop table dim3 purge;

create table dim3 (dim3_id1 number,
dim3_id2 number,
dim3_lib varchar2(25),
dim3_l1 number,
l1_lib varchar2(25))
nologging;

insert /*+ append */ into dim3
(select mod(rownum,7) d3_val,
mod(rownum,3) dim3_id1,
'Value ('||to_char(mod(rownum,7))||','||to_char(mod(rownum,3))||')',
floor(rownum/3) dim3_l1,
'Aggregate '||to_char(floor(rownum/3))
from dual
connect by level <= 21);
commit;

exec dbms_stats.gather_table_stats(user, -
'DIM3', cascade=>true, -
method_opt=>'for all columns size 254');

create unique index dim3_pk
on dim3 (dim3_id1,dim3_id2);

alter table dim3
add constraint dim3_pk
primary key (dim3_id1,dim3_id2)
using index dim3_pk;

4ème dimension

La clé primaire de la quatrième dimension est constituée d'une colonne de type varchar2. Cette dimension, contient 2 niveaux en plus du niveau de détail et du total :
drop table dim4 purge;

create table dim4 (dim4_id varchar2(15),
dim4_lib varchar2(25),
dim4_l1 varchar2(15),
l1_lib varchar2(25),
dim4_l2 varchar2(15),
l2_lib varchar2(25))
nologging;


insert /*+ append */ into dim4
(select 'AXIS ' ||to_char(rownum) dim4_id,
'Label ' ||to_char(rownum) dim4_id,
'L1.' ||to_char(case when rownum<3 then 1
when rownum=3 then 2
else 3 end),
'Level1 '||to_char(case when rownum<3 then 1
when rownum=3 then 2
else 3 end),
'L2.' ||to_char(case when rownum<=3 then 1
else 3 end),
'Level2 '||to_char(case when rownum<=3 then 1
else 3 end)
from dual
connect by level <= 4);


commit;

exec dbms_stats.gather_table_stats(user, -
'DIM4', cascade=>true, -
method_opt=>'for all columns size 254');

create unique index dim4_pk
on dim4 (dim4_id);

alter table dim4
add constraint dim4_pk
primary key (dim4_id)
using index dim4_pk;

Et maintenant ?

Nous voilà, en 5 minutes, avec un schéma pour servir de base à nos futurs articles. Nous allons commencer dès le prochain article par la création d'une vue matérialisée et son utilisation avec les fonctions de query rewrite...