Optimización SQL en Oracle – Ya a la venta!

El libro ya está disponible en todas las tiendas Amazon (.com, .co.uk, .es, .de, etc.)

¡Muchísimas gracias! ¡Espero que os guste y os sea útil!

Amazon.es              Amazon.com

Podéis echarle un ojo al interior aquí (mejor en pantalla completa):

Optimización SQL en Oracle. Últimos retoques.

¡Por fin!

El libro «Optimización SQL en Oracle» está terminado. 

En cuanto finalice el diseño de la portada y la contraportada (si los de Amazon no ponen impedimento) ya estará disponible para comprar tanto en amazon.com como en amazon.eu.

El libro ha contado con dos revisores técnicos de peso, que le han sacado punta a todo y han sentado a debate tanto los ejemplos, imágenes y conceptos expuestos, como la filosofía de trabajo del libro. Son Arturo Gutierrez y Jetro Marco. Gracias a ellos el libro ha pasado de unas 316 páginas a las más de 420 actuales. 
El índice finalmente ha quedado así:
Al final abordamos todo desde las tripas, tanto el tratamiento de las bind variables como el funcionamiento del Bind Variable Peeking, el Adaptive Cursor Sharing, el  como SQL Profiles, SQL Baselines, reescritura de vistas materializadas, el paralelismo, particionamiento, así como trazas y planes de ejecución de todo.
El libro está lleno de técnicas, herramientas, base de conocimiento y nuestro aporte profesional sobre cómo optimizar cualquier SQL que de un mal rendimiento. Nos metemos a contar qué pasa en particular con los entornos data warehouse (¿Se puede ejecutar PL/SQL masivo en un entorno data warehouse? … pues hay un capítulo que trata de cómo hacerlo a un rendimiento brutal!), desmontar mitos, descubrir la realizad subyacente de trucos como lanzar INSERTS con APPEND y cosas así. ¿Por qué Oracle dice que seguirá un plan de ejecución y luego decide utilizar otro? ¿cómo lidiar con todo esto sin volvernos locos????
Hemos destripado todas las hints, poniéndolas a prueba. Hemos buscado ejemplos de SQL ineficiente y de múltiples ejecuciones de un mismo código para rizar el rizo y comprender qué sucede en el CBO, cómo se estima la cardinalidad de las operaciones, por qué (a veces) Oracle se equivoca y por qué a veces somos nosotros los que no entendemos al motor.
Además, la bbdd está disponible para descargar gratuitamente aquí, y el SQL del libro (próximamente)!

Manejo de subconsultas en la cláusula SELECT. Parte II

(Continúa de Parte I)

Este post podría llamarse «La paradoja del increíble coste menguante» como si de un relato de G. K. Chesterton se tratara.

Si alguien pensó por la lectura de la parte I de este post que las subconsultas en la cláusula SELECT mejoraban el rendimiento, pues permitían reproducir consultas en estrella sin necesidad de tener un modelo en estrella, ni dimensiones ni jerarquías, está al borde de cometer un grave error.
El optimizador ignora los costes de combinación de las subconsultas en la cláusula SELECT, contando únicamente con el coste de acceso a los objetos de esa subconsulta. Esto sucede incluso en versión Oracle11gR2.
Como ejemplo sirva la siguiente consulta formulada sobre VUELOS (57.711 filas), RESERVAS (171.113 filas) y CLIENTES (9999 filas).

Consulta de reservas, con datos de vuelos y clientes expresado con dos joins

select reservas.id_reserva, reservas.importe, vuelos.detalles, clientes.apellidos
    from vuelos, reservas, clientes
    where vuelos.id_vuelo=reservas.vue_id_vuelo
      and reservas.cli_nif=clientes.nif;

Consulta de reservas, con datos de vuelos y clientes expresado con una join y una subconsulta en la cláusula SELECT

select reservas.id_reserva, reservas.importe,
     (select vuelos.detalles from vuelos
       where vuelos.id_vuelo=reservas.vue_id_vuelo) vuelo,
    clientes.apellidos
    from reservas, clientes
    where reservas.cli_nif=clientes.nif;

Consulta de reservas, con datos de vuelos y clientes expresado con dos subconsultas en la cláusula SELECT

select reservas.id_reserva, reservas.importe,
     (select vuelos.detalles from vuelos   
       where reservas.vue_id_vuelo=vuelos.id_vuelo) vuelo,
     (select clientes.apellidos    from clientes 
       where reservas.cli_nif=clientes.nif) cliente
    from reservas;
Los correspondientes planes de ejecución parecen evidenciar lo mencionado anteriormente: el optimizador de costes no es capaz de evaluar el impacto de la combinación de elementos de la consulta principal con los de las subconsultas en la cláusula SELECT. Por este motivo, los costes de los planes de ejecución cada vez son inferiores.

Ejecución de la consulta de reservas, con datos de vuelos y clientes expresado con dos joins con el plan de ejecución asociado y la traza de AUTOTRACE

