Archive for January, 2007

Cursor Sharing e Histogramas

January 4, 2007

Hace poco vi una base de datos que daba servicio a un Call Center y es una aplicación que se desarrolló a medida. La aplicación corre sobre Oracle 10gR2 en UNIX y el nivel de concurrencia era considerable, en horas de pico llegaba a 1500 usuarios y una media de 1000.

La base de datos tiene un monton de problemas, la maquina se ahoga por excesivo uso de CPU de la base de datos y el CPU usage no bajaba de 85% en ningún momento. Lo primero que habia que investigar era el uso de CPU. Con el AWR se veía que tenian un parsing excesivo, todos eran Hard Parsing. Mirando los TOP SQL se veía que los SQL usaban variables tipo SYS_B_0, SYS_B_1 el cual quería decir que estaba fijado el parametro CURSOR_SHARING. Se revisó que valor tenía y se vio que estaba a SIMILAR.

Hasta aqui varios puntos a tener en cuenta:

1. La aplicación no utiliza bind variables por lo tanto es un sistema que no escalaría nunca, siempre irá corto de CPU ya que todos los parsing son Hard, los mas costosos.

2. Alguien se dio cuenta y se le ocurrió la idea de poner CURSOR_SHARING. Este parametro como sabemos desde la 9i nos da tres opciones, EXACT, SIMILAR y FORCE, antes de 9i la opción de SIMILAR no existía.

3. Por qué sigue habiendo tanto Hard Parsing si estaba puesto el parametro?

Para entender la situación habría que ver que es lo que hace SIMILAR, que diferencia tiene entre SIMILAR y FORCE?

SIMILAR a diferencia de FORCE utiliza varios criterios para determinar si se debe de compartir los cursores, uno de los criterios es, si la columna del predicado de la Query tiene histogramas no compartiría los cursores, es decir si se lanzan 1000 veces una misma query pero con diferentes valores en el predicado luego en el Library Cache habrá 1000 cursores representando las 1000 queries que se ha ejecutado.

Se procedió a revisar el metodo de pasar las estadísticas en la base de datos. Como se imaginaba estaba pasando las estadísticas con histogramas en todos los campos por lo tanto no se comparte ningun cursor.

Para ver el comportamiento lo mejor es seguir un pequeño ejemplo:


lsc@lnx920-PANDO>create table t1
  2  (
  3     ID number,
  4     ANIMAL varchar2(12)
  5  )
  6  tablespace users;

Table created.

-- Generamos los datos de tal manera que las histogramas nos irian de maravilla en las Queries
lsc@lnx920-PANDO>INSERT INTO t1
  2     WITH tdata AS
  3          (SELECT ROWNUM ID
  4             FROM all_objects
  5            WHERE ROWNUM <= 30)
  6     SELECT ROWNUM,
  7            'PERRO'
  8       FROM tdata a, tdata b
  9      WHERE ROWNUM <= 900;

900 rows created.

lsc@lnx920-PANDO>INSERT INTO t1
  2     WITH tdata AS
  3          (SELECT ROWNUM ID
  4             FROM all_objects
  5            WHERE ROWNUM <= 10)
  6     SELECT ROWNUM,
  7            'GATO'
  8       FROM tdata a, tdata b
  9      WHERE ROWNUM <= 10;

10 rows created.

lsc@lnx920-PANDO>commit;

Commit complete.
 lsc@lnx920-PANDO>create index t1_i1 on t1(animal);

Index created.

-- Pasamos dbms_stats con histogramas
lsc@lnx920-PANDO>exec dbms_stats.gather_table_stats(ownname => 'LSC', tabname => 'T1',-
> estimate_percent => 20, method_opt => 'FOR COLUMNS ANIMAL SIZE 254', cascade => TRUE)

PL/SQL procedure successfully completed.

-- Revisamos v$sqlarea y vemos que no hay ningun cursor referenciando a la tabla t1
lsc@lnx920-PANDO>select sql_text, version_count
  2    from v$sqlarea
  3   where sql_text like 'select%t1%';

no rows selected

lsc@lnx920-PANDO>select count(*)
  2  from t1
  3  where animal = 'PERRO';

  COUNT(*)
----------
       900

-- Revisamos otra vez v$sqlarea y vemos que ya existe una versión de la query
lsc@lnx920-PANDO>select sql_text, version_count
  2    from v$sqlarea
  3   where sql_text like 'select%t1%';

SQL_TEXT                                           VERSION_COUNT
-------------------------------------------------- -------------
select count(*) from t1 where animal = :"SYS_B_0"              1

lsc@lnx920-PANDO>select count(*)
  2  from t1
  3  where animal = 'GATO';

  COUNT(*)
----------
        10

-- Revisamos otra vez v$sqlarea y vemos que el cursor no se comparte porque existe una versión por cada query
lsc@lnx920-PANDO>select address, sql_text, version_count
  2    from v$sqlarea
  3   where sql_text like 'select%t1%';

