jueves, 8 de mayo de 2014

Oracle: Valores NULOS en la base de datos con cálculos de promedios y otros.

Del latín nullus, nulo es un adjetivo que refiere a algo falto de fuerza o valor para tener efecto. Lo nulo puede resultar contrario a la ley o carecer de los requisitos vinculados al modo o la sustancia.

Por ejemplo: “El juez declaró nula la medida anunciada por el gobernador al considerar que viola la Constitución”, “El esfuerzo que realizas en los entrenamientos es nulo, y por lo tanto no jugarás el próximo partido”, “Los riesgos vinculados a este calefactor son nulos, ya que trabaja con energía infrarroja que no contamina ni consume oxígeno”.

En el lenguaje cotidiano, nulo se asocia a la nada o a ninguno. Si una persona dice que sus conocimientos sobre química son nulos, está haciendo referencia a que no tiene ningún tipo de capacidad vinculada a dicha materia. En un sentido similar, alguien que asegura tener nulo interés en la literatura es un sujeto que no se interesa por nada relacionado con los libros y las letras.

Sin embargo, este fenómeno de "nulo" en el lenguaje SQL dentro de la base de datos, dista mucho de los párrafos anteriormente leídos.

Cuando leemos que nulo refiere a algo falto de valor, pensaríamos que en el mundo de las matemáticas, ciencias exactas, el comportamiento debería ser congruente con esta definición, sin embargo el genio del siglo XX Albert Einstein dijo:

"Cuando las leyes de la matemática se refieren a la realidad, no son ciertas; cuando son ciertas, no se refieren a la realidad."
Vamos a ver que Einstein no estaba tan lejos de la realidad.

Usamos para este laboratorio, mi VM con OEL 6.5 y Oracle Database 12c E.E. con CDB y PDB.

SQL*Plus: Release 12.1.0.1.0 Production on Thu May 8 22:57:16 2014
Copyright (c) 1982, 2013, Oracle.  All rights reserved.

Last Successful login time: Thu May 08 2014 22:38:22 -06:00

Connected to:
Oracle Database 12c Enterprise Edition Release 12.1.0.1.0 - 64bit Production With the Partitioning, OLAP, Advanced Analytics and Real Application Testing options

SQL> alter pluggable database pdb1_cdb1 open;

Pluggable database altered.

Lo primero, nos vamos a conectar con el usuario de pruebas "HR" y vamos a crear una tabla con dos simples columnas, que muestre el nombre de una persona y su salario.

SQL> connect hr/hr@pdb1
Connected.

SQL> create table testeo_nulos ( nombre varchar2(10), salario number(12,2));

Table created.

Insertamos algunos valores en la tabla de pruebas y al propósito vamos a declara nulo el monto de salario para el último registro ingresado.

SQL> insert into testeo_nulos values ('&nombre',&salario);
Enter value for nombre: Ronald
Enter value for salario: 1000
old   1: insert into testeo_nulos values ('&nombre',&salario)
new   1: insert into testeo_nulos values ('Ronald',1000)

1 row created.

SQL> /
Enter value for nombre: Mario
Enter value for salario: 2000
old   1: insert into testeo_nulos values ('&nombre',&salario)
new   1: insert into testeo_nulos values ('Mario',2000)

1 row created.

SQL> /
Enter value for nombre: Henry
Enter value for salario: 2000
old   1: insert into testeo_nulos values ('&nombre',&salario)
new   1: insert into testeo_nulos values ('Henry',2000)

1 row created.

SQL> /
Enter value for nombre: Oscar
Enter value for salario: 1000
old   1: insert into testeo_nulos values ('&nombre',&salario)
new   1: insert into testeo_nulos values ('Oscar',1000)

1 row created.

SQL> /
Enter value for nombre: Alonso
Enter value for salario: null
old   1: insert into testeo_nulos values ('&nombre',&salario)
new   1: insert into testeo_nulos values ('Alonso',null)

1 row created.

SQL> commit;

Commit complete.

Veamos el contenido de la tabla. Como podrán ver, la tabla cuenta con 5 registros, de los cuáles 4 tienen el monto de salario asignado a un valor numérico y el último registro a un valor NULO.

SQL> select * from testeo_nulos;

NOMBRE      SALARIO
---------- ----------
Ronald 1000
Mario 2000
Henry 2000
Oscar 1000
Alonso

