To null or not to null

08/12/2007 - 22:18 por Leonardo Azpurua | Informe spam
NULL es lo que se tiene cuando se necesita una información que no se tiene.

Es decir: NADA.

La información puede no tenerse porque no se conoce o porque no existe o
porque no se sabe si existe.

En algunas condiciones, puede ser importante conocer la causa de esta
ausencia de información. Y el simple hecho de que un dato sea NULL no nos
dice esa causa. Para esos casos hay dos opciones: reemplazar el NULL por un
valor arbitrario que indique la causa de ese desconocimiento, o bien agregar
una columna a la tabla que indique, si el contenido de esa columna es NULL,
por qué es NULL.

Por suerte, para la mayoría de los fines prácticos, es irrelevante conocer
la causa de nuestro desconocmiento.

Pero el asunto es saber si está bien o mal permitir que una tabla tenga NULL
en alguna de sus columnas.

Las tablas representan información acerca de cosas que normalmente existen
en el mundo real. No representan toda la información acerca de la cosas,
sólamente aquella que es relevante para algunos fines bien específicos. Y el
conjunto total de esa información puede ser grande y heterogéneo. En una
aplicación de genealogía, por ejemplo, querremos saber quienes son los
padres, quienes los hijos, con quien se casó (y una persona puede casarse
varias veces), etc. Pero tambien podemos querer saber dónde y cuándo nació.
Una persona nace sólamente una vez, y esa vez ocurre en una fecha y lugar
específicos e inmutables (puede cambiar nuestro conocimiento de los hechos,
pero no los hechos mismos).

Lo más natural es que la tabla que contiene las personas sea más o menos
así:

PERSONAS (
ID BIGINT IDENTITY NOT NULL PRIMARY KEY,
Nombre VARCHAR(40) NOT NULL,
FechaNacimiento DATETIME,
LugarNacimiento VARCHAR(24) NOT NULL DEFAULT '',
IdPadre BIGINT, -- se permite NULL, porque puede ser la raiz de una
genealogía
IdMadre BIGINT -- lo mismo...
)

Estamos permitiendo NULL en la fecha de nacimiento, pero no en el lugar de
nacimiento. En general, es mejor que los NULL no existan, y lo mismo da que
el lugar de nacimiento contenga NULL que que esté vacío. En el caso del
lugar de nacimiento, usamos un valor imposible (la cadena vacía) para
indicar que no conocemos el dato. Para la fecha de nacimiento, en cambio,
usamos NULL. Para que una fecha sea una fecha, debe ser una fecha válida, y
en una aplicación de genealogía es prácticamente imposible determinar si una
fecha es la fecha real o si es el valor que usamos por convención para
indicar que la fecha no está disponible. De una persona podría desconocerse
el padre, o la madre o ambos.

Si queremos saber, por ejemplo, quienes nacieron en Cumaná, escribiremos
SELECT * FROM Personas WHERE LugarNacimiento = 'Cumaná'
y deberemos tener en cuenta que los resultados no representan a todas las
personas que nacieron en Cumaná, sino a todas las personas de las que se
sabe que nacieron en Cumaná.

Pero si queremos saber quienes nacieron en Enero de cualquier año,
escribiremos
SELECT * FROM Personas WHERE DATEPART(m, FechaNacimiento) = 1
y lo mismo hay que entender que la respuesta incluye a las personas de las
que se sabe que nacieron en Enero.

Si usamos para la fecha de nacimiento un valor convencional (digamos el
1-1-1000), la consulta anterior nos devolverá todas las personas que
nacieron en Enero más todas las personas que no se sabe cuando nacieron. Y
eso no sirve. Y si filtramos la consulta así:
SELECT * FROM Personas WHERE DATEPART(m, FechaNacimiento) = 1
AND DATEPART(y, FechaNacimiento) != 1000
y si por azar algun ancestro de alguien nació el 1-1-1000 será
incorrectamente omitido.

De manera que para las fechas, NULL es mejor que cualquier otra cosa.

Lo mismo pasa con los numeros. Digamos que tenemos una aplicación de
telemercadeo en la que a los datos básicos de las personas agregamos una
columna con el ingreso mensual. Es posible que una persona no tenga ingresos
mensuales, o bien es posible que no sepamos el monto de esos ingresos.
Podemos evitar el NULL escribiendo un valor negativo (-1) cuando el valor
sea desconocido. Pero si en algun momento quisiéramos conocer el ingreso
mensual promedio de las personas registradas en nuestra BD, no podríamos
escribir SELECT AVG(IngresoMensual) FROM Personas, ya que el total de
ingresos sería menor que la suma (al restarse los -1) y la cantidad de
personas sería mayor (al no igorarse los -1). Mientras que si permitimos
NULL, el resultado de esa consulta sería exacto (siempre que entendamos, por
supuesto, que representa el ingreso mensual promedio de aquellas personas
cuyo ingreso mensual se conoce).

