martes, 31 de diciembre de 2013

Joe Celko's Complete Guide to NoSQL: What Every SQL Professional Needs to Know about Non-Relational Databases

Hace dos semanas llegó a mis manos el último libro de Joe Celko «La guía completa de Joe Celko de NoSQL: Lo que todo profesional de SQL debe saber sobre las bases de datos no relacionales». A diferencia de otras obras del autor, esta publicación ofrece un panorama sobre bases de datos no relacionales y sobre otras tecnologías no tradicionales usadas en la actualidad.


En los últimos años el crecimiento de los datos ha sido excepcional por lo que antiguos sistemas basados en el modelo relacional y el lenguaje de consulta SQL han resultado insuficientes para extraer información de grandes conjuntos de datos. El libro explica distintas metodologías de consolidación y consulta de datos que ayudan a las organizaciones a convertir los cúmulos de datos en información y conocimiento, aspectos críticos para la toma de decisiones en estos tiempos de alta competitividad empresarial.


Aspectos como bases de datos orientadas a columnas, implantación de grafos en bases de datos, bases de datos orientadas a texto, procesamiento en la nube y bases de datos geográficas son tratados en el libro. Esto hace que el texto sea muy completo y heterogéneo a pesar de que en algunas ocasiones las distintas referencias a situaciones y autores pueden hacerlo un poco difícil de digerir.


Nuestra puntuación: 4,5 de 5.


Joe Celko's Complete Guide to NoSQL: What Every SQL Professional Needs to Know about Non-Relational Databases

Primera forma normal

La primera forma normal nos indica que los elementos de una tabla deben ser atómicos, es decir, que no acepten descomposición. Otra forma de ver esto es que dentro de una tabla no debe haber arreglos.

Aunque no tiene sentido definir un arreglo dentro de una tabla (Hacer esto en SQL estándar no es apropiado ni viable), muchos programadores se las arreglan para simular esto. Otros programadores más audaces introducen campos que almacenan varios datos. Mostraré  un ejemplo de cada situación:

  • Arreglos en tablas: Expondré un caso con el que recientemente tuve que lidiar: Una tabla almacena los datos de un producto como nombre, código y fabricante. Además de esto, almacena los distintos precios a los que el producto se vende. Seguidamente se muestra dicha tabla.

CREATE TABLE PRODUCTO (
PRODUCTO_ID INTEGER NOT NULL PRIMARY KEY,
CODIGO_INTERNO CHAR (20) NOT NULL UNIQUE,
DESCRIPCION VARCHAR (128) NOT NULL,
PRECIO_A NUMERIC (10.2) NOT NULL,
PRECIO_B NUMERIC (10.2) NULL,
PRECIO_C NUMERIC (10.2) NULL,
PRECIO_D NUMERIC (10.2) NULL)

  • Campos que almacenan varios datos: En este caso, el campo CODIGO_INTERNO de la tabla anterior contenía la siguiente información.

Caracteres 1 al 5: Número del fabricante.
Caracter 6: Una raya.
Caracteres 7 al 11: Tipo del producto.
Caracter 12: Una raya.
Caracteres 13 al 20: Otros datos internos de la organización.

  • Algunas consideraciones importantes:
  1. El cliente manejaba un stock de 223.000 piezas distintas.
  2. Había una tabla de fabricantes.
  3. Había una tabla de tipos de producto.

Algunos productos podían tener el primer campo de precio vacío y algunos de los otros campos de precio con algún valor. Como el primer campo de precio era requerido (NOT NULL), se llenaba este campo con valor cero cuando este campo debía estar vacío.

En el primer caso (Arreglos en tablas) se me contrató para resolver los siguientes problemas:

  • Averiguar por qué la tabla de productos crecía tan rápido.
  • Modificar las aplicaciones para que aceptara una mayor cantidad de precios por producto.
  • Mejorar los reportes de precios de producto.

En el segundo caso (Campos que almacenan varios datos), el cliente quería mejorar los siguientes elementos:

  • Que el sistema no pudiera eliminar o modificar fabricantes o tipos de productos que tuvieran registros relacionados en la tabla de productos.
  • Mejorar los tiempos de respuestas de los reportes de productos discriminados por tipo y por fabricante.

Con el esquema de datos proporcionado por el cliente, la tarea implicaba reconstruir las tablas de productos ya que:

  • La cantidad de precios por producto era variable: en algunos casos los productos podían tener un precio, en otros casos los productos podían tener tres o cuatro precios. Muy pocos productos tenían cinco precios. En menor cantidad, los precios podían ser siete u ocho por producto. Solución: Se construyó una tabla de precios vinculada con la tabla de productos. En esta nueva tabla se insertan tantos registros como precios tenga el producto.
  • No era posible crear una integridad referencial entre la tabla de fabricantes y tabla de tipos con la tabla de productos ya que no existían campos foráneos en esta última. Solución: El campo CODIGO_INTERNO se dividió en los campos necesarios para poder realizar la integridad referencial con las tablas de fabricantes y tipos de producto.

