Esquemas de datos explícitos e implícitos

Hablando de bases de datos relacionales, es común referirse al esquema de datos como la definición de las tablas, vistas, funciones, etc. que conforman la base de datos*. El esquema es sumamente importante, por supuesto, nos define qué datos admitimos y nos condiciona todo código que accede a la base de datos.

Este es un esquema explícito; está ahí, podemos enumerar los objetos de los que se compone y conocer milimétricamente su estructura; podemos saber qué tablas hay y qué columnas tienen, etc.

Ahora bien, supongamos que cogemos una aplicación que usa una base de datos y de repente, ocultamos el esquema. Asumamos por un momento que no podemos saber qué tablas hay, ni qué columnas, ni nada. Aún haciendo este gran cambio, nuestra aplicación seguirá funcionando**. Aún más, si desconectamos todas las restricciones de integridad y admitimos que se inserten valores en columnas que no existen (e incluso los almacenamos)… nuestra aplicación muy probablemente seguirá funcionando correctamente.

La primera observación interesante que podemos hacer es que aquello a lo que llamábamos el esquema de datos puede que no esté ahí, pero los datos que tenemos almacenados seguirán siguiendo el anterior esquema de datos. La programación seguirá condicionada por ese mismo esquema; seguiremos insertando en las tablas y columnas que definía el esquema.

El esquema de datos original que habíamos declarado explícitamente ya no existe, pero el esquema de datos implícito sigue ahí. Los datos continuan cumpliéndolo y el programa sigue regido por él. Existe, por tanto, un esquema implícito que podríamos deducir con bastante certeza observando los datos almacenados y el código que los manipula.

Siguiendo con este experimento, podríamos preguntarnos… ¿no hemos perdido nada, verdad?

Aparentemente, no. En cuanto a funcionamiento, quizá ocultemos y amplifiquemos algún defecto que ya existía (la base de datos aceptará datos inválidos cuando antes los rechazaba, y muy probablemente, no nos daremos cuenta), pero realmente no habremos perdido mucho.

Pero cuando nos llegue el momento de modificar o ampliar el código, sí tendremos un problema: al no poder consultar el esquema, se dificultará mucho nuestra labor. El esquema explícito era eso: explícito, claro, fácil. El esquema implícito sigue ahí, pero está oculto. Lo duro es que antes bastaba con ajustarnos al esquema explícito de los datos, que estaba delante de nuestros ojos, pero ahora seguimos teniéndo que seguir un esquema de datos implícito, mucho más críptico. Tendremos que mirar los datos almacenados o el código para saber cómo se llamaba cada tabla y cada columna, y esta información muy probablemente no esté centralizada.

Por supuesto, hay un caso en el que sí seguirá disponible. Si usamos un algo como un ORM, podremos contar con otro esquema explícito de datos; la definición del ORM -e incluso en algunos casos podremos reconstruir perfectamente el esquema a partir de la definición del ORM- claramente son conceptos si no equivalentes siempre, muy cercanos***. Si este esquema es suficientemente bueno, podría suplir perfectamente al esquema explícito de las bases de datos (e incluso mejorarlo: podría permitirnos expresar un esquema de datos más restringido).

Podríamos decir que el esquema de datos explícito de la base de datos no es estrictamente necesario, pero ciertamente, un esquema de datos explícito es una herramienta muy útil, quizás no tanto para el funcionamiento de las aplicaciones como para su codificación y mantenimiento.

Adicionalmente, es bien cierto que puedan existir datos que no se ajusten al modelo relacional -cuya estructura o restricciones sean sumamente especiales (normalmente por ser extremadamente laxos), pero aunque esto fuera cierto, seguirían teniendo un esquema implícito (o incluso explícito, si la herramienta que usáramos en sustitución de la base de datos lo permitiera o requiriera, o si nosotros  decidiéramos escribirlo) y, igual que en el caso relacional, no disponer de un esquema explícito nos entorpecería las labores de mantenimiento y extensión del código.

Así pues, aunque es posible que un esquema relacional no sea lo más adecuado para nuestros datos, es falaz concluir que la ausencia de un esquema explícito es una ventaja -el esquema implícito sigue existiendo y nos debemos ajustar a él- y es más fácil ajustarse a algo explícito y claro que a algo implícito y oculto.

* en algunos gestores de bases de datos, el término se confunde un poco porque se pueden separar los objetos de la base de datos en “esquemas” para gestionarlos mejor