Usar cualquier valor diferente de NULL para las columnas de tipos numéricos
cuyos valores puedan resultar desconocidos puede producir efectos indeseados
en los resultados de las funciones de dominio agregado.

Y usar NULL tambien. Si en vez de SELECT AVG... escribimos SELECT
SUM(IngresoMensual) / COUNT(*) FROM Personas, el valor resultante será
incorrecto, ya que COUNT(*) puede ser mayor que la cantidad de personas con
un ingreso mensual conocido. Pero ¿a quién se le ocurriría obtener un
promedio de semejante manera?

La mayoría de los problemas con el uso de NULL se originan en la existencia
de valores desconocidos y ocurren cuando quien consulta la BD ignora esta
circunstancia.

Con mucha frecuencia, los NULL aparecen a consecuencia de una deficiencia de
diseño.

Consideremos una tabla de una aplicación de Recursos Humanos:

TRABAJADOR (
Codigo varchar(8) NOT NULL PRIMARY KEY,
Nombre varchar(40) NOT NULL,
...
NOMBRE_HIJO1 varchar(40),
FECHA_NAC_HIJO1 Datetime,
SEXO_HIJO1 tinyint,
NOMBRE_HIJO2 varchar(40),
FECHA_NAC_HIJO2 Datetime,
SEXO_HIJO2 tinyint,
... y así hasta el hijo 4
)

si el trabajador no tuviese hijos, esta tabla estaría llena de NULLs. O si,
como cualquiera de mis padres, el trabajador tuviese más de cuatro hijos, no
habría donde almacenar esta información adicional.

La relación de un trabajador con sus hijos es de 1 a muchos, y debería
representarse mediante una tabla adicional, en la cual podrían o no
permitirse nulos en alguna de las columnas.

Otra torpeza de diseño, en esa misma aplicación es:

TRABAJADOR (
Codigo varchar(8) NOT NULL PRIMARY KEY,
Nombre varchar(40) NOT NULL,
...
EscuelaPrimaria varchar(40),
FechaEgresoPrimaria datetime,
EscuelaSecundaria varchar(40),
FechaEgresoSecundaria datetime,
Universidad varchar(40),
FechaEgresoUniversidad datetime,
TituloObtenido varchar(18),
...
)

aunque casi todo el mundo ha terminado la escuela primaria, no todo el mundo
ha terminado la secundaria, y aun en los paises más desarrollados un poco
menos de la mitad de las personas no obtuvieron nunca un título
universitario. Al menos la mitad de las filas de esta tabla están condenadas
a tener NULL en los campos relacionados con la educación universitaria, y si
la empresa contrata mucho personal obrero no calificado, muchas filas
contendrán NULL en las columnas relacionadas con la educación secundaria. La
relación de un persona con un determinado nivel de estudios es de 1 a (0,
1), no de 1 a 1. Y la probabilidad de que el valor de la relación sea cero
es relativamente elevada. En estos casos, es mejor dividir la tabla en dos:

TRABAJADOR (
Codigo varchar(8)...
...
)

y

EDUCACION_TRABAJADOR (
CodigoTrabajador varchar(8) NOT NULL,
Nivel (Primaria, Secundaria, Superior) NOT NULL,
Instituto varchar(40) NOT NULL,
FechaEgreso DateTime NOT NULL,
TituloObtenido varchar(18)
)

con la ventaja adicional de que podemos almacenar más elementos de la
historia: pasé por no menos de cuatro instituciones durante la primaria, y
por no menos de seis durante el bachillerato. Y hay quienes tienen más de un
grado universitario (cada vez son más las personas con esta característica),
o bien decidimos que queremos extender esta tabla para anotar los postgrados
realizados, por ejemplo (agregando el valor Postgrado al dominio de
<Nivel>).

Para representar una cosa pueden ser necesario un conjunto mínimo de
atributos. De ese conjunto mínimo de atributos, algunos podrán representarse
mediante un dato simple (es decir, un dato que pueda almacenarse en una
"celda" de la tabla) y otros (por ejemplo, los eventos de la historia
escolar, o la información sobre los hijos de los trabajadores) requerirán
por su naturaleza y cantidad de una tabla adicional.