ADDRESS  SQL_TEXT                                           VERSION_COUNT
-------- -------------------------------------------------- -------------
5BC43B48 select count(*) from t1 where animal = :"SYS_B_0"              2

-- Con el ADDRESS podemos verificar en v$sql_shared_cursor que no existe ningún tipo de mismatch
lsc@lnx920-PANDO>select * from V$SQL_SHARED_CURSOR
  2  where KGLHDPAR = '5BC43B48'
  3  ;

ADDRESS  KGLHDPAR U S O O S L S E B P I S T A B D L T R I I R L I O S M U T N F L
-------- -------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
5BCFC0CC 5BC43B48 N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N
5B9D636C 5BC43B48 N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N N

-- Esto quiere decir que no comparte cursor por otras razones, en este caso por las histogramas

-- Verificamos el numero de los Hard Parses
lsc@lnx920-PANDO>alter system flush shared_pool;

System altered.

lsc@lnx920-PANDO>@sesstat
Nombre de usuario:  [LSC]
SID:  [11] 11
Statistic name:  [NONE] Parse

   SID USERNAME         NAME                                                                 VALUE
------ ---------------- ------------------------------------------------------------ -------------
    11 LSC              parse count (failures)                                                   0
    11 LSC              parse count (hard)                                                       0
    11 LSC              parse time cpu                                                           4
    11 LSC              parse time elapsed                                                       4
    11 LSC              parse count (total)                                                     20

-- Ejecutamos 1000 veces la misma query con SQL dinamico y con diferente valores en el predicado
lsc@lnx920-PANDO>begin
  2          for i in 1..1000
  3          loop
  4                  execute immediate 'select * from t1 where animal = ' || i;
  5          end loop;
  6  end;
  7  /

PL/SQL procedure successfully completed.

lsc@lnx920-PANDO>@sesstat
Nombre de usuario:  [LSC] .
SID:  [11] 11
Statistic name:  [NONE] Parse

   SID USERNAME         NAME                                                                 VALUE
------ ---------------- ------------------------------------------------------------ -------------
    11 LSC              parse count (failures)                                                   0
    11 LSC              parse time cpu                                                         126
    11 LSC              parse time elapsed                                                     331
    11 LSC              parse count (hard)                                                     971
    11 LSC              parse count (total)                                                   1028

-- Vemos que el numero de Hard Parses ha incrementado 971 y el total 1008

-- Revisamos el numero de versiones del cursor - 1000, una por cada query ejecutada
lsc@lnx920-PANDO>select sql_text, version_count
  2    from v$sqlarea
  3   where sql_text like 'select%t1%';

SQL_TEXT                                                         VERSION_COUNT
---------------------------------------------------------------- -------------
select * from t1 where animal = :"SYS_B_0"                                1000

La solución temporal para el cliente era o poner a FORCE el CURSOR_SHARING ó pasar estadísticas sin histogramas. Las dos soluciones son parches temporales, la definitiva es reescribir la aplicación para que use Bind Variables.

Orden de los registros y el Order By

January 3, 2007

Cuando lanzas una query y quieres que te devuelva los datos en una orden específica se utiliza la clausula Order By. Creía que esto era lógico hasta ayer, cuando un cliente empezó a quejarse de que su aplicación dejó de funcionar después de aplicar el Patchset 9.2.0.8 en su base de datos 9.2.0.1.

Los datos son de este tipo y deben ser devuelto en esta orden

[LOT]PALET.PORT[1].PROD
[LOT]PALET.PORT[1].LG_SP
[LOT]PALET.PORT[1].ETAPA
[LOT]PALET.PORT[1].TIPO_OPER

Dspues del parche esta devolviendo en esta orden

[LOT]PALET.PORT[1].ETAPA
[LOT]PALET.PORT[1].PROD
[LOT]PALET.PORT[1].TIPO_OPER
[LOT]PALET.PORT[1].LG_SP

Se quedó inutilizable la aplicación porque la lógica de la aplicación dependía del orden de inserción de los registros (a quien se le ocurre?). Primero tiene que procesar [LOT]PALET.PORT[1].PROD luego [LOT]PALET.PORT[1].LG_SP y asi sucesivamente.

Por definición Oracle no garantiza la orden de devolver los datos a no ser que se especifíca la clausula de Order By, esta claro que los que diseñaron la aplicación no tenian mucha idea.

De todas maneras el orden que necesitan tampoco se podría resolver con un Order By, no sigue ningún criterio. Problema de diseño.

Con un ejemplo podemos ver que pasaria si desarrollas tu aplicación de dicha manera:

 
-- Creamos Tablespace APEX_D con dos datafiles de 1M cada uno
SQL>create tablespace apex_d
  2  datafile
  3  '/u02/oradata/lnx920/apex_d_01.dbf' size 1M,
  4  '/u02/oradata/lnx920/apex_d_02.dbf' size 1M
  5  extent management local uniform size 128K
  6  segment space management manual;