SQL> select reservas.id_reserva, reservas.importe, vuelos.detalles, clientes.apellidos
  2      from vuelos, reservas, clientes
  3      where vuelos.id_vuelo=reservas.vue_id_vuelo
  4        and reservas.cli_nif=clientes.nif;
171113 filas seleccionadas.
Transcurrido: 00:00:01.54
Plan de Ejecución
———————————————————-
Plan hash value: 858327892
—————————————————————————————-
| Id  | Operation           | Name     | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
—————————————————————————————-
|   0 | SELECT STATEMENT    |          |   171K|    13M|       |   904   (2)| 00:00:11 |
|*  1 |  HASH JOIN          |          |   171K|    13M|       |   904   (2)| 00:00:11 |
|   2 |   TABLE ACCESS FULL | CLIENTES |  9999 |   361K|       |    27   (0)| 00:00:01 |
|*  3 |   HASH JOIN         |          |   171K|  7686K|  1528K|   875   (2)| 00:00:11 |
|   4 |    TABLE ACCESS FULL| VUELOS   | 57711 |   845K|       |   137   (1)| 00:00:02 |
|   5 |    TABLE ACCESS FULL| RESERVAS |   171K|  5180K|       |   311   (2)| 00:00:04 |
—————————————————————————————-
Predicate Information (identified by operation id):
—————————————————
   1 – access(«RESERVAS».»CLI_NIF»=»CLIENTES».»NIF»)
   3 – access(«VUELOS».»ID_VUELO»=»RESERVAS».»VUE_ID_VUELO»)
Estadísticas
———————————————————-
         15  recursive calls
          0  db block gets
      13013  consistent gets
         96  physical reads
          0  redo size
    7835592  bytes sent via SQL*Net to client
     125996  bytes received via SQL*Net from client
      11409  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
     171113  rows processed
A grandes rasgos, el resumen de la ejecución puede ser una lectura de 13.013 bloques en memoria, un tiempo de ejecución de 1 minuto y 54 segundos y un coste de 904.

Ejecución de la consulta de reservas, con datos de vuelos y clientes expresado con una join y una subconsulta en la cláusula SELECT con el plan de ejecución asociado y la traza de AUTOTRACE

SQL> select reservas.id_reserva, reservas.importe,
  2     (select vuelos.detalles from vuelos
  3         where vuelos.id_vuelo=reservas.vue_id_vuelo) vuelo,
  4      clientes.apellidos
  5      from reservas, clientes
  6      where reservas.cli_nif=clientes.nif;
171113 filas seleccionadas.
Transcurrido: 00:00:02.40
Plan de Ejecución
———————————————————-
Plan hash value: 402988295
—————————————————————————————-
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
—————————————————————————————-
|   0 | SELECT STATEMENT            |          |   171K|    11M|   340   (2)| 00:00:05 |
|   1 |  TABLE ACCESS BY INDEX ROWID| VUELOS   |     1 |    15 |     2   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | VUE_PK   |     1 |       |     1   (0)| 00:00:01 |
|*  3 |  HASH JOIN                  |          |   171K|    11M|   340   (2)| 00:00:05 |
|   4 |   TABLE ACCESS FULL         | CLIENTES |  9999 |   361K|    27   (0)| 00:00:01 |
|   5 |   TABLE ACCESS FULL         | RESERVAS |   171K|  5180K|   311   (2)| 00:00:04 |
—————————————————————————————-
Predicate Information (identified by operation id):
—————————————————
   2 – access(«VUELOS».»ID_VUELO»=:B1)
   3 – access(«RESERVAS».»CLI_NIF»=»CLIENTES».»NIF»)
Estadísticas
———————————————————-
         15  recursive calls
          0  db block gets
     374003  consistent gets
          0  physical reads
          0  redo size
    7835589  bytes sent via SQL*Net to client
     125996  bytes received via SQL*Net from client
      11409  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
     171113  rows processed
En esta ejecución, el número de bloques leídos en memoria ha aumentado a 374.003 y el tiempo de ejecución ha aumentado a 2 minutos 40 segundos. Sin embargo el coste de la ejecución se ha reducido a 340 (menos de la mitad). El número de bytes estimado como total de la ejecución también se estima mejorado: de 13 millones a 11 millones.

Ejecución de la consulta de reservas, con datos de vuelos y clientes expresado con dos subconsultas en la cláusula SELECT con el plan de ejecución asociado y la traza de AUTOTRACE
SQL> select reservas.id_reserva, reservas.importe,
  2       (select vuelos.detalles from vuelos   
  3          where reservas.vue_id_vuelo=vuelos.id_vuelo) vuelo,
  4       (select clientes.apellidos    from clientes 
  5          where reservas.cli_nif=clientes.nif) cliente
  6      from reservas;
