Recientemente ha caído en mis manos un código PL/SQL (en forma de ‘package’) en el cual se utilizaban profusamente ‘hints’ /*+ APPEND */ en varias partes del mismo, seguidos -claro- de los correspondientes ‘COMMIT’. Esto (hacer ‘commit’ en medio de procesos), dentro de un paquete/procedimiento es algo que, de entrada, repele mis más primarios instintos. Como quiera que pregunté al programador en cuestión y me dijo que lo hacía ‘por rendimiento’, llegué a la conclusión de que muchas veces ciertos programadores ‘oyen campanas y no saben dónde’. Él había oído que así los ‘INSERTS’ corren más, pero no conocía todos los aspectos que, en un entorno real (Base de Datos OLTP con múltiples usuarios), pueden provocar efectos muy perjudiciales.
Vamos a intentar ver las peculiaridades de este ‘hint’ para poder decidir cuando utilizarlo y cuando no.
Para empezar, hay que tener claro qué hace exactamente este ‘hint’: /*+ APPEND */ inserta los datos en los segmentos utilizando bloques nuevos por encima del HWM (High WaterMark). Esto hace que se evite la utilización de segmentos de ‘ROLLBACK/UNDO’, ya que no hay posibidad de que otro usuario registre actividad sobre los mismos. A esto se le llama ‘DIRECT PATH‘. Aquí es (sobre todo si se suma a otros aspectos que pueden también eliminar REDO, como el uso de ‘NOLOGGING’) donde se consigue una mejora en el rendimiento. Esto viene de perlas en el caso de cargas masivas de tablas.
Pero ¿qué pasa si lo utilizamos en un entorno multiusuario (como nuestro amigo el programador lo hacía en mitad de la utilización normal de una base de datos OLTP)?
Como siempre, crearemos una de nuestras tablas de pruebas:
carlosal@db01.xxxxxx> SELECT * FROM V$VERSION
2 /
BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - 64bi
PL/SQL Release 10.2.0.1.0 - Production
CORE 10.2.0.1.0 Production
TNS for Solaris: Version 10.2.0.1.0 - Production
NLSRTL Version 10.2.0.1.0 - Production
carlosal@db01.xxxxxx> CREATE TABLE PRUEBA07 AS SELECT *
2 FROM DBA_OBJECTS WHERE 1=0
3 /
Tabla creada.
carlosal@db01.xxxxxx>
Ahora insertamos filas e inmediatamente intentamos acceder a la tabla (contando cuantas filas tiene)
Primero sin ‘hint’:
carlosal@db01.xxxxxx> INSERT INTO PRUEBA07 SELECT * FROM DBA_OBJECTS
2 /
11174 filas creadas.
carlosal@db01.xxxxxx> SELECT COUNT('X') FROM PRUEBA07
2 /
COUNT('X')
----------
11174
carlosal@db01.xxxxxx> ROLLBACK
2 /
Rollback terminado.
carlosal@db01.xxxxxx>
Ningún problema. Vamos ahora a utilizar el ‘hint’:
carlosal@db01.xxxxxx> INSERT /*+ APPEND */ INTO PRUEBA07
2 SELECT * FROM DBA_OBJECTS
3 /
11174 filas creadas.
carlosal@db01.xxxxxx> SELECT COUNT('X') FROM PRUEBA07
2 /
SELECT COUNT('X') FROM PRUEBA07
*
ERROR en línea 1:
ORA-12838: cannot read/modify an object after modifying it in parallel
carlosal@db01.xxxxxx> COMMIT
2 /
Confirmación terminada.
carlosal@db01.xxxxxx> SELECT COUNT('X') FROM PRUEBA07
2 /
COUNT('X')
----------
11174
¡Ups! Cuando intentamos acceder a la tabla ANTES DE HACER UN COMMIT, nos aparece el ORA-12838. Hasta que no hacemos un ‘commit’ las cosas no funcionan como esperamos.
La explicación es que al utilizar el ‘hint’ y ’saltarnos’ en ROLLBACK/UNDO estamos imposibilitando el famoso READ COMMITED, por el cual veremos los datos de nuestra transacción en modo aislado. Cuando hacemos el ‘commit’ volvemos al ‘estado normal de las cosas’ y sólo entonces podemos ver que tenemos 11174 filas en la tabla. Esto explica por qué nuestro programador intercalaba ‘commits’ dentro de su procedimiento. Si no, no tendría visibilidad sobre los datos insertados.
Este es uno de los aspectos ‘peliagudos’ del uso de /*+ APPEND */, pero hay más.
Supongamos nuevas inserciones sobre una tabla existente sobre la cual se registra activdad de otros usuarios. En un insert ‘normal’ no habrá problemas de concurrencias. Si hacemos un ‘insert’ masivo mientras otros usuarios modifican datos, la base de datos lo asume ‘como si nada’:
Sesión 1:
carlosal@db01.xxxxxx> INSERT INTO PRUEBA07 SELECT * FROM DBA_OBJECTS
2 /
11174 filas creadas.
carlosal@db01.xxxxxx>
Nótese que no hemos hecho el ‘commit’, por lo que la transacción está activa.
En la sesión 2:
carlosal@db01.xxxxxx> SELECT MIN (OBJECT_ID), MAX(OBJECT_ID) FROM PRUEBA07
2 /
MIN(OBJECT_ID) MAX(OBJECT_ID)
-------------- --------------
2 44228
carlosal@db01.xxxxxx> UPDATE PRUEBA07
2 SET OBJECT_ID = 44226 WHERE OBJECT_ID =2
3 /
1 fila actualizada.
carlosal@db01.xxxxxx> ROLLBACK
2 /
Rollback terminado.
carlosal@db01.xxxxxx>
La sesión 2 puede funcionar sin novedad. Pero si la sesión 1 utiliza el famoso ‘hint’ tenemos que:
Sesión 1:
carlosal@db01.xxxxxx> INSERT /*+ APPEND */ INTO PRUEBA07
2 SELECT * FROM DBA_OBJECTS
3 /
11174 filas creadas.
carlosal@db01.xxxxxx>
Otra vez hemos dejado la transacción pendiente.
Sesión 2:
carlosal@db01.xxxxxx> UPDATE PRUEBA07 SET OBJECT_ID = 44226
2 WHERE OBJECT_ID =2
3 /
_
Ahora la sesión 2 se queda esperando. La sesión 1 HA BLOQUEADO LA TABLA para poder efectuar su ‘insert’ en ‘DIRECT PATH’, y hasta que no concluya su transacción, la tabla permanecerá bloqueada.
Ni que decir tiene que en un entorno multiusuario esto es el típico ‘peor caso posible’.
En resumen:
Yo no soy muy partidario de los ‘hints’ en general (aunque siempre hay excepciones, claro), pero este ‘hint’ en particular puede ser especialmente dañino. /*+ APPEND */ debería usarse casi exclusivamente en herramientas -’scripts’- de carga masiva de datos y sólo bajo determinadas circunstancias (principalmente cuando tenemos un entorno ‘aislado’ en el cual no hay más usuarios accediendo a la tabla sobre la que estamos insertando datos).
Por otra parte, bien utilizado en esos casos, su uso puede dar rendimientos espectaculares.
Pero siempre -siempre- sabiendo exactamente qué estamos haciendo. Y por qué.
Saludos.
Carlos.