Hagamos una simple operación aritmética. Procedemos a sumar los salarios y a calcular el promedio de los mismos.

SQL> select sum(salario), avg(salario) from testeo_nulos;

SUM(SALARIO) AVG(SALARIO)
------------ ------------
6000     1500

Evidentemente, el monto de la suma de los salarios es correcta, pero que sucede con el promedio.? Si vemos la tabla existen 5 registros, por tanto si hago la siguiente consulta, sería lo mismo que hiciera el calculo con una calculadora de bolsillo.

SQL> select sum(salario)/count(*) from testeo_nulos;

SUM(SALARIO)/COUNT(*)
---------------------
1200

Qué sucede. ? Porqué el monto calculado con la función AVG no es el mismo, con el calculo "hecho a mano"?

Qué pasa si actualizamos la columna de salarios para el empleado "Alonso" con el valor "0".?

SQL> update testeo_nulos
  2  set salario=0
  3  where nombre='Alonso';

1 row updated.

SQL> commit;

Commit complete.

Si observan los resultados de la consulta nuevamente ejecutada, ahora en el resultado obtenido como producto de la aplicación de la función AVG ( Promedio ), me da un resultado distinto al primer resultado obtenido.

SQL> select sum(salario), avg(salario) from testeo_nulos;

SUM(SALARIO) AVG(SALARIO)
------------ ------------
6000     1200

Qué paso realmente? Bueno simplemente la función de promedio del lenguaje SQL, por si sola no evalúa los "NULOS", los ignora y esto podría dar un resultado que matemáticamente es correcto, pero que en la práctica es incorrecto, ya que existen 5 empleados en la tabla de registro y 4 de ellos tienen un salario definido y 1 de ellos no.

Si no conocemos de esta situación, podríamos obtener algunos datos poco exactos. 

Conocimiento lo que sucede, siempre que realizamos un calculo que involucre cantidades numéricas, sobre una columna que no tenga definida algún atributo de restricción como NOT NULL, debemos combinar las funciones aritméticas con la función de validación de nulos, como se muestra en el ejemplo y así estaremos tomando en cuenta a toda la población de registros de la tabla.

SQL> update testeo_nulos
  2  set salario=NULL
  3  where nombre='Alonso';

1 row updated.

SQL> commit;

Commit complete.

SQL> select sum(nvl(salario,0)), avg(nvl(salario,0)) from testeo_nulos;

SUM(NVL(SALARIO,0)) AVG(NVL(SALARIO,0))
------------------- -------------------
      6000   1200

SQL> select sum(salario), avg(salario) from testeo_nulos;

SUM(SALARIO) AVG(SALARIO)
------------ ------------
6000     1500

SQL> 

Así que conceptualmente "NULO" no es algo falto de valor. Un valor NULO en una tabla con la que se realizan cálculos matemáticos, puede ocasionar serios errores de calculo.

Por ejemplo, que pasa si el empleado "Alonso" se acogió a un permiso temporal sin goce de salario. Durante este periodo, se procede a realizar una revisión de los salarios de los empleados y obtenemos de la tabla de planillas el salario promedio para determinar el porcentaje de aumento aplicable para cada empleado.

Si establecemos que para el salario promedio que esta por debajo o es igual a $1.200,00 aplicaremos un 5 % de ajuste y si esta por encima de este monto, aplicaremos un 3%. 

Cuánto dejarían de recibir cada empleado mensualmente por el hecho de que Alonso se fue con permiso sin goce de salario y no nos dimos cuenta.?

SQL> 
select nombre,
salario*0.05 "Salario <=$1200",
salario*0.03"Salario >$1200",
(salario*0.05) - ( salario*0.03) "Diferencia"
from testeo_nulos;

NOMBRE   Salario <=$1200 Salario >$1200 Diferencia
---------- --------------- -------------- ----------
Ronald 50       30  20
Mario       100       60  40
Henry       100       60  40
Oscar 50       30  20
Alonso

SQL>

Resumen estás de acuerdo con Einsten.? Hay que tener mucho cuidado con el manejo de los valores no definidos en columnas que son sensibles de cálculos. Antes de realizar un calculo de este tipo, defina el escenario que debe ser tomado en cuenta, en caso de que alguna columna pudiera estar presente.