171113 filas seleccionadas.
Transcurrido: 00:00:02.39
Plan de Ejecución
———————————————————-
Plan hash value: 465102819
—————————————————————————————-
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
—————————————————————————————-
|   0 | SELECT STATEMENT            |          |   171K|  5180K|   311   (2)| 00:00:04 |
|   1 |  TABLE ACCESS BY INDEX ROWID| VUELOS   |     1 |    15 |     2   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | VUE_PK   |     1 |       |     1   (0)| 00:00:01 |
|   3 |  TABLE ACCESS BY INDEX ROWID| CLIENTES |     1 |    37 |     2   (0)| 00:00:01 |
|*  4 |   INDEX UNIQUE SCAN         | CLI_PK   |     1 |       |     1   (0)| 00:00:01 |
|   5 |  TABLE ACCESS FULL          | RESERVAS |   171K|  5180K|   311   (2)| 00:00:04 |
—————————————————————————————-
Predicate Information (identified by operation id):
—————————————————
   2 – access(«VUELOS».»ID_VUELO»=:B1)
   4 – access(«CLIENTES».»NIF»=:B1)
Estadísticas
———————————————————-
         15  recursive calls
          0  db block gets
     406374  consistent gets
          0  physical reads
          0  redo size
    7835587  bytes sent via SQL*Net to client
     125996  bytes received via SQL*Net from client
      11409  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
     171113  rows processed
En este caso, el tiempo de ejecución es prácticamente el mismo, mientras que el coste se muestra aun mejor que el de la ejecución anterior (340 anteriores frente a 311) pero el número de bloques leídos en memoria aumenta (374.003 anteriores frente a 406.374).

Las trazas generadas por la utilidad tkprof vienen a confirmar prácticamente lo mostrado en la traza de autotrace.

Traza de la utilidad TKPROF sobre la consulta de reservas, con datos de vuelos y clientes expresada con dos joins

select reservas.id_reserva, reservas.importe, vuelos.detalles, clientes.apellidos
    from vuelos, reservas, clientes
    where vuelos.id_vuelo=reservas.vue_id_vuelo
      and reservas.cli_nif=clientes.nif
call     count       cpu    elapsed       disk      query    current        rows
——- ——  ——– ———- ———- ———- ———-  ———-
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch    11409      0.39       0.51         96      13009          0      171113
——- ——  ——– ———- ———- ———- ———-  ———-
total    11411      0.39       0.52         96      13009          0      171113

Traza de la utilidad TKPROF sobre la consulta de reservas, con datos de vuelos y clientes expresada con una join y una subconsulta en la cláusula SELECT

select reservas.id_reserva, reservas.importe,
     (select vuelos.detalles from vuelos where vuelos.id_vuelo=reservas.vue_id_vuelo) vuelo,
    clientes.apellidos
    from reservas, clientes
    where reservas.cli_nif=clientes.nif
call     count       cpu    elapsed       disk      query    current        rows
——- ——  ——– ———- ———- ———- ———-  ———-
Parse        1      0.01       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch    11409      1.26       1.27          0     373999          0      171113
——- ——  ——– ———- ———- ———- ———-  ———-
total    11411      1.27       1.27          0     373999          0      171113

Traza de la utilidad TKPROF sobre la consulta de reservas, con datos de vuelos y clientes expresada con dos subconsultas en la cláusula SELECT

select reservas.id_reserva, reservas.importe,
     (select vuelos.detalles from vuelos   
       where reservas.vue_id_vuelo=vuelos.id_vuelo) vuelo,
     (select clientes.apellidos    from clientes 
       where reservas.cli_nif=clientes.nif) cliente
    from reservas
call     count       cpu    elapsed       disk      query    current        rows
——- ——  ——– ———- ———- ———- ———-  ———-
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch    11409      1.32       1.24          0     406370          0      171113
——- ——  ——– ———- ———- ———- ———-  ———-
total    11411      1.32       1.24          0     406370          0      171113
En las dos ejecuciones con subconsultas en la cláusula SELECT se aprecia, además, el aumento de tiempo de CPU por el mayor número de bloques a procesar en memoria.
Cuidado, por tanto, con las subconsultas expresadas a ese nivel de ejecución, pues el optimizador no evalua sus pesos correctamente, al quedar fuera del estudio de accesos y combinaciones entre tablas, mediante joins y filtros convencionales. Los resultados expresados por los planes de ejecución de su estimación en coste pueden confundir, ya que muestran costes mejores sobre ejecuciones claramente más ineficientes.

Manejo de subconsultas en la cláusula SELECT. Parte I.

El motor SQL de Oracle es muy flexible. Se permite el uso de subconsultas en la cláusula WHERE, y HAVING, así como directamente en la cláusula FROM (se tratan como vistas) y también como forma de resolver una columna en el conjunto de resultados en la cláusula SELECT.
Lo curioso es que, dado que el formateo de filas se hace al final, de algún modo el optimizador ignora el coste de combinación de las tablas de la subconsulta, pues las trata una vez ha procesado la sentencia.
Por ejemplo, la consulta de clientes con su identificación de sexo y estado civil.
SQL> set autotrace traceonly
SQL> select nif, cn_sexo sexo, cn_ec estado
  2  from clientes, sexos, estadosciviles
  3  where clientes.ec_id_ec=estadosciviles.id_ec
  4    and clientes.sex_id_sexo=sexos.id_sexo;