** las bases de datos permiten a las aplicaciones consultar el esquema [explícito]; hay aplicaciones que utilizan esta funcionalidad (para permitir su personlización mediante la creación de nuevas tablas, etc.); en este poco frecuente caso, dejarían de funcionar, claro

*** lo que nos debería llevar a pensar que uno de los dos es redundante e innecesario

corregido por el de siempre

Upserts…

Trabajando en un pequeño experimento, se me ha ocurrido un insert idempotente:

insert into foo (bar) (select ‘bar_value’ as bar where not exists (select 1 from foo where bar = ‘bar_value’));

(la sintaxis es de PostgreSQL, pero supongo que es adaptable a otras bases de datos). Igual intento adaptarlo a hacer upserts.

El mito de la escalabilidad

Hace mucho mucho tiempo, en una galaxia muy muy lejana, existía una web de noticias informáticas llamada Slashdot. Por motivos oscuros, Slashdot creció brutalmente en popularidad y por tanto, cada vez que aparecía una noticia en portada de Slashdot enlazando a una web, una cantidad elevada de personas hacía click en la noticia, dirigiendo a ésta ingentes cantidades de tráfico. En los inicios de Internet, no muchas webs estaban suficientemente bien preparadas para soportar carga como Slashdot, y por tanto este tráfico tendía a colapsar los sistemas que recibían enlaces de Slashdot- este efecto fue denominado “Slashdot Effect“.

Avanzando un poco hacia el presente, el tráfico de internet se ha multiplicado. De los 70 millones de usuarios de Internet en el 97 (nacimiento de Slashdot), hemos pasado a los 2110 millones de usuarios actuales (fuente); es decir, se ha multiplicado por 30 el número de usuarios, y muy probablemente, el uso de cada usuario de Internet también se ha intensificado- cada vez dedicamos más tiempo y hacemos más cosas en Internet.

Hoy en día, sitios como Facebook presumen de tener 400 millones de usuarios entrando cada día en sus servicios (¡5 veces el número de usuarios de toda Internet en el 97!) y se suben unos 250 millones de fotos diarias. En Twitter, coincidiendo con hechos como la muerte de Osama Bin Laden, llegan a escribirse más de 12 millones de tuits en una hora.

Así pues, si en el 97 el volumen de uso podía ya suponer un problema para los servicios de Internet, con el enorme crecimiento de la red, ¿se acrecentan estos problemas?

Sin duda alguna. A pesar de que la potencia de los ordenadores se ha acrecentado, esta no ha parecido contrarrestar el hecho de que cada vez queremos más datos, más rápidos y más inmediatos de la red. El mencionado Twitter ha sufrido a lo largo de su carrera para poder soportar el uso que se le da; su Fail Whale ha sido enormemente popular y aún hoy se deja ver de cuando en cuando.

¿Por qué es “difícil” tratar este problema?

Comencemos por lo más básico. Un servidor normalito que podemos adquirir, si sabemos buscar, por 50€/mes, puede servir sin muchos problemas y sin grandes esfuerzos, unas 50 peticiones por segundo de una página dinámica sencilla que muestra información almacenada en una base de datos. Esto equivale a servir 4.320.000 peticiones en un día. Supongamos que cada usuario se pasa unos 10 minutos diarios en la página, y hace una petición nueva cada 10 segundos; eso son unas 60 peticiones. El servidor anterior podría atender a unos 72.000 usuarios/diarios, pudiendo llegar a atender a 3000 usuarios/hora.

Estas cifras parecen respetables, y la buena noticia es que adquiriendo servidores más caros, obtenemos una mejor relación precio/peticiones/s, con lo que podemos fácilmente soportar más carga de este tipo simplemente echando un poco de dinero al problema.

Lógicamente, nunca podríamos llegar a soportar una carga brutal- del orden de magnitud de Facebook, o incluso mucho menos buscando servidores más caros- no existen servidores de tal potencia. Pero seguimos teniendo opciones sencillas. Si en vez de coger un servidor, cogemos más de uno y copiamos toda la base de datos y todo el sistema en estos servidores, las carga se puede repartir en ellos prácticamente a la perfección; si compramos 7 servidores, multiplicaremos las peticiones que podemos atender casi por 7.