Pero existe la probabilidad de que uno de los atributos simples requeridos
no esté disponible (por ejemplo, los ingresos mensuales para la aplicación
de telemercadeo) y que sea preferible almacenar la información incompleta
que no almacenarla en absoluto. Si esa probabilidad es relativamente baja,
lo más sensato (en nombre de la simplicidad) es almacenarlo en una columna
de la tabla principal, permitiendo nulos cuando el tipo lo requiera (numeros
o fechas). Pero si esa probabilidad es alta, es mejor usar una tabla
adicional relacionada con la principal.

Cuán baja es una probabilidad baja, o cuán alta es una probabilidad alta es
algo que depende de la aplicación, de las políticas de los usuarios o del
gusto personal del diseñador. Pero si el dato es un dato *requerido* por lo
general lo dejo en la tabla principal, con la esperanza de que con el tiempo
se irán llenando los huecos.

Los NULL requieren de una convención: es falso todo aquello que no se sabe
que es cierto. Es una burrada epistemológica, pero es una buena solución
práctica.

Si queremos promover una colección con todas las grabaciones de Jethro Tull
(cuyo pico de actividad fue a mediados de los '70) probablemente querremos
enviarla a las personas que tuvieran entre 15 y 30 años en 1975 (es decir,
nacidas entre 1945 y 1960). Para obtener ese listado escribiriamos:

SELECT *
FROM Clientes
WHERE DATEPART(y, FechaNacimiento) BETWEEN 1945 AND 1960

pero si quisiéramos estar seguros de que TODAS las personas nacidas dentro
de ese período van a recibir la promoción, y si no nos importa ofrecerla
tambien a otras personas aunque pudieran no tener la edad requerida,
escribiríamos:

SELECT *
FROM Clientes
WHERE FechaNacimiento IS NULL
OR DATEPART(y, FechaNacimiento) BETWEEN 1945 AND 1960

Esta convención aplica sólo para el tratamiento de conjuntos:

Dadas dos personas (12, 'Leonardo', NULL, 'Caracas') y (16, 'Maria',
29-5-1952, NULL), si se pide una lista de las personas nacidas en la década
de los 1950s, sólo aparecerá María. Pero si se pregunta "Leonardo nació en
la década de los 1950's" la respuesta correcta es "no lo sé".

Ante una pregunta como esa, los valores posibles son SI/NO/NO SE SABE.

Eso nos lleva a una "lógica de tres valores". En la lógica matemática
tradicional, una afirmación solo puede ser verdadera o falsa. En la lógica
de tres valores, se agrega la posibilidad (NO SE SABE). Las tablas de verdad
para los operadores básicos -AND, OR y NOT- no tienen dos o cuatro valores,
sino tres o nueve.

Pero este problema, real y relevante, no se suprime con la omisión de los
NULL. El problema surge del hecho *MATERIAL* de que no conocemos un elemento
de información. Y no importa cuantas vueltas le demos, siempre habrá
preguntas a las que no se pueda dar una respuesta correcta de manera simple.

Los NULL son lo que nos permite almacenar la información aun cuando no la
tengamos completa. Las inconsistencias que algunos teóricos exponen en su
contra no la cuasan los NULL, sino la incompletitud de la información
disponible.

Si no hubiera NULL, tendríamos que partir las tablas ante la más
insignificante posibilidad de que un elemento de información no estuviera
disponible.

En nuestra tabla PERSONAS es posible que la ciudad, o la fecha de nacimiento
o ambas sean desconocidas.

Si partimos la tabla, y queremos obtener una lista de todas las personas con
su fecha y ciudad de nacimiento habría que escribir

SELECT Personas.Id, Personas.Nombre, FechasNacimiento.Fecha,
CiudadesNacimiento.Ciudad
FROM Personas LEFT JOIN FechasNacimiento
ON Personas.Id = FechasNacimiento IdPersona
LEFT JOIN CiudadesNacimiento
ON Personas.Id = CiudadesNacimiento IdPersona

en vez de

SELECT * FROM Personas

(y lo mismo obtendremos NULL en el conjunto de resultados).

Sólo parto una tabla cuando la relacion es de 1:n o cuando el elemento de
información es accesorio y la probabilidad de que no esté disponible es
mayor que un medio.

No hay nada malo en los NULL.


Salud!
 

Leer las respuestas

#1 Pablo Roca
09/12/2007 - 19:31 | Informe spam
Muy bueno Leonardo


Saludos,

Pablo Roca
La Coruna - Spain
http://www.portalfox.com

Preguntas similares