Thursday, April 12, 2012

Como determinar tiempo transcurrido en milisegundos

Se puede usar SYSDATE para realizar cálculos de rendimiento. Un programador que sólo tenga acceso a SQL*Plus usaría SYSDATE antes y después de llamar a una función para medir su rendimiento. Sin embargo, hay un problema... la máxima precisión de SYSDATE es en "segundos".

Existen situaciones donde se requiere mayor precisión, en escala de milisegundos. Un ejemplo es medir el rendimiento de una función que, aunque la medición tradicional retorne que duró 0 segundos (usando SYSDATE), resulta que realizando aproximadamente 400000 llamadas en ambiente productivo la aplicación tarda 10 minutos en responder. Se requiere determinar el rendimiento en milisegundos de la función para ver donde está ocurriendo el lapso que retrasa el rendimiento.

A partir de Oracle 9i se introduce el comando SYSTIMESTAMP, cuya precisión máxima es en milisegundos.


SELECT SYSTIMESTAMP FROM DUAL ; 


Resultado:



SQL> SELECT SYSTIMESTAMP FROM DUAL ;

SYSTIMESTAMP
---------------------------------------------------------------------------
12-APR-12 05.16.38.678190 AM -04:30

SQL>


Ejemplo de análisis de rendimiento con SYSDATE (para analizar rendimientos que superan 1 segundo):


SQL> CREATE PROCEDURE pesimo IS 
 v_ineficiente NUMBER ; 
BEGIN

 -- Tabla con 14809 registros y ponemos a 
 -- trabajar el servidor para que
 -- cuente 219306481 registros
 SELECT COUNT(*) 
   INTO v_ineficiente 
   FROM tabla, tabla
 ;
 
END ; 
/

Procedure created.

SQL> SET SERVEROUTPUT ON ;
SQL> DECLARE
  2     v_d1 DATE ;
  3     v_d2 DATE ;
  4     v_n  NUMBER ;
  5  BEGIN
  6     v_d1 := SYSDATE ;
  7     pesimo ;
  8     v_d2 := SYSDATE ;
  9     v_n := ( v_d2-v_d1 ) * 86400 ;
 10     DBMS_OUTPUT.PUT_LINE( 'Tiempo transcurrido : '
 11          ||v_n|| ' segundos'  ) ;
 12  END ;
 13  /
Tiempo transcurrido : 92 segundos

PL/SQL procedure successfully completed.

Elapsed: 00:01:31.09
SQL>


Ejemplo de análisis de rendimiento con SYSTIMESTAMP (cuando se requiere milisegundos de precisión):



CREATE PROCEDURE rapido IS 
 v_eficiente NUMBER ; 
BEGIN

        -- Tabla con 51500 registros
 SELECT COUNT(*) 
   INTO v_eficiente 
   FROM tabla_rapida
  WHERE fecha = 20120110
 ;
 
END ; 
/

Procedure created.

SQL> SET SERVEROUTPUT ON ;
SQL> DECLARE
  2     v_d1 TIMESTAMP ;
  3     v_d2 TIMESTAMP ;
  4     v_n  INTERVAL DAY TO SECOND ;
  5  BEGIN
  6     v_d1 := SYSTIMESTAMP ;
  7     rapido ;
  8     v_d2 := SYSTIMESTAMP ;
  9     v_n := v_d2-v_d1 ;
 10     DBMS_OUTPUT.PUT_LINE( 'Tiempo transcurrido : '
 11       ||EXTRACT( SECOND FROM v_n )|| ' segundos'  ) ;
 12  END ;
 13  /
Tiempo transcurrido : .064362 segundos

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.01
SQL>


En un escenario donde no se dispone de recursos DBA (por cualquier motivo) y el usuario oracle asignado no permite ver los planes de ejecución ni el costo de consumo de recursos y se requiere una solución inmediata, entonces el programador puede deducir que cualquier query/función/procedimiento que tarde por ejemplo 0.02 segundos en retornar, por muy rápida que parezca, no se puede ejecutar 400000 veces y pretender que dure menos de 30 segundos en el total de todas sus ejecuciones (tardaba 10 minutos en el momento que se me asignó el módulo). Para lograr el objetivo se debían entonar los 15 queries que se ejecutan dentro de la función por cada llamada(algunos con más de 9 tablas cruzadas y subqueries en sus cláusulas FROM y WHERE). La solución fue analizarlas con varios SYSTIMESTAMP en cada instrucción porque SYSDATE siempre devolvería 0 segundos.