Claves foráneas

En muchas oportunidades las personas me preguntan sobre el concepto de clave foránea y su repercusión en la definición de las tablas. Trataré de dar una explicación sencilla con conceptos básicos y algunos ejemplos.

El concepto de clave foránea implica relación o vinculación. En el caso de base de datos, se refiere a un registro de una tabla que está relacionado o vinculado con un registro de otra (o de la misma) tabla.

Veamos el siguiente ejemplo:

CREATE TABLE color (
color_id INTEGER NOT NULL PRIMARY KEY,
nombre VARCHAR (10)
);

CREATE TABLE producto (
producto_id INTEGER NOT NULL PRIMARY KEY,
nombre VARCHAR (10),
color_id  INTEGER NULL,
producto_color_fk FOREIGN KEY (color_id) REFERENCES color (color_id)
);

Aquí estamos indicando que el campo color_id de la tabla producto, está vinculado o relacionado con el campo color_id de la tabla color. Esto implica que un valor del campo color_id de la tabla producto será válido si y sólo sí está almacenado en el campo color_id de cualquiera de los registros de la tabla color.

Adicionalmente, el campo color_id de la tabla producto tiene como característica que es NO requerido (puede ser NULL). Esto implica que dicho campo acepta el valor NULL además de los valores almacenados en el campo color_id de la tabla color. Si se desea que no acepte valores NULL, debe cambiarse la definición según se indica seguidamente:

CREATE TABLE producto (
producto_id INTEGER NOT NULL PRIMARY KEY,
nombre VARCHAR (10),
color_id  INTEGER NOT NULL,
producto_color_fk FOREIGN KEY (color_id) REFERENCES color (color_id)
);


Esto hará que dicho campo sea requerido y sólo pueda contener valores almacenados en el campo color_id de la tabla color.

Será el gestor de bases de datos el que se encargue de verificar la validez de los valores almacenados en el campo color_id de la tabla producto. Esto libera al programador de realizar aplicaciones que revisen si un valor existe en la tabla color. Esto garantiza consistencia en los datos almacenados.

La cláusula EXISTS y su uso en el tratamiento de valores NULL

La cláusula EXISTS es una instrucción que devuelve VERDADERO si un conjunto tiene por lo menos un elemento y devuelve FALSO si el conjunto está vacío. La cláusula puede obligarse a trabajar de manera inversa anteponiéndole la cláusula NOT. En el contexto que hacemos referencia, consideraremos como conjunto a cualquier consulta anidada.

Para ilustrar el funcionamiento de la cláusula EXISTS supondremos la existencia de las tablas PRODUCTO y DETALLE que hemos utilizado en anteriores publicaciones. Adicionalmente realizaremos algunos ejemplos para ilustrar el uso de la cláusula EXISTS.

  • PRODUCTO { {1,lápiz},{2,creyón},{3,marcador}};

  • DETALLE {{1,1,10,2},{2,1,8,1},{3,2,5,3},{4,NULL,5,5},{5,NULL,10,2}}


Algunos ejemplos:

  1. EXISTS ( SELECT 1 FROM DETALLE WHERE PRODUCTO_ID = 1). Esta instrucción devuelve VERDADERO ya que existe por lo menos un registro en DETALLE que cumple con la condición PRODUCTO_ID = 1.

  2. EXISTS ( SELECT 1 FROM DETALLE WHERE PRODUCTO_ID = 3). Esta instrucción devuelve FALSO ya que no existe ningún registro en DETALLE que cumpla con la condición PRODUCTO_ID = 3.

  3. NOT EXISTS ( SELECT 1 FROM DETALLE WHERE PRODUCTO_ID = 1). Esta instrucción devuelve FALSO ya que es la negación del ejemplo [1]. Ya que el EXISTS devuelve VERDADERO, su negación por medio de la cláusula NOT devuelve FALSO.

  4. EXISTS ( SELECT 1 FROM DETALLE WHERE PRODUCTO_ID = NULL). Esta instrucción devuelve FALSO ya que la comparación PRODUCTO_ID = NULL siempre va a fallar ya que se intenta comparar un valor NULL con el operador de igualdad. Recuerde que para comparar valores NULL debe usarse IS NULL o IS NOT NULL.

  5. EXISTS ( SELECT 1 FROM DETALLE WHERE PRODUCTO_ID IS NULL). Esta instrucción devuelve VERDADERO ya que existe por lo menos un registro en DETALLE que cumple con la condición PRODUCTO_ID IS NULL.