Tablespace created.

-- Creamos una tabla de tal manera que cada registro ocupe un bloque (de 8K). Usamos el campo A como identificador incremental
SQL>create table t1
  2  (
  3     a number,
  4     b varchar2(64),
  5     c varchar2(64),
  6     d varchar2(64),
  7     e varchar2(64),
  8     f varchar2(64)
  9  )
 10  tablespace apex_d
 11  pctused 1
 12  pctfree 99;

Table created.
-- Insertamos 20 registros para que use 20 bloques
SQL>insert into t1
  2  WITH tdata AS
  3  (SELECT ROWNUM ID
  4     FROM all_objects
  5    WHERE ROWNUM <= 10)
  6  SELECT
  7     ROWNUM,
  8     DBMS_RANDOM.STRING ('U', 64),
  9     DBMS_RANDOM.STRING ('U', 64),
 10     DBMS_RANDOM.STRING ('U', 64),
 11     DBMS_RANDOM.STRING ('U', 64),
 12     DBMS_RANDOM.STRING ('U', 64)
 13    FROM tdata a, tdata b
 14   WHERE ROWNUM <= 20;

20 rows created.

SQL>commit;

Commit complete.
-- Vemos en que datafile y bloque estan ubicados cada registro
SQL>select
  2     dbms_rowid.rowid_to_absolute_fno(rowid, 'LSC', 'T1') ABS_FNO,
  3     dbms_rowid.rowid_relative_fno(rowid) REL_FNO,
  4     dbms_rowid.rowid_block_number(rowid) BLOCKNO,
  5     dbms_rowid.rowid_row_number(rowid) ROWNO,
  6     a
  7  from t1;

   ABS_FNO    REL_FNO    BLOCKNO      ROWNO          A
---------- ---------- ---------- ---------- ----------
         5          5         10          0          1
         5          5         11          0          2
         5          5         12          0          3
         5          5         13          0          4
         5          5         14          0          5
         5          5         15          0          6
         5          5         16          0          7
         5          5         17          0          8
         5          5         18          0          9
         5          5         19          0         10
         5          5         20          0         11
         5          5         21          0         12
         5          5         22          0         13
         5          5         23          0         14
         5          5         24          0         15
         6          6          9          0         16
         6          6         10          0         17
         6          6         11          0         18
         6          6         12          0         19
         6          6         13          0         20

20 rows selected.

-- Liberamos espacio de los bloques intermedios borrando 10 registros
SQL>delete t1 where a between 5 and 14;

10 rows deleted.

SQL>commit;

Commit complete.

-- Insertamos otros 10 para que reutilice el espacio liberado
SQL>insert into t1
  2  WITH tdata AS
  3  (SELECT ROWNUM ID
  4     FROM all_objects
  5    WHERE ROWNUM <= 10)
  6  SELECT
  7     ROWNUM + 20, -- Sumamos 20 para que los nuevos registros empiecen por 21 sobre el campo A
  8     DBMS_RANDOM.STRING ('U', 64),
  9     DBMS_RANDOM.STRING ('U', 64),
 10     DBMS_RANDOM.STRING ('U', 64),
 11     DBMS_RANDOM.STRING ('U', 64),
 12     DBMS_RANDOM.STRING ('U', 64)
 13  FROM tdata a, tdata b
 14  WHERE ROWNUM <= 10;

10 rows created.

SQL>commit;

Commit complete.

-- Consultamos otra vez a ver si el resultado que devuelve la query sigue el orden de insercion
SQL>select
  2     dbms_rowid.rowid_to_absolute_fno(rowid, 'LSC', 'T1') ABS_FNO,
  3     dbms_rowid.rowid_relative_fno(rowid) REL_FNO,
  4     dbms_rowid.rowid_block_number(rowid) BLOCKNO,
  5     dbms_rowid.rowid_row_number(rowid) ROWNO,
  6     a
  7  from t1;

   ABS_FNO    REL_FNO    BLOCKNO      ROWNO          A
---------- ---------- ---------- ---------- ----------
         5          5         10          0          1
         5          5         11          0          2
         5          5         12          0          3
         5          5         13          0          4
         5          5         14          1         30
         5          5         15          0         29
         5          5         16          0         28
         5          5         17          0         27
         5          5         18          0         26
         5          5         19          0         25
         5          5         20          0         24
         5          5         21          0         23
         5          5         22          0         22
         5          5         23          0         21
         5          5         24          0         15
         6          6          9          0         16
         6          6         10          0         17
         6          6         11          0         18
         6          6         12          0         19
         6          6         13          0         20

20 rows selected.

Si fijamos en el campo A vemos que aparece el 30 despues del 4 cuando el que desarrolla piensa que el valor deberia ser 15! Porque se ha reutilizado los espacios de los bloques 14 al bloque 23 del fichero 6 que se liberó con el Delete.