Gracias a esta técnica se pudo determinar cuales eran las tablas que no estaban operando adecuadamente y se entonaron hasta que se logró un tiempo menor a 0.001 segundos por query, al punto que al momento de realizar las 400000 llamadas retornaba en 32 segundos.


Generación de Series Numéricas y Combinaciones

Para generar series numéricas se puede combinar la tabla DUAL con la cláusula CONNECT BY LEVEL si lo estructuramos correctamente. 

Anexo algunos ejemplos que les será de utilidad.

Rango de números, desde 181 al 200 :


SELECT * 
  FROM ( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=200 ) x 
 WHERE x.x>180
;


Resultados:


         X
----------
       181
       182
       183
       184
       185
       186
       187
       188
       189
       190
       191
       192
       193
       194
       195
       196
       197
       198
       199
       200

20 rows selected.


Traer todos los meses del año en curso (2012):


SELECT TO_DATE(
   TO_CHAR( TRUNC( SYSDATE ), 'YYYY' )||LPAD( X, 2, '0' )||'01' , 'YYYY-MM-DD'
             ) mes FROM ( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL 
                      CONNECT BY LEVEL<=12 )
;


Resultado:


MES
---------
01-JAN-12
01-FEB-12
01-MAR-12
01-APR-12
01-MAY-12
01-JUN-12
01-JUL-12
01-AUG-12
01-SEP-12
01-OCT-12
01-NOV-12
01-DEC-12


Generar todos los días del año:


SELECT 
    TO_CHAR( TO_DATE( TO_CHAR( SYSDATE, 'YYYY' )||'0101', 'YYYYMMDD' )-1+X , 
    'YYYYMMDD' ) dia FROM ( 
SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=366 )
 WHERE TO_CHAR( TO_DATE( TO_CHAR( SYSDATE, 'YYYY' )||'0101', 'YYYYMMDD' )-1+X, 
       'YYYY' ) = TO_CHAR( SYSDATE, 'YYYY' ) 
;

Resultado:

DIA
--------
20120101
20120102
20120103
20120104
...
20121225
20121226
20121227
20121228
20121229
20121230
20121231

366 rows selected.


Todos los días de otro año sin importar si es bisiesto o no, ejemplo 2011:


SELECT 
    TO_CHAR( TO_DATE( '20110101', 'YYYYMMDD' )-1+X , 'YYYYMMDD' 
           ) dia FROM ( 
SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=366 )
 WHERE TO_CHAR( TO_DATE( '20110101', 'YYYYMMDD' )-1+X, 'YYYY' ) = '2011'
;

Resultado:

DIA
--------
20110101
20110102
20110103
20110104
...
20111225
20111226
20111227
20111228
20111229
20111230
20111231

365 rows selected.


Generar Combinaciones en Binario de 4 bits. La uso bastante para generar casos de prueba en funciones/procedimientos:


SELECT c4.x-1 c4, c3.x-1 c3, c2.x-1 c2, c1.x-1 c1 FROM 
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=2 ) c1,
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=2 ) c2,
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=2 ) c3,
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=2 ) c4
;

Resultado:

        C4         C3         C2         C1
---------- ---------- ---------- ----------
         0          0          0          0
         0          0          0          1
         0          0          1          0
         0          0          1          1
         0          1          0          0
         0          1          0          1
         0          1          1          0
         0          1          1          1
         1          0          0          0
         1          0          0          1
         1          0          1          0
         1          0          1          1
         1          1          0          0
         1          1          0          1
         1          1          1          0
         1          1          1          1

16 rows selected.

Generar Combinaciones con otras tablas, por ejemplo Países en tabla "x" y tres letras A,B,C por cada país y por cada 2 sets:


SELECT c3.x c3, c2.x PAIS, CHR( 64+c1.x ) OP FROM 
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=3 ) c1,
( SELECT * FROM x /*+ INDEX( CBRA )*/ ) c2,
( SELECT ROWNUM x /*+ INDEX( CBRA )*/ FROM DUAL CONNECT BY LEVEL<=2 ) c3
;


