miércoles, 24 de noviembre de 2010

Como realizar update/delete masivos en forma efectiva

En esta nota voy a mostrarles un método efectivo para modificar o eliminar una gran cantidad de filas sobre una tabla grande. En general las tablas voluminosas se encuentran particionadas para lograr escalar en forma natural. El particionamiento principalmente provee 3 tipos de beneficios: 1) mejora la performance, 2) facilita la administración y mantenimiento y 3) incrementa la disponibilidad de los datos. Resolver una consulta usando como tabla subyacente particionada puede verse de la misma forma que resolver un problema dividiendolo en partes. La conocida premisa: divide y conquistaras es el principal objetivo detrás de particionar.

Desde que se introdujo el feature de partitioning (Oracle 8) se ha ampliado notablemente el set de operaciones posibles sobre tablas e indices para dar soporte y manejar las tablas/indices particionados. Con cada nuevo release se fueron agregando distintas opciones, métodos de particionado y operaciones para manipulación de segmentos. Los distintos features introducidas en cada release son:


Oracle 8 (1997)
  • Partition Pruning (*)
  • Range Partitioning (incluye operaciones ADD, DROP, RENAME, TRUNCATE, MODIFY, MOVE, SPLIT y EXCHANGE)

Oracle 8i (1999)
  • Particionamiento Hash
  • Particionamiento compuesto: range/hash
  • Se agregó la operación MERGE

Oracle 9i R2 (2002)
  • List Partitioning
  • Particionamiento compuesto: Range/List
  • Cláusula UPDATE GLOBAL INDEXES

Oracle 10g R1 (2004)
  • Indices globales particionados por Hash y List

Oracle 10g R2 (2005)
  • Se incremento el limite de particiones/subparticiones de 65k a 4M

Oracle 11g R1 (2007)

  • Particionamiento compuesto: range-range, list-range, list-list y list-hash.
  • Se agregó particionamiento por intervalo, por referencia y de sistema.

Oracle 11g R2 (2009)

  • Columnas virtuales como primary key para tablas particionadas referenciadas.
  • Indices particionados por sistema para tablas particionadas por lista.


Como se puede ver, practicamente en cada nuevo release hubo algún agregado de nueva funcionalidad. Sin embargo, a mi entender, el principal feature existe desde el primer release con partitioning (1997). Me refiero al partition pruning o poda de partición, que posibilita que el optimizador (siempre hablando de CBO) elija en forma automática, precisa y transparente la partición o particiones donde se encuentra los datos requeridos. Esto permite segmentar los datos y solo procesar los que nos interesan, sin tener que agregar ninguna inteligencia adicional en el código de aplicación.

Con respecto a las operaciones, la gran mayoria existen desde Oracle 8, solo se agregó tiempo después el MERGE. Una operación muy interensante es EXCHANGE, con la cual se puede intercambiar una tabla sin particionar con una partición. Justamente es esta la operación que voy a usar para proponer una alternativa rapida para cambiar o borrar gran cantidad de filas sobre tablas particionadas. A continuación, somo suelo hacer, voy a mostrar los pasos en detalle y comparar los tiempos y uso de recursos:

Voy a crear una tabla T particionada por lista con 3 particiones

create table t(c1 int,c2 varchar2(10),
c3 date,
c4 char(1))
partition by list (c4)
(
partition t_a values ('A') ,
partition t_b values ('B') ,
partition t_c values ('C')
);

Ahora voy a insertar 10M de filas distribuidas en forma arbitraria sobre las particiones:

insert into t
select rownum,
dbms_random.string('a',10),
sysdate-dbms_random.value(-100,100),
chr(trunc(dbms_random.value(65,68)))
from dual
connect by rownum <= 10000000;

Inserto 5M de filas sobre la partición en la que voy a trabajar para tener mas filas:

insert into t
select rownum+10000000,
dbms_random.string('a',10),
sysdate-dbms_random.value(-100,100),
'A' from dual
connect by rownum <= 5000000;