9999 filas seleccionadas.
Plan de Ejecución
———————————————————-
Plan hash value: 3779357754
————————————————————————————————
| Id  | Operation                     | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
————————————————————————————————
|   0 | SELECT STATEMENT              |                | 10001 |   341K|    34   (6)| 00:00:01 |
|*  1 |  HASH JOIN                    |                | 10001 |   341K|    34   (6)| 00:00:01 |
|   2 |   TABLE ACCESS FULL           | ESTADOSCIVILES |     5 |    55 |     3   (0)| 00:00:01 |
|   3 |   MERGE JOIN                  |                | 10000 |   234K|    31   (7)| 00:00:01 |
|   4 |    TABLE ACCESS BY INDEX ROWID| SEXOS          |     2 |    16 |     2   (0)| 00:00:01 |
|   5 |     INDEX FULL SCAN           | SEX_PK         |     2 |       |     1   (0)| 00:00:01 |
|*  6 |    SORT JOIN                  |                |  9999 |   156K|    29   (7)| 00:00:01 |
|   7 |     TABLE ACCESS FULL         | CLIENTES       |  9999 |   156K|    27   (0)| 00:00:01 |
————————————————————————————————
Predicate Information (identified by operation id):
—————————————————
   1 – access(«CLIENTES».»EC_ID_EC»=»ESTADOSCIVILES».»ID_EC»)
   6 – access(«CLIENTES».»SEX_ID_SEXO»=»SEXOS».»ID_SEXO»)
       filter(«CLIENTES».»SEX_ID_SEXO»=»SEXOS».»ID_SEXO»)
Estadísticas
———————————————————-
          1  recursive calls
          0  db block gets
        108  consistent gets
          1  physical reads
          0  redo size
     346739  bytes sent via SQL*Net to client
       7846  bytes received via SQL*Net from client
        668  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       9999  rows processed
La lectura de 108 bloques en memoria corresponde mayoritariamente a los bloques de la tabla CLIENTES, al índice de clave primaria de la tabla SEXOS y al acceso completo por FULL SCAN de la tabla ESTADOSCIVILES. El coste se tasa en 34.
Esta misma consulta, expresando la resolución del valor de sexo y estado civil del cliente directamente en las columnas de resultado, suponen un plan de ejecución similar, pero tasado ligeramente inferior.
SQL> select nif,
  2         (select cn_sexo from sexos where id_sexo=clientes.sex_id_sexo) sexo,
  3         (select cn_ec from estadosciviles where id_ec=clientes.ec_id_ec) estado
  4  from clientes;
9999 filas seleccionadas.
Transcurrido: 00:00:00.07
Plan de Ejecución
———————————————————-
Plan hash value: 3745735041
———————————————————————————————-
| Id  | Operation                   | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
———————————————————————————————-
|   0 | SELECT STATEMENT            |                |  9999 |   156K|    27   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| SEXOS          |     1 |     8 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | SEX_PK         |     1 |       |     0   (0)| 00:00:01 |
|   3 |  TABLE ACCESS BY INDEX ROWID| ESTADOSCIVILES |     1 |    11 |     1   (0)| 00:00:01 |
|*  4 |   INDEX UNIQUE SCAN         | EC_PK          |     1 |       |     0   (0)| 00:00:01 |
|   5 |  TABLE ACCESS FULL          | CLIENTES       |  9999 |   156K|    27   (0)| 00:00:01 |
———————————————————————————————-
Predicate Information (identified by operation id):
—————————————————
   2 – access(«ID_SEXO»=:B1)
   4 – access(«ID_EC»=:B1)
Estadísticas
———————————————————-
          1  recursive calls
          0  db block gets
        766  consistent gets
          0  physical reads
          0  redo size
     380260  bytes sent via SQL*Net to client
       7846  bytes received via SQL*Net from client
        668  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
       9999  rows processed
Prácticamente, los costes de acceso y gestión de la tabla SEXOS y ESTADOSCIVILES parecen omitirse en el cálculo (el anterior plan medía 34, menos 3 de acceso a cada tabla y 1 de acceso al índice), pero el volumen total de bloques leídos en memoria se multiplica por siete.

Traza de ejecución con uso de joins

select nif, cn_sexo sexo, cn_ec estado
from clientes, sexos, estadosciviles
where clientes.ec_id_ec=estadosciviles.id_ec
  and clientes.sex_id_sexo=sexos.id_sexo