Jugando con servidores más o menos costosos y en mayor o menor cantidad de ellos, podemos atender “tantas peticiones como queramos”, a un coste usualmente bastante razonable.

Adicionalmente, la mayor parte de “mostrar información almacenada” en la red hoy en día tiene una característica- es enormemente repetitiva; la página principal de un diario puede necesitar mostrarse a millones de usuarios, pero es esencialmente la misma para todos- así que por muy costoso que sea recuperar y formatear la información, es un trabajo que sólo tenemos que hacer la primera vez- luego podemos guardarnos el trabajo realizado y simplemente copiarlo para el resto de peticiones subsiguientes.

Pero el problema es que no todo es mostrar información almacenada. El problema realmente duro es almacenar información.

El primer problema es que nos surge es la persistencia. Normalmente querremos que la información que se guarde en nuestra web sea persistente; es decir, que no desaparezca. Eso nos obliga para cada escritura a realizar el trabajo duro, registrar esta información en un disco duro o un SSD. Esto es algo que no nos podemos ahorrar de ninguna manera y que de hecho, como veremos ahora, constituye un verdadero límite doloroso al rendimiento que podemos ofrecer.

Un sistema básico puede perfectamente realizar 1500 transacciones de escritura por segundo. Como en el caso de las lecturas, simplemente gastándonos dinero podemos ampliar este número muy fácilmente.

El problema, es que una vez alcanzado el límite del sistema, aplicar el mismo truco que antes (poner dos servidores en vez de uno)… no funciona excesivamente bien.

En el caso de las lecturas, asumíamos que podíamos replicarlo todo a todos los servidores para que cualquiera de ellos pueda atender cualquier petición. Esto nos lleva a que cada escritura que realicemos se tendrá que replicar a todos los servidores, con lo que… ¡no ganamos nada! Como cada escritura tiene que “pagarse” en todos los servidores, nuestra velocidad de escritura haciendo esto nunca aumentará.

Así pues, un replicado simple y obvio nos permite escalar la velocidad de lectura, pero no la de escritura. Si nos topamos con nuestro límite de velocidad de escritura… ¿cómo lo superaremos?

Hay varias alternativas.

Una bastante obvia es no replicar las escrituras a todos los servidores. Eso hará que cada servidor pueda escribir independientemente, con lo que combinaremos la velocidad de escritura de todos los servidores y aumentaremos nuestro rendimiento. Pero obviamente, no todos los datos estarán escritos en todos los servidores, y por tanto no podremos usar la estrategia para acelerar lecturas que comentábamos antes.

Lo que podemos hacer entonces es, en vez de repartir todas las peticiones entre todos los servidores, las repartiremos de otra manera, “particionando” la información. Por ejemplo, pondremos a los usuarios de cada país en un servidor diferente. Esto obviamente nos incrementará el rendimiento, siempre y cuando podamos hallar una partición adecuada.

El problema es que muchas veces, esto no es posible. Todos los usuarios de Twitter quieren acceder a los tuits de cualquier usuario, los usuarios están conectados entre sí de maneras arbitrarias, de manera que es imposible particionarlos de ninguna manera.

Cuando nos hallamos en esta situación, tenemos un problema, obviamente. La solución menos costosa es sacrificar la calidad de nuestros datos. Escribamos los datos a no todos los servidores, y repliquémoslo al resto “cuando podamos”. Aceptemos que algún servidor tendrá información desactualizada (pero que al menos podremos dar un servicio más o menos potable, pero rápido)… hagamos aproximaciones y aceptemos no dar datos 100% correctos e incluso aceptemos que algún dato puede perderse.

Lógicamente, esto no es factible de aplicar para cosas que requieran exactitud, como compras y pagos y cosas de este tipo… ¿pero para tuits y mensajes en Facebook? Pues probablemente sí.

En esto se fundamentan los sistemas NoSQL que prometen escalabilidad barata- una de las cosas en las que se fundamentan es en no proteger los datos con la paranoia habitual- hecho que debemos aceptar como sacrificio cuando los usamos como atajo a la escalabilidad.

En conclusión- escalar un sistema tiene un coste razonable para las cargas de consultas y visualización de información… los sistemas cuyo punto crítico sea este se pueden escalar mucho de una manera relativamente económica. En cambio, los sistemas cuyo cuello de botella sea la escritura son mucho más complicados de escalar adecuadamente- y uno de los costes que podemos pagar es el de la integridad de esos datos.

