viernes, 27 de julio de 2007

WHEN OTHERS THEN NULL ;

Veo muchas veces a los desarrolladores poniendo en sus códigos...


EXCEPTION
WHEN OTHERS THEN
NULL ;



Las programadores que hacen ésto no estan concientes de sus graves consecuencias.
Un desarrollador JAMAS debe poner código de ese tipo. Porque? Por la simple razón que poniendolo, es como si jamas lo hubieramos puesto. Se entiende? No tiene ningún sentido ponerlo porque no cumple ninguna función. Ahh! Si! Cumple con la función de que si tenemos un error durante la ejecución de nuestro código... jamas nos enteramos; y si nos enteramos de que hubo un error es demasiado difícil encontrarlo.

Veamos un muy sencillo ejemplo:


SQL_9iR2> CREATE OR REPLACE PROCEDURE pr_log_errores
2 (
3 p_cod_error IN VARCHAR2 ,
4 p_msj_error IN VARCHAR2
5 )
6 IS
7 BEGIN
8 INSERT INTO log_errores
9 (
10 fecha ,
11 cod_error,
12 msj_error
13 )
14 VALUES
15 (
16 SYSDATE ,
17 p_cod_error,
18 p_msj_error
19 ) ;
20 COMMIT ;
21 EXCEPTION
22 WHEN OTHERS THEN
23 NULL ;
24 END ;
25 /

Procedure created.

SQL_9iR2> BEGIN
2 pr_log_errores('ORA-00001','unique constraint violated.') ;
3 END ;
4 /

PL/SQL procedure successfully completed.


Como podemos ver, ejecutamos nuestro procedimiento de prueba y terminó perfectamente... pero, realmente terminó bien? Veamos...


SQL_9iR2> SELECT *
2 FROM log_errores ;

no rows selected


Aha! El procedimiento no insertó en la tabla de logueo el error que le pasamos como parámetro. Pero como puede ser esto posible?


SQL_9iR2> DESC log_errores
Name Null? Type
------------------ -------- -------------
FECHA DATE
COD_ERROR VARCHAR2(100)
MSJ_ERROR VARCHAR2(10)


El campo MSJ_ERROR tiene una longitud de 10 caracteres y nosotros queremos introducir una cadena más larga. Nosotros nunca nos enteramos de que hubo un error.
Imaginense si es un proceso Batch crítico que se ejecuta todos los días de forma automática durante la madrugada. Si ocurre algún error... nadie se podría dar cuenta que el proceso en algún punto falló.

Corregimos nuestro procedimiento...


SQL_9iR2> CREATE OR REPLACE PROCEDURE pr_log_errores
2 (
3 p_cod_error IN VARCHAR2 ,
4 p_msj_error IN VARCHAR2
5 )
6 IS
7 BEGIN
8 INSERT INTO log_errores
9 (
10 fecha ,
11 cod_error,
12 msj_error
13 )
14 VALUES
15 (
16 SYSDATE ,
17 p_cod_error,
18 p_msj_error
19 ) ;
20 COMMIT ;
21 EXCEPTION
22 WHEN OTHERS THEN
23 RAISE ;
24 END ;
25 /

Procedure created.

SQL_9iR2> BEGIN
2 pr_log_errores('ORA-00001','unique constraint violated.') ;
3 END ;
4 /
BEGIN
*
ERROR at line 1:
ORA-01401: inserted value too large for column
ORA-06512: at "IP_UTILS.PR_LOG_ERRORES", line 24
ORA-06512: at line 2

jueves, 26 de julio de 2007

Compulsive Tuning Disorder

Descubrí AQUI si tenés los síntomas del CTD...

Geeks...

Muy interesante ARTÍCULO que explica cómo tratar en un ambiente laboral a los Geeks y cuales son sus necesidades.
Darles trabajos desafiantes, importantes y significativos a ésta clase de personas es esencial.

Si sos un Geek o crees que lo sos y estas en un trabajo en el cual sentis que estas desperdiciando mucho de vos, quizás sea hora de pensar en un cambio...

martes, 17 de julio de 2007

Evitá el uso de Variables Globales

Veamos un ejemplo:


SQL_9iR2> CREATE OR REPLACE PACKAGE test_pkg
2 AS
3 PROCEDURE test_global_variable ;
4 END ;
5 /

Package created.

SQL_9iR2> CREATE OR REPLACE PACKAGE BODY test_pkg
2 AS
3 variable_globar NUMBER ;
4 PROCEDURE mostrar_variable_global ( p_valor IN NUMBER )
5 IS
6 BEGIN
7 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
8 variable_globar := 2 ;
9 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
10 END ;
11
12 PROCEDURE test_global_variable
13 IS
14 BEGIN
15 variable_globar := 1 ;
16 mostrar_variable_global(variable_globar) ;
17 END ;
18
19 END test_pkg ;
20 /

Package body created.


SQL_9iR2> EXEC test_pkg.test_global_variable ;
Valor de la variable global: 1
Valor de la variable global: 2


PL/SQL procedure successfully completed.


Cómo puede ser ésto posible? Bueno, como los valores IN se pasan por referencia, el valor de la variable global también es pasada al procedimiento mostrar_variable_global por referencia.

Para evitar éste problema con las variables globales tenemos 2 opciones:
1) No usar Variables Globales :)
2) Si estamos forzados a utilizarlas, podemos asignar el contenido de la variable global a una nueva variable y pasar el valor de esa nueva variable como parámetro. Sino, podemos modificar el input del parámetro.

Veamos las 2 opciones implementadas:

- Opción 1:

SQL_9iR2> CREATE OR REPLACE PACKAGE test_pkg
2 AS
3 PROCEDURE test_global_variable ;
4 END test_pkg ;
5 /

Package created.

SQL_9iR2> CREATE OR REPLACE PACKAGE BODY test_pkg
2 AS
3 variable_globar NUMBER ;
4 PROCEDURE mostrar_variable_global ( p_valor IN NUMBER )
5 IS
6 BEGIN
7 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
8 variable_globar := 2 ;
9 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
10 END ;
11
12 PROCEDURE test_global_variable
13 IS
14 l_variable_global_aux NUMBER ;
15 BEGIN
16 variable_globar := 1 ;
17 l_variable_global_aux := variable_globar ;
18 mostrar_variable_global(l_variable_global_aux) ;
19 END ;
20
21 END test_pkg ;
22 /

Package body created.


SQL_9iR2> EXEC test_pkg.test_global_variable ;
Valor de la variable global: 1
Valor de la variable global: 1



- Opción 2:

SQL_9iR2> CREATE OR REPLACE PACKAGE test_pkg
2 AS
3 PROCEDURE test_global_variable ;
4 END test_pkg ;
5 /

Package created.

SQL_9iR2> CREATE OR REPLACE PACKAGE BODY test_pkg
2 AS
3 variable_globar NUMBER ;
4 PROCEDURE mostrar_variable_global ( p_valor IN NUMBER )
5 IS
6 BEGIN
7 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
8 variable_globar := 2 ;
9 dbms_output.put_line( 'Valor de la variable global: '||p_valor ) ;
10 END ;
11
12 PROCEDURE test_global_variable
13 IS
14 BEGIN
15 variable_globar := 1 ;
16 mostrar_variable_global(variable_globar+0) ;
17 END ;
18
19 END test_pkg ;
20 /

Package body created.


SQL_9iR2> EXEC test_pkg.test_global_variable ;
Valor de la variable global: 1
Valor de la variable global: 1


PL/SQL procedure successfully completed.

The 10g Plan Table

Muy interesante ARTÍCULO que muestra cómo sacar más provecho de la Plan Table en 10g...

sábado, 14 de julio de 2007

SQL Injection

SQL Injection es una técnica de ataque a una base de datos. Mediante una aplicación Web, el atacante puede modificar los parámetros que envía desde un formulario hacia la base de datos y de ésta manera, modificar la sentencia SQL que se ejecuta. Podemos ejecutar sentencias DML, DDL... entre otras cosas.

Veamos un ejemplo de SQL Injection en Oracle:

Creamos una tabla para nuestra prueba que contiene usuarios y passwords:


SQL_9iR2> CREATE TABLE login AS
2 SELECT 'user_'||level username, to_char(level*10000) password
3 FROM dual
4 CONNECT BY level <= 5 ; Table created.

SQL_9iR2> SELECT *
2 FROM login ;

USERNAME PASSWORD
---------------- ----------
user_1 10000
user_2 20000
user_3 30000
user_4 40000
user_5 50000

5 rows selected.


Creamos un procedimiento de login que recibe como parámetros el usuario y password de validación. Si los datos son correctos, se devuelven todos los usuarios y passwords de la tabla login, en caso contrario se devuelve una leyenda de error:


SQL_9iR2> CREATE OR REPLACE PROCEDURE pr_login
2 (
3 p_username IN VARCHAR2 ,
4 p_password IN VARCHAR2
5 )
6 IS
7 l_flag BOOLEAN DEFAULT FALSE ;
8 l_cursor SYS_REFCURSOR ;
9 l_query VARCHAR2(4000) ;
10 registro login%ROWTYPE ;
11 BEGIN
12 l_query := 'SELECT *'
13 ||' FROM login'
14 ||' WHERE username = '''||p_username||''''
15 ||' AND password = '''||p_password||'''' ;
16
17 dbms_output.put_line('CONSULTA: '||l_query) ;
18
19 OPEN l_cursor FOR l_query ;
20 LOOP
21 FETCH l_cursor INTO registro ;
22 EXIT WHEN l_cursor%NOTFOUND ;
23 dbms_output.put_line('USER : '||registro.username||'
'||'PASS:'||registro.password) ;
24 IF NOT ( l_flag ) THEN
25 l_flag := TRUE ;
26 END IF ;
27 END LOOP ;
28 CLOSE l_cursor ;
29 IF NOT ( l_flag ) THEN
30 dbms_output.put_line('LOGIN INCORRECTO!') ;
31 END IF ;
32 EXCEPTION
33 WHEN OTHERS THEN
34 dbms_output.put_line('LOGIN INCORRECTO!') ;
35 END ;
36 /

Procedure created.


Bien, ahora vamos a probar nuestro procedimiento de login:


SQL_9iR2> EXEC pr_login('user_2','123456789') ;

CONSULTA: SELECT * FROM login WHERE username = 'user_2' AND password = '123456789'

LOGIN INCORRECTO!

PL/SQL procedure successfully completed.


El procedimiento anduvo a la perfección! Validó el usuario y password, y como resultan ser incorrectos, nos devolvió un error.
Fijense que en el procedimiento incluimos una linea de código (la nro. 17) que nos va a mostrar la sentencia SQL que estamos ejecutando.

Ahora probemos nuevamente ejecutar el procedimiento de login pero modificando el segundo parámetro:


SQL_9iR2> EXEC pr_login('user_2','123456789'' OR ''x''=''x') ;

CONSULTA: SELECT * FROM login WHERE username = 'user_2' AND password = '123456789' OR 'x'='x'

USER : user_1 PASS: 10000
USER : user_2 PASS: 20000
USER : user_3 PASS: 30000
USER : user_4 PASS: 40000
USER : user_5 PASS: 50000

PL/SQL procedure successfully completed.


Les presento a SQL Injection! Como acabamos de observar, modificando uno de los parámetros, pudimos lograr modificar la sentencia SQL que se ejecuta en la base de datos y por consiguiente, ver todos los usuarios y passwords de la tabla login.

La única pregunta que queda por hacer es: ¿ Qué tan segura es su aplicación ?

jueves, 12 de julio de 2007

High Water Mark (HWM)

Generalmente, cuando se crea una tabla o un índice... se crea un segmento asociado al objeto. El segmento se crea con determinados bloques del data file, pero muy poco espacio es destinado para nuestro uso. Cuando los datos van llenando los bloques asignados al segmento, se van alocando más bloques para que usemos.
A medida que los bloques van siendo alocados, la HWM se posiciona en el último bloque para mostrarnos la cantidad total de bloques alocados hasta el momento y estaban disponibles para ser usados.
Un error común es pensar que a medida que eliminamos datos de los bloques, la HWM se refresca para mostrarnos que estamos utilizando menos bloques; pero la realidad es que la HWM se mantiene siempre apuntando al último bloque alocado.
Sabemos que cuando realizamos un Full Scan, son leídos secuencialmente todos los bloques de la tabla hasta la HWM, por lo cual, debemos tener en claro que aunque eliminemos datos de una tabla, ese espacio "vacío" se va a seguir leyendo, lo cual trae como consecuencia la lectura de bloques innecesarios.

Veamos un ejemplo:


SQL_9iR2> CREATE TABLE emp
2 (id NUMBER , sexo VARCHAR2(1 ) ;

Table created.

SQL_9iR2> INSERT INTO emp
2 SELECT level , 'M'
3 FROM dual
4 CONNECT BY level <= 800000 ;

800000 rows created.

SQL_9iR2> INSERT INTO emp
2 SELECT level , 'F'
3 FROM dual
4 CONNECT BY level <= 200000 ;

200000 rows created.

SQL_9iR2> ANALYZE TABLE emp COMPUTE STATISTICS ;

Table analyzed.

SQL_9iR2> SET AUTOTRACE TRACEONLY

SQL_9iR2> SELECT sexo
2 FROM emp
3 WHERE id = 900000 ;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=160 Card=10000 Bytes=60000)
1 0 TABLE ACCESS (FULL) OF 'EMP' (Cost=160 Card=10000 Bytes=60000)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1655 consistent gets
0 physical reads
0 redo size
213 bytes sent via SQL*Net to client
363 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

SQL_9iR2> DELETE FROM emp
2 WHERE sexo = 'M' ;

800000 rows deleted.

SQL_9iR2> SET AUTOTRACE TRACEONLY

SQL_9iR2> SELECT sexo
2 FROM emp
3 WHERE id = 900000 ;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=160 Card=10000 Bytes=60000)
1 0 TABLE ACCESS (FULL) OF 'EMP' (Cost=160 Card=10000 Bytes=60000)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1655 consistent gets
419 physical reads
0 redo size
213 bytes sent via SQL*Net to client
363 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed


Como pudimos ver, aunque hayamos eliminados datos de la tabla, seguimos leyendo la misma cantidad de bloques que antes porque la HWM no se refrescó y sigue apuntando al último bloque alocado en el segmento.
Para solucionar éste problema, podemos realizar...

1) TRUNCATE ...
- Si la tabla esta vacia podemos hacer un TRUNCATE para resetear la HWM. Sino, podemos hacer un export de los datos, luego truncar la tabla y realizar un import.
2) ALTER TABLE ... MOVE
- De ésta manera reorganizamos la tabla. Tener en cuenta que luego de ejecutar el ALTER, hay que hacer un REBUILD de todos los índices de la tabla!
3) Dropear y recrear el objeto (export/import)
4) ALTER TABLE ... SHRINK SPACE | SHRINK SPACE COMPACT
- Para 10g en adelante.

Apliquemos a nuestro ejemplo el punto 2...


SQL_9iR2> ALTER TABLE emp MOVE ;

Table altered.

SQL_9iR2> ANALYZE TABLE emp COMPUTE STATISTICS ;

Table analyzed.

SQL_9iR2> SET AUTOTRACE TRACEONLY

SQL_9iR2> SELECT sexo
2 FROM emp
3 WHERE id = 900000 ;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=33 Card=2000 Bytes=12000)
1 0 TABLE ACCESS (FULL) OF 'EMP' (Cost=33 Card=2000 Bytes=12000)

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
335 consistent gets
0 physical reads
0 redo size
213 bytes sent via SQL*Net to client
363 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed


Como vemos, la HWM se refresco y ahora estamos leyendo sólo los bloques que contienen nuestros datos.

martes, 10 de julio de 2007

Overclocking your brain

Excelente ARTÍCULO que explica varios ejercicios para optimizar el cerebro.


“I just found out that the brain is like a computer. If that’s true, then there really aren’t any stupid people. Just people running DOS.”

- Anonymous

Modificar el prompt de SQL*Plus

Cuando estamos trabajando con SQL*Plus, solemos trabajar con varias ventanas abiertas cada una conectada a una instancia diferente. Esto suele ser un problema ya que podemos confundirnos de instancia y trabajar en una instancia diferente. Cuando iniciamos SQL*Plus, Oracle busca un archivo llamado "login.sql" dentro del path definido en la variable ORACLE_PATH, y si no lo encuentra, busca el archivo "glogin.sql" (global login).
Podemos modificar el prompt de SQL*Plus para identificar con facilidad el esquema e instancia en donde nos encontramos.
Para realizar ésta modificación, abrimos el archivo "login.sql" o "glogin.sql" y colocamos lo siguiente:


set term off
set serveroutput on size 1000000 format wrapped
set linesize 70
set trimspool on
set pagesize 9999
set heading on
set underline -

define sql_prompt=idle
define sql_prompt = 'not connected'

column id_plus_exp FOR 990 HEADING i
column parent_id_plus_exp FOR 990 HEADING p
column plan_plus_exp FOR a60
column object_node_plus_exp FOR a8
column other_tag_plus_exp FOR a29
column other_plus_exp FOR a44

column user_sid new_value sql_prompt

select lower(user)||'@'||'&_CONNECT_IDENTIFIER' user_sid
from dual;

set sqlprompt '&sql_prompt> '

set timing on
set term on


Nota: Este script es el que tengo actualmente en mi cliente. Se puede modificar a gusto de cada uno.

El prompt quedará de la siguiente manera: "user@instancia>"

Fast Dual

Muchas aplicaciones ejecutan millones de veces consultas a la tabla DUAL. En versiones anteriores a la 10g, Oracle realizaba 3 LIO's (Logical I/O) por cada vez que se consultaba a la tabla. Esto es muy costoso en ambientes en los cuales se hace referencia a esta tabla todo el tiempo.
Para solucionar este problema, podemos modificar la tabla DUAL y hacer referencia a la tabla X$DUAL. Esta tabla es una tabla virtual, lo cual no implica ningún LIO.

Ejemplo:


$ sqlplus "/as sysdba"

SQL_9iR2> SET AUTOTRACE TRACEONLY STATISTICS

SQL_9iR2> SELECT * FROM dual ;


Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
402 bytes sent via SQL*Net to client
504 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed


SQL_9iR2> SELECT * FROM x$dual ;

ADDR INDX INST_ID D
-------- ---------- ---------- -
0A8A90D8 0 1 X


SQL_9iR2> CREATE VIEW fast_dual AS
2 SELECT 'X' dummy FROM x$dual ;

View created.

SQL_9iR2> GRANT SELECT ON fast_dual TO PUBLIC ;

Grant succeeded.



SQL_9iR2> CONN algun_esquema/algun_password ;

SQL_algun_esquema> CREATE SYNONYM dual FOR sys.fast_dual ;

Synonym created.

SQL_algun_esquema> SET AUTOTRACE TRACEONLY STATISTICS
SQL_algun_esquema> SELECT * FROM dual ;

1 row selected.

Statistics
---------------------------------------------------
0 recursive calls
0 db block gets
0 consistent gets
0 physical reads
0 redo size
326 bytes sent via SQL*Net to client
498 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed


En 10gR2 se soluciona este problema ya que se modificó la tabla DUAL existente y ahora se hace uso de la tabla virtual X$DUAL.

Hard Parse, Soft Parse & “Softer” Soft Parse

Procesamiento de una consulta:

1) Validación Sintáctica
2) Validación Semántica
3) Optimización
4) Generación del QEP (Query Execution Plan)
5) Ejecución del QEP (Query Execution Plan)

El punto 1 al 4 forma parte del Parseo de la consulta, mientras que el punto 5 es la propia ejecución.

Cuando ejecutamos una consulta, siempre se realizan, como mínimo, los pasos 1 y 2. Luego de ejecutarse estos pasos, Oracle transforma la consulta en un valor hash y la envía a la Shared Pool y en la Library Cache se busca si existe alguna consulta con el mismo valor hash (si alguna sesión ya la utilizó en algun momento). En caso de que exista, se compara el texto de la consulta con la que se encontró en la Library Cache para validar si son exactamente iguales (este paso adicional se realiza porque puede llegar a haber varias consultas con el mismo valor hash); en caso de que lo sean, se procede a ejecutar esa consulta. Esto es lo que llamamos un Soft Parse. Si la consulta no existe, Oracle realiza los pasos 3 y 4. Esto es conocido como un Hard Parse. El Hard Parse es muy costoso para Oracle Server ya que implica realizar varios latches (loqueos) en la SGA y consume mucha CPU.
Como bien sabemos, cada consulta que ejecutamos implica la utilización de un cursor (un cursor es un espacio de memoria destinado a la ejecución de nuestra consulta). Lo ideal, es que nuestra aplicación abra los cursores que vaya a utilizar, ejecute las sentencias x veces y luego los cierre. Muchas aplicaciones como Forms no suelen ejecutar los cursores de esta forma, lo que implica que no podamos reutilizar los cursores y siempre tengamos que abrir nuevamente los que ya ejecutamos.
Para reducir éste problema, podemos utilizar el parámetro de inicialización SESSION_CACHED_CURSORS que nos va a permitir realizar un "Softer" Soft Parse. Si setemos el parámetro en 100, Oracle mantendrá 100 cursores abiertos para que los podamos reutilizar y evitarnos tener que abrirlos cada vez. Este espacio de memoria destinado al manejo de cursores, se mantiene con una lista LRU.
Oracle recomiendo que el parámetro se setee en una primera instancia en 50 e ir monitoreandolo para verificar si conviene incrementar su valor. Este parámetro debe setearse considerando el valor de OPEN_CURSORS.

select to_char(100 * sess / calls, '999999999990.00') || '%' cursor_cache_hits,
to_char(100 * (calls - sess - hard) / calls, '999990.00') || '%' soft_parses,
to_char(100 * hard / calls, '999990.00') || '%' hard_parses
from ( select value calls from v$sysstat where name = 'parse count (total)' ),
( select value hard from v$sysstat where name = 'parse count (hard)' ),
( select value sess from v$sysstat where name = 'session cursor cache hits' ) ;

CURSOR_CACHE_HITS SOFT_PARSES HARD_PARSES
----------------- ----------- -----------
59.11% 39.49% 1.39%


select
'session_cached_cursors' parameter,
lpad(value, 5) value,
decode(value, 0, ' n/a', to_char(100 * used / value, '990') || '%') usage
from
( select
max(s.value) used
from
sys.v_$statname n,
sys.v_$sesstat s
where
n.name = 'session cursor cache count' and
s.statistic# = n.statistic#
),
( select
value
from
sys.v_$parameter
where
name = 'session_cached_cursors'
)
union all
select
'open_cursors',
lpad(value, 5),
to_char(100 * used / value, '990') || '%'
from
( select
max(sum(s.value)) used
from
sys.v_$statname n,
sys.v_$sesstat s
where
n.name in ('opened cursors current', 'session cursor cache count') and
s.statistic# = n.statistic#
group by
s.sid
),
( select
value
from
sys.v_$parameter
where
name = 'open_cursors'
) ;

PARAMETER VALUE USAGE
---------------------- --------------- -----
session_cached_cursors 100 100%
open_cursors 300 57%


Si el valor del SESSION_CACHED_CURSORS se encuentra en el 100%, deberíamos incrementar el valor del parámetro con normalidad.

lunes, 9 de julio de 2007

How To Be A Programmer

Excelente GUÍA de cómo se un programador. Recomiendo leerla.
Me causó gracia la frase... "Debugging is fun, because it begins with a mystery".

Oracle Database 11g


Oracle anunció el lanzamiento de Oracle Database 11g el día 11 de Julio de 2007 en la ciudad de Nueva York.

Nieva en Buenos Aires! histórico!!!


Qué mejor manera de celebrar el día de la Independencia que con una nevada???


La última vez que ocurrió fue en junio de 1918. El fenómeno se registra a raíz de la irrupción de aire polar en los niveles medios de la atmósfera y la temperatura promedio. La térmica llegó 1,2 grados bajo grado. "Continuará hasta la madrugada de mañana y puede intensificarse durante la noche", dijo el Servicio Meteorológico.

fuente: Clarin.com
The views expressed on this blog are my own and do not necessarily reflect the views of Oracle.