call     count       cpu    elapsed       disk      query    current        rows
——- ——  ——– ———- ———- ———- ———-  ———-
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch      668      0.01       0.04          1        108          0        9999
——- ——  ——– ———- ———- ———- ———-  ———-
total      670      0.01       0.04          1        108          0        9999
Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 91  (VUELOS)
Number of plan statistics captured: 1
Rows     Execution Plan
——-  —————————————————
      0  SELECT STATEMENT   MODE: ALL_ROWS
   9999   HASH JOIN
      5    TABLE ACCESS   MODE: ANALYZED (FULL) OF ‘ESTADOSCIVILES’  (TABLE)
   9999    MERGE JOIN
      2     TABLE ACCESS   MODE: ANALYZED (BY INDEX ROWID) OF ‘SEXOS’ (TABLE)
      2      INDEX   MODE: ANALYZED (FULL SCAN) OF ‘SEX_PK’ (INDEX (UNIQUE))
   9999     SORT (JOIN)
   9999      TABLE ACCESS   MODE: ANALYZED (FULL) OF ‘CLIENTES’ (TABLE)

Traza de ejecución con subconsultas en la cláusula SELECT

select nif,
       (select cn_sexo from sexos where id_sexo=clientes.sex_id_sexo) sexo,
       (select cn_ec from estadosciviles where id_ec=clientes.ec_id_ec) estado
from clientes
call     count       cpu    elapsed       disk      query    current        rows
——- ——  ——– ———- ———- ———- ———-  ———-
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch      668      0.00       0.01          0        766          0        9999
——- ——  ——– ———- ———- ———- ———-  ———-
total      670      0.00       0.01          0        766          0        9999
Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 91  (VUELOS)
Number of plan statistics captured: 1
Rows     Execution Plan
——-  —————————————————
      0  SELECT STATEMENT   MODE: ALL_ROWS
      2   TABLE ACCESS   MODE: ANALYZED (BY INDEX ROWID) OF ‘SEXOS’ (TABLE)
      2    INDEX   MODE: ANALYZED (UNIQUE SCAN) OF ‘SEX_PK’ (INDEX (UNIQUE))
      5   TABLE ACCESS   MODE: ANALYZED (BY INDEX ROWID) OF ‘ESTADOSCIVILES’ (TABLE)
      5    INDEX   MODE: ANALYZED (UNIQUE SCAN) OF ‘EC_PK’ (INDEX (UNIQUE) )
   9999   TABLE ACCESS   MODE: ANALYZED (FULL) OF ‘CLIENTES’ (TABLE)

La ejecución mediante subconsultas parece indicar al optimizador que no es necesario ordenar la tabla CLIENTES para facilitar las joins, y que el obtener por cada fila desde memoria las filas correspondientes al sexo y al estado civil resulta óptimo.
Es algo muy parecido a una ejecución en estrella. La diferencia está en el cálculo de un hash para resolver los valores de sexo y estado civil, o el acceso directo a los bloques. Aunque la segunda ejecución parezca ejecutarse en menos tiempo, hay que tener en cuenta el alto consumo de bloques en memoria, cosa que no sucedería en una ejecución en estrella. 
En este caso, la ejecución con subconsultas en la cláusula SELECT sustituyendo las joins ha supuesto una mejora del rendimiento. En la parte II publicaré un caso completamente opuesto. Mientras que los costes en el plan de ejecución aparentemente van siendo cada vez menores, el rendimiento real de las ejecuciones resulta cada vez peor.

Optimización SQL en Oracle. En venta, próximamente!

En breve estará a la venta mi libro «Optimización SQL en Oracle». 
Durante los últimos dos años he estado escribiendo este libro que resume, a mi modo de ver, todo lo que un administrador o programador debería conocer para optimizar código SQL.
En él describo cómo funciona el optimizador y cómo se comporta el servidor para escoger los mejores planes de ejecución, los aspectos a considerar para crear tablas  de diferentes tipos (tablas IOT, clusters, tablas particionadas, etc.) y lo mismo relativo a los índices. Herramientas para optimizar SQL, desde asesores a las herramientas «manuales» como explain plan, tkprof, autotrace, generación de trazas, análisis de AWR, etc.
Además, también dedico un apartado a los entornos datawarehouse, a optimización SQL de código ineficiente con casos prácticos resueltos, y un glosario completo de hints con ejemplos de su uso y «maluso», y sus consecuencias para el rendimiento.
Este libro responde preguntas y cuestiones habituales como el motivo por qué no siempre es eficiente acceder a las tablas usando índices, escenarios ineficientes, usos incorrectos de tipos de datos y sus consecuencias en la optimización, uso correcto del paralelismo, el particionamiento, las vistas materializadas, jerarquías, dimensiones, consecuencias de usar NOLOGGING, como tratar subconsultas, uso de IN y EXISTS, DISTINCT, ordenaciones, etc.
El esquema del libro es el siguiente:
Por el momento está en fase BETA, pendiente de revisión técnica. Para esta revisión cuento con dos administradores de los más fuertes de España, y vamos a asegurarnos que en las más de 300 páginas no se nos escapa un error.
Me gustaría decir, como los de Valve, «When it’s done, it’s done» como fecha de publicación, pero espero que en cosa de un par de meses pueda estar disponible a la venta.
Estoy contento porque se trata del primer libro en español que trata exclusivamente de optimización SQL y todo su universo. Muchos libros (principalmente en inglés) tratan de aspectos del rendimiento, sobre todo del motor (memoria, procesos) o se centran exclusivamente en administración o programación, pero éste es el primer libro que conozco absolutamente específico, en español, con ejemplos en español, tablas con nombres en cristiano (vuelos, reservas, etc.), sin ser una traducción de una obra en inglés o un copia/pega de partes de la documentación de Oracle.
Yo estoy satisfecho del resultado, y espero que pueda ser de utilidad. Estoy seguro de que incluso los usuarios más avanzados se sorprenderán aprendiendo cosas nuevas, o redefiniendo conceptos, o encontrando una forma práctica y accesible de resumir las funcionalidades y componentes que afectan a la eficiencia del servidor de base de datos.
Os dejo unas imágenes del libro, en fase BETA, listo para revisarlo y corregirlo antes de sacarlo a la luz.