Resultado:


        C3 PAIS       OP
---------- ---------- --
         1 Venezuela  A
         1 Venezuela  B
         1 Venezuela  C
         1 Panamá     A
         1 Panamá     B
         1 Panamá     C
         1 Chile      A
         1 Chile      B
         1 Chile      C
         1 Argentina  A
         1 Argentina  B
         1 Argentina  C
         1 España     A
         1 España     B
         1 España     C
         1 EEUU       A
         1 EEUU       B
         1 EEUU       C
         2 Venezuela  A
         2 Venezuela  B
         2 Venezuela  C
         2 Panamá     A
         2 Panamá     B
         2 Panamá     C
         2 Chile      A
         2 Chile      B
         2 Chile      C
         2 Argentina  A
         2 Argentina  B
         2 Argentina  C
         2 España     A
         2 España     B
         2 España     C
         2 EEUU       A
         2 EEUU       B
         2 EEUU       C

36 rows selected.
 

Autoejecutar comandos cuando se abre SQL*Plus

En la ruta donde se instaló Oracle ( $ORACLE_HOME ), existe una carpeta llamada sqlplus que contiene las carpetas admin, demo, doc, mesg.... Dentro de la carpeta admin existe un archivo glogin.sql. El archivo glogin.sql contiene comandos que siempre son ejecutados cada vez que se llama SQL*Plus (sea la versión windows o consola). A continuación un ejemplo del contenido del archivo glogin.sql de un cliente Oracle 9i:


--
-- Copyright (c) Oracle Corporation 1988, 2000.  All Rights Reserved.
--
-- NAME
--   glogin.sql
--
-- DESCRIPTION
--   SQL*Plus global login startup file.
--
--   Add any sqlplus commands here that are to be executed when a user
--   starts SQL*Plus on your system
--
-- USAGE
--   This script is automatically run when SQL*Plus starts
--

-- For backward compatibility
SET PAGESIZE 14
SET SQLPLUSCOMPATIBILITY 8.1.7

-- Used by Trusted Oracle
COLUMN ROWLABEL FORMAT A15

-- Used for the SHOW ERRORS command
COLUMN LINE/COL FORMAT A8
COLUMN ERROR    FORMAT A65  WORD_WRAPPED

-- Used for the SHOW SGA command
COLUMN name_col_plus_show_sga FORMAT a24

-- Defaults for SHOW PARAMETERS
COLUMN name_col_plus_show_param FORMAT a36 HEADING NAME
COLUMN value_col_plus_show_param FORMAT a30 HEADING VALUE

-- Defaults for SET AUTOTRACE EXPLAIN report
COLUMN id_plus_exp FORMAT 990 HEADING i
COLUMN parent_id_plus_exp FORMAT 990 HEADING p
COLUMN plan_plus_exp FORMAT a60
COLUMN object_node_plus_exp FORMAT a8
COLUMN other_tag_plus_exp FORMAT a29
COLUMN other_plus_exp FORMAT a44

-- [CBRA][20120412] Las siguientes lineas son personalizadas --
SET TIMING ON ; 
SET LINES 1500 ; 




En el ejemplo anterior, se agrega SET TIMING ON para que cada query imprima, además de su reporte, cuanto tiempo transcurrió durante su ejecución. Adicional se agrega SET LINES 1500 para aquellos reportes que excedan 80 líneas no se desplacen a la siguiente línea.

 Ejecutando SQL*Plus con la modificación realizada en glogin.sql:


SQL*Plus: Release 9.2.0.1.0 - Production on Thu Apr 12 04:50:47 2012

Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.


Connected to:
Oracle9i Enterprise Edition Release 9.2.0.8.0 - 64bit Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.8.0 - Production

SQL> SELECT SYSDATE FROM DUAL ;

SYSDATE
---------
12-APR-12

Elapsed: 00:00:00.00
SQL> SELECT COUNT(*) FROM compras ;

  COUNT(*)
----------
   5419959

Elapsed: 00:00:23.08
SQL>

Nótese que aparece "Elapsed: 00:00:23.08" sin haber ejecutado previamente SET TIMING ON, todo automático.

Si te sirvió de ayuda o quieres realizar alguna observación, bienvenidos tus comentarios.