Luego de cargados todos los valores se confirman (commit) y luego se recolectan estadisticas.
Veamos el plan para una consulta que cuenta filas sobre la partición 1 (t_a):

explain plan for
select count(1)
from t where c2 > 'R' and c4 = 'A';

select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------
Plan hash value: 2901716037

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 13 | 8455 (3)| 00:03:04 | | |
| 1 | SORT AGGREGATE | | 1 | 13 | | | | |
| 2 | PARTITION LIST SINGLE| | 5588K| 69M| 8455 (3)| 00:03:04 | 1 | 1 |
|* 3 | TABLE ACCESS FULL | T | 5588K| 69M| 8455 (3)| 00:03:04 | 1 | 1 |
-----------------------------------------------------------------------------------------------

Claramente se observa que el optimizador solo accedió la partición 1. Ejecutando la consulta vemos que la estimación del optimizador fue buena:


select count(1)
from t
where c4 = 'A' and c2 > 'R';



COUNT(1)
----------
5610297

El total de filas de la partición es:

select count(1)
from t
where c4 = 'A' ;

COUNT(1)
----------
8333946

En este punto, ya tenemos una partición con mas de 8.3M de filas de las cuales vamos a modificar 5.6M, lo cual es mas del 67%.
Primero voy a testear un update normal sobre la tabla T para luego realizar la comparativa con la misma modificación pero usando otro enfoque mas eficiente.


update t
set c3 = c3+1
where c4 = 'A'
and c2 > 'R'

5610297 filas actualizadas.

Transcurrido: 00:04:45.37

La modificación demoró 4' 45". Pensemos que la base de datos debe mantener la consistencia para garantizar la lectura consistente (mediante el UNDO) y persistir los cambios para poder recuperarse si un evento de falla ocurre durante la modificación (REDO). Estos mecanismos provocan que los tiempos se incrementen y se genere información adicional.

Revisemos cuanto espacio de UNDO y REDO se necesitó para realizar el update:

select 'REDO_SIZE',
round(ms.value/1024/1024) value
from v$mystat ms,
v$statname sn
where ms.STATISTIC# = sn.STATISTIC#
and sn.NAME = 'redo size'
union all
SELECT 'UNDO_SIZE',
t.used_ublk*8/1024 value
FROM v$transaction t, v$session s
WHERE t.addr = s.taddr
AND s.audsid = userenv('sessionid')

REDO_SIZE 2489 Mb
UNDO_SIZE 885 Mb

Para modificar 5.3M se necesitaron 2489Mb de redo y 885Mb de undo!!!. En el ejemplo, la tabla no tiene indices. Si tuviera indices y la columna modificada sea parte de las columnas de indexación se generaría mas redo y undo, y además la sentencia tendría que actualizar los indices por cada fila modificada lo cual provocaría que el update demore bastante mas. Si el procesamiento masivo fuera un delete en lugar de un update, se generará mas undo (el delete es la operación dml que mas undo genera) y se tendrá que mantener balanceados los indices, lo cual implica mas tiempo de procesamiento.

Existe una forma mas sencilla de realizar el update usando la operación estrella de partitioning: EXCHANGE. Antes de usar el exchange tenemos que crear una tabla auxiliar (T_A) y para acelerar la creación configuro la tabla como nologging e inserto en forma directa usando el hint APPEND.
create table t_a_aux nologging as
select /*+ APPEND */
c1,
c2,
case when (c2>'R') then c3+1
else c3 end c3,
c4
from t
where c4 = 'A'

Transcurrido: 00:00:22.04

Solo se necesitaron 22" para insertar la filas en la tabla auxiliar. Con la función DECODE o CASE realizo el cambio simulando el update. Ahora solo resta realizar el intercambio entre la tabla auxiliar y la partición t_a con la operación EXCHANGE:

ALTER TABLE t
EXCHANGE PARTITION t_a
WITH table t_a_aux ;

Transcurrido: 00:00:11.46

El exchange se realizó en casi 12". Sumando la creación de la tabla auxiliar y el exchange, todo demoró solo 44"!!!, es decir mas de 6 veces mas rapido que el update tradicional.
Ejecutando la consulta para obtener el espacio de redo y undo generado se obtiene:
REDO_SIZE     1 Mb
UNDO_SIZE 0 Mb
Practicamente no hubo alocación de undo/redo. Por lo cual, para ciertos casos resulta muy util usar este metodo para actualizar dado que los tiempos de procesamiento se reducen sensiblemente y ademas los requerimientos de undo y redo son minimizados casi por completo.

Para eliminar (delete) en forma masiva, la creación de la tabla auxiliar solo deberá llenarse con las filas que no se borran. Si se necesitara borrar muchas filas de una tabla no particionada se podrá utilizar el mismo enfoque, es decir reemplazar el delete por un insert en una tabla nueva, recrear los indices y renombrar.

miércoles, 17 de noviembre de 2010

Reportes de Métricas de Carga y Tiempos de Respuesta de la Base de Datos (10g+)

A partir de 10g se agregaron vistas dinamicas e información historica para poder entender mejor y en forma mas rapida la actividad de la base de datos. Si bien los reportes de statspack y AWR tienen la información, estos se basan de los snapshots como referencia para analizar un intervalo. Generalmente los intervalos son de 1 hora (automatico y default en 10g+) y muchas veces hay que esperar al próximo snapshot para tener una idea de la actividad actual.

Con las nuevas vistas dinamicas se puede saber casi en tiempo real cual es la actividad de la base consultando las siguientes vistas dinamicas:

V$SYSMETRIC : Metricas mas recientes y menos recientes del ultimo minuto
(una muestra cada 15").
V$SYSMETRIC_HISTORY : Ultima hora de todas las muestras (elige una muestra por
minuto).
V$SYSMETRIC_SUMMARY : Resumen de la actividad de la ultima hora (maximos, minimos,
promedios y desviación standard).


y las vistas historias que persisten parte de la información de las vistas dinamicas
de la ultima hora (el proceso MMON se encarga de copiar parte de la información mas relevante de las vistas V$ a disco) y se externaliza el resultado con las siguientes vistas:

DBA_HIST_SYSMETRIC_HISTORY
DBA_HIST_SYSMETRIC_SUMMARY

Con esta información a disposición se obtiene una idea muy detallada de la actividad y el perfil de carga. Veamos una query que usa la vista sumarizada y retorna entre otros, los mismos datos que encontramos en los reportes statspack/awr en la parte "Load Profile" en la columa tabulada por segundo:

select metric_name,
case (metric_id)
when 2016 then round(minval/1024/1024,2)
when 2058 then round(minval/1024/1024,2)
else round(minval,2) end Min,
case (metric_id)
when 2016 then round(maxval/1024/1024,2)
when 2058 then round(maxval/1024/1024,2)
else round(maxval,2) end Max,
case (metric_id)
when 2016 then round(average/1024/1024,2)
when 2058 then round(average/1024/1024,2)
else round(average,2) end Avg,
case (metric_id)
when 2016 then round(standard_deviation/1024/1024,2)
when 2058 then round(standard_deviation/1024/1024,2)
else round(standard_deviation,2) end STDDEV,
case (metric_id)
when 2016 then 'Mbytes Per Second'
when 2058 then 'Mbytes Per Second'
else metric_unit end metric_unit
from v$sysmetric_summary
where metric_id in (2003,2026.2004,2006,2016,2018,2030,
2044,2046,2058,2071,2075,2081,2123)
order by metric_id



METRIC_NAME MIN MAX AVG STDDEV METRIC_UNIT
---------------------------------------------------------------- ---------- ---------- ---------- ---------- ----------------------------------------------------------------
User Transaction Per Sec 0 28.75 24.4 1.64 Transactions Per Second
Physical Writes Per Sec 0 29.63 21.91 2.25 Writes Per Second
Redo Generated Per Sec 0 .06 .05 0 Mbytes Per Second
Logons Per Sec 0 1.18 .93 .08 Logons Per Second
Logical Reads Per Sec 0 1015.72 592.05 75.19 Reads Per Second
Total Parse Count Per Sec 0 52.75 28.29 4.33 Parses Per Second
Hard Parse Count Per Sec 0 7.24 1.29 .88 Parses Per Second
Network Traffic Volume Per Sec 0 .03 .02 0 Mbytes Per Second
DB Block Changes Per Sec 0 339.75 287.39 19.15 Blocks Per Second
CPU Usage Per Sec 0 11.27 9.88 .51 CentiSeconds Per Second
User Rollback UndoRec Applied Per Sec 0 .3 .03 .07 Records Per Second
Database Time Per Sec 0 72.48 26.67 8.1 CentiSeconds Per Second



Los datos anteriores se pueden obtener por transacción si se llegara a necesitar.

Ahora voy a mostrar como obtener las metricas basadas en percentiles, con ratios y porcentajes de las ultimas dos muestras del ultimo minuto. La mas reciente es de a lo sumo 15 segundos y la mas antigua es de a lo sumo 60 segundos.


select metric_name,
round(value,2) value,
metric_unit
from v$sysmetric
where metric_name like '%\%%' escape '\'
or metric_name like '%Percent%'
or metric_name like '%Ratio%'


METRIC_NAME VALUE METRIC_UNIT
---------------------------------------------------------------- ---------- ----------------------------------------------------------------
Buffer Cache Hit Ratio 95.75 % (LogRead - PhyRead)/LogRead
Memory Sorts Ratio 100 % MemSort/(MemSort + DiskSort)
Redo Allocation Hit Ratio 100 % (#Redo - RedoSpaceReq)/#Redo
User Commits Percentage 100 % (UserCommit/TotalUserTxn)
User Rollbacks Percentage 0 % (UserRollback/TotalUserTxn)
Cursor Cache Hit Ratio 232.71 % CursorCacheHit/SoftParse
Execute Without Parse Ratio 63.74 % (ExecWOParse/TotalExec)
Soft Parse Ratio 96.05 % SoftParses/TotalParses
User Calls Ratio 33.48 % UserCalls/AllCalls
Host CPU Utilization (%) 4.63 % Busy/(Idle+Busy)
PX downgraded 1 to 25% Per Sec 0 PX Operations Per Second
PX downgraded 25 to 50% Per Sec 0 PX Operations Per Second
PX downgraded 50 to 75% Per Sec 0 PX Operations Per Second
PX downgraded 75 to 99% Per Sec 0 PX Operations Per Second
User Limit % 0 % Sessions/License_Limit
Database Wait Time Ratio 42.12 % Wait/DB_Time
Database CPU Time Ratio 57.88 % Cpu/DB_Time
Row Cache Hit Ratio 99.75 % Hits/Gets
Row Cache Miss Ratio .25 % Misses/Gets
Library Cache Hit Ratio 98.1 % Hits/Pins
Library Cache Miss Ratio 1.9 % Misses/Gets
Shared Pool Free % 91.27 % Free/Total
PGA Cache Hit % 99.89 % Bytes/TotalBytes
Process Limit % 24.7 % Processes/Limit
Session Limit % 16.86 % Sessions/Limit
Streams Pool Usage Percentage 0 % Memory allocated / Size of Streams pool
Buffer Cache Hit Ratio 96.18 % (LogRead - PhyRead)/LogRead
Memory Sorts Ratio 100 % MemSort/(MemSort + DiskSort)
Execute Without Parse Ratio 64.59 % (ExecWOParse/TotalExec)
Soft Parse Ratio 95.72 % SoftParses/TotalParses
Host CPU Utilization (%) 4.39 % Busy/(Idle+Busy)
Database CPU Time Ratio 15.8 % Cpu/DB_Time
Library Cache Hit Ratio 97.64 % Hits/Pins
Shared Pool Free % 91.28 % Free/Total


Otra consulta que suelo usar es mas simple y solo me retorna el tiempo de respuesta general y el tiempo de respuesta por transacción, ambos en segundos, y asi se puede analizar rapidamente y detectar si algo esta pasando con la base. Yo tengo idea de los tiempos razonables para cada base y si veo algo que se dispara me doy cuenta mirando solo esos dos valores. Abajo muestro como es la consulta que utilizo y la salida de la misma.

select end_time,
round(max(decode(metric_id,2106,value/100,null)),4) "SQLRTime",
round(max(decode(metric_id,2109,value/100,null)),4) "RTime/Trx"
from v$sysmetric_history
where metric_id in (2106,2109)
and end_time > sysdate-10/24/60
group by end_time
order by end_time desc


END_TIME SQLRTime RTime/Trx
18/11/2010 12:15:34 p.m. 0.0013 0.0172
18/11/2010 12:14:33 p.m. 0.0063 0.0843
18/11/2010 12:13:33 p.m. 0.0087 0.1101
18/11/2010 12:12:33 p.m. 0.0039 0.1147
18/11/2010 12:11:34 p.m. 0.009 0.1214
18/11/2010 12:10:34 p.m. 0.0062 0.1145
18/11/2010 12:09:34 p.m. 0.0079 0.1102
18/11/2010 12:08:34 p.m. 0.0081 0.1167
18/11/2010 12:07:34 p.m. 0.0085 0.1112
18/11/2010 12:06:34 p.m. 0.0078 0.1141


Si quisiera ver la historia mas antigua o necesito armar un reporte historico sumarizado y/o agrupado por hora, dia, semana o mes se puede usar una vista historica (DBA_HIST_xxx).

Aplicación de Parche 11.2.0.2 (no tan parche)

La semana pasada tuve que aplicar el parche 11.2.0.2 sobre un equipo de desarrollo. Cuando entré a metalink y busqué el parche que aplicaba a mi SO (Solaris SPARC) me llamó la atención el tamaño del parche. Los ultimos parches que instalé recuerdo que no pesaban mucho mas de 1Gb. El parche de 11.2.0.2 sobre la plataforma que necesitaba pesa 5.1Gb!, y para AIX mas de 6Gb. Recien una vez que leí la documentación entendí el porque. El tema es que Oracle cambió la politica de aplicación de parches a partir de 11.2.0.2. Ahora no son mas incrementales, sino totales y ademas contienen todo el bundle, es decir el server, cliente, gateway, grid, etc.

Recuerdo que en 9i venia todo junto, si querias instalar solo un cliente necesitabas bajarte 3 archivos que contenian todo, lo cual resultaba engorroso. En 10g independizaron las instalaciones de cliente, server, grid, etc. Ahora parece que nuevamente hay que bajar todo y luego elegir en la instalación lo que necesitamos.
Es obligatorio usar un home separado para la instalación, al ser total no se puede parchear sobre el home actual. En mi caso ese nuevo requisito no me molesta ya que siempre considero como una buena practica instalar los parches sobre una copia en un home nuevo, para minimizar riesgos por si el parche falla a la mitad de la instalación y la vuelta atrás requiere respaldar los binarios anteriores. Si estan muy cortos de espacio, se complica un poco crear un home separado asi que en ese caso habrá que bajar las bases, respaldar los binarios, borrarlos e instalar el nuevo home.

La instalación sobre Solaris SPARC no tuvo contratiempos. Tuve que upgradear un Oracle 11g R1 (11.1.0.7) y un Oracle 11g R2 (11.2.0.1). Ambas actualizaciones se realizaron perfectamente y sin ningun contratiempo.