actualizado con los comentarios de mi editor habitual

Hazañas informáticas II: el modelo de datos relacional

Los primeros ordenadores se destinaron a la introducción y proceso de datos- no en vano los orígenes de IBM se remontan a la gestión del censo de habitantes de los Estados Unidos. Naturalmente, hasta el más primitivo de los sistemas de programación provee de primitivas de almacenamiento de datos, mecanismos sencillos para almacenar, organizar y acceder a datos en la memoria volátil del sistema (es decir, que estos datos se pierden al finalizar la ejecución del programa).

Rápidamente, los programadores se ocuparon de implementar funcionalidades que almacenaban estos datos en algún soporte persistente, de manera que los datos se conserven entre ejecución y ejecución del programa, y diseñaron maneras de organizar los datos de manera que las operaciones que se quieren realizar con ellos se hagan de una manera eficiente.

E. F. Codd, allá por 1969, planteó un sistema generalizado de almacenamiento y proceso de datos basado en unos principios muy sencillos.

Codd se dio cuenta que la información que típicamente se quería procesar se podía particionar en hechos simples, del siguiente estilo:

  • La factura #1 es a nombre de Javier Sánchez, a fecha 3/12/2007
  • La factura #2 es a nombre de Lucía Martínez, a fecha 5/12/2007
  • La línea 1 de la factura #32 es para “Mano de obra”, precio 300€
  • La línea 2 de la factura #32 es para “Materiales”, precio 450€

Podemos escribir estos hechos de una manera compacta empleando dos tablas:

Número de factura Nombre Fecha
1 Javier Sánchez 3/12/2007
2 Lucía Martínez 5/12/2007

 

Número de factura Línea Concepto Cantidad
32 1 Mano de obra 300€
32 2 Materiales 450€

Cuando usemos el modelo relacional, definiremos las “tablas” (relaciones) que usaremos en nuestra aplicación, especificando qué “columnas” (atributos) tendrán y qué tipo de valores podrán ponerse en cada columna (en el ejemplo anterior, números de factura, números de línea, nombres, conceptos, fechas y cantidades).

Los sistemas de bases de datos relacionales nos permiten definir con precisión estas tablas, insertar valores, actualizarlos y realizar consultas sobre ellos (e.g. ¿cuál es la fecha del último pedido de Miguel Hernández?), sin preocuparnos de la implementación real de esta estructura de datos, su almacenamiento persistente ni los algoritmos de búsqueda para las consultas.

El trabajo con las bases de datos relacionales se realiza a través de un lenguaje específico adaptado a ellas; el lenguaje más común se conoce como SQL (Structured Query Language) mediante el cuál podemos expresar cosas como:

create table facturas (
  numero_factura          numero,
  nombre                  texto,
  fecha_factura           fecha
);

Que define la tabla facturas con las columnas que hemos visto anteriormente, o:

insert into facturas(numero_factura, nombre, fecha_factura) values (1, 'Javier Sánchez', 3/12/2007);

, que nos permite insertar uno de los hechos que hemos tabulado antes y consultas como:

select max(fecha) from facturas where nombre = 'Miguel Hernández';

a la que nos referíamos anteriormente.

Fijémonos que estas consultas son “declarativas”, es decir que no hablan de cómo se deben almacenar estos datos ni realizarse estas operaciones, sino que sólo dicen “qué se quiere hacer”.

Un siguiente paso interesante en el trabajo con estas relaciones es darnos cuenta que podemos trabajar con varias tablas simultáneamente que comparten información. Por ejemplo, podríamos preguntarnos cuánto se ha gastado Ana Jiménez en concepto de mano de obra entre todos sus pedidos; la tabla de pedidos nos dice qué facturas corresponden a Ana Jiménez y la tabla de líneas de facturas nos dice qué líneas de factura son de mano de obra y por qué cantidad- podemos asociar las dos tablas mediante la columna número de factura presente en ambas. Mediante una sencilla abstracción matemática, podemos realizar consultas y operaciones sobre varias tablas utilizando las posibles asociaciones que haya entre ellas.