«La query que tumba la máquina».

Hace unos años publiqué un artículo llamado «PL/SQL y ejecuciones en host» en el que describía el paso a paso para poder, desde PL/SQL, ejecutar código en el sistema operativo.

Oracle no permite que los procedimientos y funciones puedan acceder al host, pero sí permite llamadas a funciones externas implementadas con C o PASCAL, y redireccionadas como librerías mediante un objeto library.

Mi intención inicial fue la de crear un procedimiento PL/SQL que realizara un backup en caliente del servidor, realizase un export, import, o cualquier otra invocación a un ejecutable residente en el sistema operativo.

Hoy he visto una configuración similar en una base de datos en un entorno de producción, que realizan la misma implementación pero mediante una función.

create or replace
FUNCTION sysrun (syscomm IN VARCHAR2)
  RETURN BINARY_INTEGER
  AS LANGUAGE C
  NAME «sysrun»
  LIBRARY shell_lib
  PARAMETERS(syscomm string);

De este modo, la función retorna un 0 si la llamada al SHELL_LIB se ha ejecutado correctamente, o un número si ha habido un error.
Hice esta prueba, y pude constatar un mito: La consulta que tumba el servidor existe!!.
SQL> select sysrun('sudo reboot') from dual;

Broadcast message from root (Thu Jan 27 13:16:34 2011):

The system is going down for reboot NOW!

SYSTEM.SYSRUN('SUDOREBOOT')
---------------------------
                          0

Encriptación de datos desde aplicación.

En todos los ejemplos que he encontrado sobre encriptación y desencriptación de datos en Oracle, siempre se usan procedimientos PL/SQL para establecer la seguridad en la base de datos. No he encontrado un sólo ejemplo que permita hacer un insert «encriptado» y una consulta «desencriptada».

Imaginando el siguiente escenario: Cada usuario tiene una «palabra secreta» para desencriptar su propia información. En la base de datos todo se registra encriptado.

Para ello, Oracle ofrece dos paquetes:

DBMS_OBFUSCATION_TOOLKIT. A partir de Oracle8i, que soporta encriptación DES y triple DES (Data Encription Standard), y con ciertas limitaciones (por ejemplo, los datos a encriptar han de ser un múltiplo de 8 bytes).

DBMS_CRYPTO. A partir de Oracle10g. Soporta más formas de encriptación, como la AES (Advanced Encription Standard), que sustituye el anterior DES y no hay limitación con el número de carácteres.

Para mas información, la documentación de Oracle ofrece esta comparativa de funcionalidades.

Uso de DBMS_OBFUSCATION_TOOLKIT

El siguiente ejemplo muestra la encriptación de la palabra «SECRETO» (8 bytes) y genera un error al intentar encriptar «SECRETITOS!» (11 bytes)

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>SECRETO!’,key_string=>’clavedesencript’) from dual;

DBMS_OBFUSCATION_TOOLKIT.DESENCRYPT(INPUT_STRING=>’SECRETO!’,KEY_STRING=>’CLAVED
——————————————————————————–
lr??

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>SECRETITOS!’,key_string=>’clavedesencript’) from dual;
select DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’SECRETITOS!’,key_string=>’clavedesencript’) from dual
*
ERROR at line 1:
ORA-28232: invalid input length for obfuscation toolkit
ORA-06512: at «SYS.DBMS_OBFUSCATION_TOOLKIT_FFI», line 21
ORA-06512: at «SYS.DBMS_OBFUSCATION_TOOLKIT», line 126

El error ORA-28232 corresponde a la longitud inadecuada de la cadena a encriptar. ‘SECRETITOS!» tiene 11 carácteres y el paquete está limitado a múltiplos de 8 bytes. Por ejemplo, el número de una tarjeta de crédito.

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’1234567812345678′,
2 key_string=>’clavedesencript’)
3 from dual;

DBMS_OBFUSCATION_TOOLKIT.DESENCRYPT(INPUT_STRING=>’1234567812345678′,KEY_STRING=
——————————————————————————–
}??X??