Explicados los ejemplos, veremos cómo puede ser usada la cláusula EXISTS para validar condiciones de existencia en una consulta anidada. Si esta última no devolviera registros cuyo PRODUCTO_ID fuera NULL, esto bastaría para saber los productos que no se han vendido:



SELECT * FROM PRODUCTO WHERE PRODUCTO_ID NOT IN
(SELECT PRODUCTO_ID FROM DETALLE);



Según se observó en la publicación previamente mencionada, esta consulta fallaba por la presencia de valores NULL en la consulta anidada. Dicho esto, podemos usar la cláusula EXISTS para saber los productos que no se han vendido de la siguiente manera.


SELECT * FROM PRODUCTO WHERE NOT EXISTS
(SELECT 1 FROM DETALLE WHERE DETALLE.PRODUCTO_ID = PRODUCTO.PRODUCTO_ID );



Para ejecutar dicha consulta, el motor de SQL hará el siguiente recorrido:

  1. Para cada elemento de la consulta externa, pasará el valor del campo PRODUCTO_ID a la consulta anidada.

  2. Una vez obtenido el valor de PRODUCTO_ID, la consulta anidada buscará el primer registro que cumpla con la cláusula WHERE. Dependiendo del motor de base de datos, al encontrar la primera coincidencia, la consulta anidada detiene su ejecución y devuelve un conjunto no vacío. Al ser el conjunto no vació la cláusula EXISTS devuelve VERDADERO y la cláusula NOT lo convierte en FALSO. Ello implica que la cláusula WHERE externa falla y el registro no es mostrado. Esto ocurre ya que el producto se ha vendido por lo menos una vez y no debe mostrarse.

  3. Si la cláusula WHERE de la consulta anidada no encuentra valores coincidentes, ésta devuelve un conjunto vacío, haciendo que la cláusula EXISTS devuelva FALSO y a su vez haciendo que el NOT devuelva VERDADERO. Esto hace que la cláusula WHERE externa sea exitosa y el registro se muestra ya que el producto no se vendió.

  4. Ya que seguramente la búsqueda de la consulta anidada hace referencia a una clave foránea, esta búsqueda será indexada, por lo que se hará de manera eficiente.

  5. La consulta original, además de funcionar erróneamente, para cada registro de la consulta externa se revisa secuencialmente cada valor de la consulta anidada. Esto afecta enormemente el rendimiento de la consulta en general.

Finalmente, la cláusula EXISTS puede usarse con claves foráneas compuestas, haciendo referencia dentro de la consulta anidada a cada campo que pertenece a la clave foránea.


Ejemplo:


SELECT .... FROM X WHERE NOT EXISTS

(SELECT 1 FROM Y WHERE Y.A = X.A AND Y.B = X.B AND Y.C=X.C......)

El problema de los valores NULL [2]

En ocasiones, las restricciones de integridad se definen como no obligatorias. Dicho de otra manera, es posible que el valor de la clave foránea pueda ser NULL. La presencia de valores NULL en una consulta anidada puede arrojar resultados distintos a los esperados. Previo a la explicación de los problemas de los valores NULL en consultas anidadas recordaremos algunas cosas elementales.

  • Lógica booleana: La negación de un conjunto de comparaciones relacionadas por medio de cláusulas OR, resultará en la negación de cada comparación y las cláusulas OR se convierten en AND. Ejemplos:


NOT ( A=B OR C=D OR E=F) es equivalente a (A<>B AND C<>D AND E<>F)




  • Los valores NULL sólo deben compararse con las instrucciones IS NULL o IS NOT NULL. Cualquier comparación con operadores clásicos como mayor, mayor o igual, distinto, etc., hace que la comparación falle y traiga como respuesta FALSO.



  • El operador IN, compara un elemento contra un conjunto. Si el elemento está en ese conjunto, la comparación resultará VERDADERA. El operador NOT IN hace lo contrario. Si el elemento no está en dicho conjunto, la comparación resultará VERDADERA. Ejemplos:


1 IN (1,2,3): VERDADERO
1 IN (2,3): FALSO
1 NOT IN (2,3): VERDADERO
1 NOT IN (1,2,3): FALSO

    Para afectos prácticos, el elemento se compara con cada uno de los elementos del conjunto. Si el elemento es igual a cualquiera de ellos, la comparación es VERDADERA.

Aclarados los aspectos básicos, supongamos la existencia de las siguientes tablas:

CREATE TABLE detalle
(
identificador INTEGER NOT NULL,
producto_id INTEGER NULL,
cantidad INTEGER NOT NULL,
precio NUMERIC (10.2) NOT NULL
);