Como el modelo relacional es tremendamente abstracto, es apropiado para una gran cantidad de aplicaciones. Así pues, se han desarrollado implementaciones de bases de datos que utilizan el modelo relacional que podemos usar en nuestros programas en vez de tener que “reinventar la rueda” del almacenamiento de datos constantemente. Además, resulta que toda “mejora” que se introduzca en una de estas implementaciones se aprovecha inmediatamente por todas las aplicaciones que usen la implementación.  Por ejemplo, las operaciones entre varias tablas se pueden beneficiar de técnicas de optimización que hacen que las bases de datos sean muchísimo más rápidas que las implementaciones simples de estas operaciones.

Adicionalmente, las aplicaciones se benefician de otras características de los sistemas de gestión de base de datos, como son:

  • Restricciones de integridad. Podemos definir en la base de datos restricciones que deben cumplir los datos para considerarse correctos. Por ejemplo, una factura para ser correcta debe incluir un número correcto, un nombre y una fecha válida, evitando que se puedan introducir datos inválidos. Más allá, podemos decir cosas más interesantes como que el número de factura de una línea de factura debe corresponder a un número de factura en la tabla de facturas.
  • Transacciones. Muchas veces una serie de operaciones sobre una base de datos deben realizarse en conjunto o no realizarse. Por ejemplo, una transferencia bancaria debe incluir un cargo en la cuenta origen y un ingreso en la cuenta destino; claramente si una de las dos operaciones falla, la otra no puede realizarse.

Estas y otras facilidades que ofrecen las bases de datos no son triviales de implementar, y el hecho de poder aprovechar la implementación de la base de datos nos permite desarrollar nuestra aplicación más rápidamente sin tenernos que preocupar de estos complicados detalles.

Hoy en día, tras más de 40 de años desde su invención, el modelo relacional es el estándar de facto para almacenamiento de datos en aplicaciones donde la integridad de los datos es vital- y es también inmensamente popular en aplicaciones menos críticas por su conveniencia, sencillez y velocidad.

 


Database in Depth – Relational Theory for Practitioners

Database in Depth es un libro del mítico C. J. Date, que ha publicado una barbaridad de libros sobre bases de datos relacionales.

Yo me hallaba buscando un libro para avanzar mis conocimientos de bases de datos; principalmente para mejorar los diseños de esquemas y la manera de trabajar con ellos- seguramente desde un punto de vista teórico pero aplicable a nivel práctico.

En casi todos los mentideros de Internet le dirigen a uno hacia los libros de Date. La complicación es escoger uno, ya que aparentemente todos tienen temáticas y contenidos muy similares. Finalmente, me decidí por Database in Depth porque todo parecía indicar que era el que mejor ajustaba a mis intenciones.

En realidad, ahora creo que no lo es.

Database in Depth parte de la definición estricta del modelo relacional, enunciando y rebatiendo errores populares en la interpretación de las sagradas escrituras de Codd, y apuntando todos los pecados del SQL y sus implementaciones. Esto es interesante, ya que lo hace señalando las inconsistencias del SQL (por ejemplo, defiende la tesis de que NULL no debería existir,  a partir de contradicciones derivadas de su uso) y sus carencias.

Esto tiene un interés práctico, ya que posiblemente se puedan conseguir mejores (y sobre todo, más puros) modelos de datos intentándonos ajustar al modelo relacional verdadero, pero tiene el peligro de que, realmente, no existen implementaciones de éste. En efecto, Date defiende la intercambiabilidad de tablas y vistas, pero pocos gestores de bases de datos implementan actualizaciones sobre vistas, con lo que no se puede explotar este concepto como se debiera (e intentarlo puede llevarnos por el mal camino).

Date también trata temas de normalización, y su explicación es clara e interesante, y llena de argumentos nuevos para mi contra la popular denormalización (por ejemplo, apunta a las dificultades de escribir consultas correctas sobre esquemas denormalizados que estoy seguro que se le han escapado a más de uno), y que son bastante más aplicables a las implementaciones existentes.

En cuanto a la redacción del libro, Date es conciso y claro. No tiene miedo de expresar sus opiniones, pero creo que especifica muy bien cuando está siendo subjetivo. El tratamiento matemático de la teoría puede asustar a alguno, pero es completo y comprensible.

A parte de esto, el libro se me ha quedado corto. El libro no trata (o al menos yo no he sabido extraerlo) temas que me interesaban como el uso de claves artificiales, las consultas y la programación en general sobre bases de datos. A pesar de ello, los 19 eurillos que cuesta son relativamente económicos para un libro técnico, y al menos el contenido teórico ya los vale.