De modo que la desencriptación funciona de igual modo, usando la función DESDecrypt

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESDecrypt(
2 input_string=>DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’SECRETO!’,key_string=>’CLAVE_BUENA’),
3 key_string=>’CLAVE_BUENA’) from dual;

DBMS_OBFUSCATION_TOOLKIT.DESDECRYPT(INPUT_STRING=>DBMS_OBFUSCATION_TOOLKIT.DESEN
——————————————————————————–
SECRETO!

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESDecrypt(
2 input_string=>DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’1111222233334444′,key_string=>’CLAVE_BUENA’),
3 key_string=>’CLAVE_BUENA’) from dual;

DBMS_OBFUSCATION_TOOLKIT.DESDECRYPT(INPUT_STRING=>DBMS_OBFUSCATION_TOOLKIT.DESEN
——————————————————————————–
1111222233334444

y si se utiliza una clave distinta, la información no se desencriptará adecuadamente.

SQL> select DBMS_OBFUSCATION_TOOLKIT.DESDecrypt(
2 input_string=>DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’SECRETO!’,key_string=>’CLAVE_BUENA’),
3 key_string=>’CLAVE_ERRONEA’) from dual;

DBMS_OBFUSCATION_TOOLKIT.DESDECRYPT(INPUT_STRING=>DBMS_OBFUSCATION_TOOLKIT.DESEN
——————————————————————————–
???! k

…ó producirá un error.


SQL> select DBMS_OBFUSCATION_TOOLKIT.DESDecrypt(
2 input_string=>DBMS_OBFUSCATION_TOOLKIT.DESEncrypt(input_string=>’1111222233334444′,key_string=>’CLAVE_BUENA’),
3 key_string=>’CLAVE_MALA’) from dual;
ERROR:
ORA-29275: partial multibyte character

no rows selected

Uso de DBMS_CRYPTO

El siguiente ejemplo muestra la encriptación de la palabra «SECRETITOS!» (11 bytes) usando una suite de encriptación que ya viene implementada. En concreto es la DES_CBC_PCKS5, que contiene encriptación DES, encadenamiento de cifrado de bloques y modificadores de relleno PCKS5.

Es preciso, para invocar correctamente a este paquete, realizar una conversión a RAW de las cadenas a encriptar. He utilizado para ello el paquete UTL_RAW y la función UTL_I18N.STRING_TO_RAW.

SQL> select DBMS_CRYPTO.ENCRYPT(src => UTL_I18N.STRING_TO_RAW (‘SECRETITOS!’, ‘AL32UTF8’),
2 typ => 4353,
3 key => UTL_I18N.STRING_TO_RAW (‘clavedesencript’, ‘AL32UTF8’)
4 )
5 from dual;

DBMS_CRYPTO.ENCRYPT(SRC=>UTL_I18N.STRING_TO_RAW(‘SECRETITOS!’,’AL32UTF8′),TYP=>4
——————————————————————————–
1BA7F933C2CAD0C7F4FDA685775BE0E7

Y la desencriptación de la información, con la función DECRYPT.

SQL> select UTL_RAW.cast_to_varchar2(
2 DBMS_CRYPTO.DECRYPT(
3 DBMS_CRYPTO.ENCRYPT(src => UTL_I18N.STRING_TO_RAW (‘SECRETITOS!’, ‘AL32UTF8’),
4 typ => 4353,
5 key => UTL_I18N.STRING_TO_RAW (‘clavedesencript’, ‘AL32UTF8’)
6 ),
7 typ => 4353,
8 key => UTL_I18N.STRING_TO_RAW (‘clavedesdecript’, ‘AL32UTF8’)
9 )
10 )
11 from dual;

UTL_RAW.CAST_TO_VARCHAR2(DBMS_CRYPTO.DECRYPT(DBMS_CRYPTO.ENCRYPT(SRC=>UTL_I18N.S
——————————————————————————–
SECRETITOS!

Para más información sobre las múltiples formas de encriptación y uso de claves, lo mejor es consultar la documentación del paquete DBMS_CRYPTO.

Oracle10g Data Miner.

Una de las cosas más interesantes en la explotación de una base de datos es, finalmente, la minería de éstos.

El concepto de «minería de datos» se basa en el análisis de los datos con fines predictivos, para encontrar patrones ocultos en éstos… ¿quien podría adivinar que a una determinada hora o un determinado día de la semana se consume un determinado producto? ¿o que un producto orientado a hombres (cuchillas de afeitar) pasa a ser usado por mujeres solteras de un rango de edad?

Una predicción de este tipo podría sugerir la creación de una nueva linea de producto, ofertas, etc.

Hace tiempo impartí una conferencia sobre cómo implementar un modelo de base de datos de reservas en vuelos, desde un diagrama entidad-relación concreto hasta la explotación de datos históricos en el futuro. La aplicación pasaba por varias etapas (diseño, implementación, uso/cargas de datos, paso a histórico y reporting), y aunque los datos eran cargas completamente aleatorias, sucedían ciertos patrones interesantes.

Los casados tomaban vuelos a Roma, los solteros a Milán, «Air France» viajaba con los vuelos a medio llenar, y otras compañías apenas tenían uno o dos vuelos con pérdidas…

De modo que se me ocurrió hacer una prueba de minería de datos, para buscar una predicción que no pudiera verse «a simple vista». Éste fue el resultado:

Ejemplo de Uso – Oracle Data Miner – Parte 1/2

Ejemplo de Uso – Oracle Data Miner – Parte 2/2

Al final, la predicción (absurda) sobre los datos aleatorios fue una relación de probabilidades en las cuales un determinado tipo de persona (hombre/mujer, de una edad, de una ciudad) reservaría billetes con precios superiores a la media.

Funciones Analíticas.

Hay veces en las que encuentro consultas SQL a las que llamo «query ataque de pánico», porque puedo imaginarme el sufrimiento del que la programó. Con mucha frecuencia, este tipo de consultas aparecen en casos en las que una función analítica hubiera resuelto toda la complejidad del problema.

Al detalle. Una consulta del tipo «Datos de cliente con la fecha del primer contrato, fecha de la primera cancelación de contrato, fecha del último contrato contratado, fecha de…» suele consultarse con una subconsulta para cada «fecha de…».

Éste ejemplo, o el típico «Los tres contratos más recientes, las cinco últimas cancelaciones, etc.» siempre hacen que los programadores realicen una subconsulta por cada una de las condiciones… y otra y otra y al final el rendimiento se incrementa tanto de consultar varias veces la misma tabla.

…evidentemente, la consulta SQL se ha hecho tan vasta que resulta muy complicado mantenerla.

Para esta casuística, las funciones analíticas se aplican a un subconjunto de registros, por lo que Oracle, para gestionarlo correctamente, crea una ventana SQL intermedia para reagrupar una y otra vez los resultados de una consulta. Así, dado el anterior ejemplo, Oracle tomaría todos los contratos de ese cliente y los agruparía para cada columna de resultados: el primer contrato contratado, el primer cancelado, el último contrato de alta, etc. sin necesidad de consultar una y otra vez la tabla de contratos.

Las funciones analíticas tienen la siguiente sintaxis (no es la sintaxis completa).

FUNCIÓN_ANALITICA(campo)
OVER (PARTITION BY campo_agr1, campo_agr2
ORDER BY campo_ord1 NULLS LAST)

Las funciones analíticas están descritas en la documentación de Oracle y entre éstas, por ejemplo, se hallan MAX(), MIN(), RANK(), etc.

Un ejemplo de su uso sería, por ejemplo, intentar corregir esta consulta:


SELECT     a.ID_FACTURA,                                  
           a.FALINEA_AUX – b.minCount + 1 ID_FALINEA,     
           a.ID_CLIENT,                                   
           a.ID_COMPTEFACT,                               
           a.PRODUCT_ID,                                  
           a.ID_PRCATPRODUCTE,                            
           a.DS_PRNUMSERVEI,                              
           a.ID_FACONCEPTE,                               
           a.DT_FAFACTURACIO,                             
           a.NUM_FAIMPORTCONCEPTE,                        
           a.PRODUCT_LABEL,                               
           a.DT_MOVIMENT,                                 
           a.FG_TIPUSOPERACIO,                            
           a.asset_id,                                    
           a.PRODUCT_ATTR_VALUE                           
FROM vw_ci_linia_factura_tmp a,                       
     (select t.id_factura,                                
             t.dt_fafacturacio,                           
             min(t.falinea_aux) minCount                  
      from vw_ci_linia_factura_tmp t                  
      group by t.id_factura,                              
               t.dt_fafacturacio                          
     ) b                                                  
WHERE a.id_factura = b.id_factura                         
ORDER BY a.id_factura, a.FALINEA_AUX – b.minCount + 1 ASC;

En este caso, la necesidad de obtener la primera línea de factura para cada linea de factura, parece que obliga a cargar dos veces la vista (y su conjunto de tablas) en memoria y hacer una JOIN entre ambas.

No es necesario. Los costes de ejecución se reducen a la mitad.


SELECT     a.ID_FACTURA,                     
           a.FALINEA_AUX – min(falinea_aux) over 
(partition by id_factura, dt_fafacturacio) +1 ID_FALINEA,      
           a.ID_CLIENT,                   
           a.ID_COMPTEFACT,             
           a.PRODUCT_ID,                 
           a.ID_PRCATPRODUCTE,           
           a.DS_PRNUMSERVEI,             
           a.ID_FACONCEPTE,              
           a.DT_FAFACTURACIO,            
           a.NUM_FAIMPORTCONCEPTE,       
           a.PRODUCT_LABEL,              
           a.DT_MOVIMENT,                
           a.FG_TIPUSOPERACIO,           
           a.asset_id,                   
        a.PRODUCT_ATTR_VALUE             
FROM sta_vw_ci_linia_factura_tmp a       
ORDER BY 1,2 ASC;