ALTER TABLE detalle ADD PRIMARY KEY (identificador);

CREATE TABLE producto
(
producto_id INTEGER NOT NULL,
descripcion CHAR (50) NOT NULL
)

ALTER TABLE producto ADD PRIMARY KEY (producto_id);

¿Por qué podría fallar esta consulta?

SELECT * FROM PRODUCTO WHERE PRODUCTO_ID NOT IN
(SELECT PRODUCTO_ID FROM DETALLE);


Obsérvese que el campo PRODUCTO_ID en la tabla DETALLE puede ser NULL. A efectos de la explicación, supongamos que los valores de este campo son los siguientes: 1, 2, 3 y NULL. Dicho esto, la consulta anterior es equivalente a ésta:

SELECT * FROM PRODUCTO WHERE PRODUCTO_ID NOT IN (1, 2, 3, NULL);


Lo que la hace equivalente a:

SELECT * FROM PRODUCTO WHERE NOT (PRODUCTO_ID IN (1, 2, 3, NULL) )


Lo que es equivalente a:

SELECT * FROM PRODUCTO WHERE NOT
(PRODUCTO_ID = 1 OR PRODUCTO_ID = 2 OR PRODUCTO_ID = 3 OR PRODUCTO_ID = NULL )


Y por lo visto en el apartado de lógica booleana esta consulta es equivalente a:

SELECT * FROM PRODUCTO WHERE
(PRODUCTO_ID <> 1 AND PRODUCTO_ID<>2 AND PRODUCTO_ID<> 3 AND PRODUCTO_ID <> NULL)


Ya que NULL sólo debe compararse con IS NULL o IS NOT NULL, la expresión PRODUCTO_ID <> NULL siempre fallará y por lo tanto, la expresión:


(PRODUCTO_ID <> 1 AND PRODUCTO_ID<>2 AND PRODUCTO_ID<> 3 AND PRODUCTO_ID <> NULL )


también siempre fallará. Como corolario, la consulta jamás traerá registros debido a la presencia de un valor NULL en la consulta anidada.

Para evitar este problema la consulta debe reescribirse de la siguiente manera:

SELECT * FROM PRODUCTO WHERE PRODUCTO_ID NOT IN
(SELECT PRODUCTO_ID FROM DETALLE WHERE PRODUCTO_ID IS NOT NULL);


 Esto soluciona el problema de existencia de valores NULL en la consulta anidada.

lunes, 30 de diciembre de 2013

El problema de los valores NULL [1]

En varios foros he encontrado consultas que tienen en común el uso de filtros que involucran valores nulos. Previo a la discusión, refrescaremos algunos conceptos propios del modelo de datos relacional.

Los valores nulos (NULL de ahora en adelante) son utilizados en bases de datos para representar información desconocida. Este valor es independiente del tipo de datos que se utilice, por tanto, es posible asignar el valor NULL a atributos numéricos, alfanuméricos y temporales. Algunas personas confunden el NULL con una cadena de caracteres de longitud cero o con un entero cuyo valor es cero. Esto no es correcto ya que una cadena de longitud cero, es una cadena que no posee caracteres (pero que posee un valor) mientras que una cadena que es NULL simplemente es una cadena que no posee información.

Las comparaciones de valores NULL no pueden ser realizadas con los operadores clásicos (mayor, menor, distinto, etc.) ya que éstos pertenecen a la lógica binaria (verdadero - falso). Ya que la lógica utilizada por el esquema relacional es trivaluada (verdadero - falso - desconocido), cualquier comparación de un valor por NULL mediante los operadores clásicos fallará. Las cláusulas IS NULL y IS NOT NULL verifican respectivamente si un valor es NULL o si un valor no es NULL. En realidad es sólo una cláusula, pero una es la negación de la otra.

Seguidamente se dan algunos ejemplos con algunos valores y condiciones.


ValoresCondiciónResultado
A = NULL, B = 5A = NULL AND B = 5Desconocido. «A» no puede compararse con «igual» ya que su valor es NULL. «A» debe compararse así «A IS NULL».
A = NULL, B = 5A IS NULL AND B = 5Cierto.
A = NULL, B = 8A IS NULL AND B = 9Falso. B vale 8, no 9.
A = NULL, B = 7A IS NULL AND B IS NOT NULLCierto.
A = 7A IS NOT NULLCierto.
A = NULL, B = 4A IS NULL AND B >= 3Cierto.
A = NULL, B = 5A IS NULL AND B NULLDesconocido. «B» debe compararse así «B IS NOT NULL»

Para efectos prácticos, aquellos casos cuyo resultado sea «Desconocido», se comportan como un «Falso». Esto trae efectos nocivos en consultas que usen operadores clásicos en valores NULL.