SQL

Juan Mellado, 27 Marzo, 2010 - 11:07

CassandraCassandra es una de las piezas de software clave dentro de las complejas infraestructuras de una red social de tanto éxito hoy en día como es Facebook. De hecho, es un desarrollo original de la propia Facebook que ha liberado como código abierto. Otros sitios tan populares como Twitter o Digg también están apostando por este software.

Cassandra se define a si misma como una base de datos distribuida de segunda generación altamente escalable. Sin embargo, el término "base de datos" resulta engañoso para los que estamos acostumbrados a trabajar con las tradicionales base de datos relacionales. Cassandra no sigue el modelo relacional. No es un "RDBMS". Es una base de datos "NoSQL".

¿Pero cómo son las bases de datos "NoSQL"? Pues una de sus principales características es que carecen de una estructura fija de tablas. Es decir, se componen de un conjunto de entidades básicas, pero estas carecen de una definición fija de atributos. Es como si cada fila de cada tabla, en un modelo relacional, pudiera tener el número de columnas que quisiera. Algo que de entrada resulta bastante extraño para los que estamos acostumbrados a trabajar con modelos relacionales. Y motivo por el que la mayor parte de los artículos que pueden encontrarse por Internet tratan de explicar como trabajar con estos nuevos modelos comparándolos con los antiguos relacionales.

El ejemplo comparativo más clásico es el propuesto por Jonathan Ellis, desarrollador principal de Cassandra, y que consiste en definir una base de datos para un servicio web, llamémoslo "Multiblogs", similar al popular Blogger, que permite a sus usuarios crearse sus propios blogs y dejar comentarios en los mismos. En Cassandra esta tarea es tan sencilla como editar un fichero de configuración llamado "storage-conf.xml" y añadir las siguientes líneas:

<Keyspace Name="Multiblogs">
  <ColumnFamily CompareWith="TimeUUIDType" Name="Blogs"/>
  <ColumnFamily CompareWith="TimeUUIDType" Name="Comments"/>
</Keyspace>

Y no hay más. En serio. ¿Y dónde están los nombres de los atributos? ¿Dónde están los clásicos "title", "author", ...? ¿Y los tipos de cada atributo? ¿Y se permiten NUMBER, VARCHAR, DATE, ...? ¿Y cómo se definen las claves primarias? ¿Y qué ocurre con las foreign keys? ¿Y ...? A mi particularmente la definición del modelo me resultó tan poco natural la primera vez que la ví, que creo que intentar explicarla en base al modelo relacional es un error que desvirtúa su verdadera naturaleza. Mejor ver primero como se estructura, y luego hacer las comparaciones.

Cassandra Data Model

El modelo de datos de Cassandra se compone de los siguientes elementos:

- Column: Es el elemento de menor granularidad al que se puede hacer referencia por un nombre. Como un pixel dentro de un bitmap. Lo interesante es que no es un valor escalar, sino un estructura compuesta por tres atributos: name: binary, value: binary y timestamp: int64.

Un ejemplo de una instancia de este tipo de estructura en notación JSON:

{ name: "address", value: "Baker Street", timestamp: 123456789 }

No hay restricciones con respecto a lo que pueden contener los atributos. Todos los valores los deben suministrar los clientes de la base de datos, incluido el timestamp. Como una "variable" definida dentro de un programa en cualquier lenguaje de programación. Y ahí reside precisamente la diferencia con los modelos relaciones tradicionales. El modelo apenas proporciona al gestor información acerca del dominio de la aplicación. No se define hasta el último detalle lo que puede insertarse o no en la base de datos. De hecho, en la práctica puede insertarse cualquier tupla de la forma (nombre, valor, fecha) que se quiera. Cassandra, a lo que se dedica en realidad, es a gestionar de forma muy eficiente colecciones distribuidas verdaderamente enormes de este tipo de tuplas, del orden de billones, de una manera altamente escalable.

El resto de elementos del modelo de datos de Cassandra sirven para agrupar estas simples tuplas.

- Super Column: Es una agregación de columns que puede referenciarse por un nombre. Se implementa como una estructura que se compone de dos atributos: name: binary y columns: list<Column>. En la práctica se la considera igual que una column, sólo que en vez de almacenar un valor almacena una lista de columns. Es un recurso bastante útil, ya que permite tener una colección de valores anidados asociados a otro valor.

Un ejemplo de una instancia de este tipo de estructura:

{ name: "profile",
  columns: {
    firstName: { name: "firstName", value: "Arthur", timestamp: 1 },
    lastName: { name: "lastName", value: "Doyle", timestamp: 2 },
    city: { name: "city", value: "London", timestamp: 3 }
  }
}

De igual forma que antes, no hay restricción acerca de lo que deben contener. Nada impediría insertar en el ejemplo una nueva column con cualquier tipo de información en tiempo de ejecución por parte del cliente.

Repito. El contenido de estas agrupaciones se define en tiempo de ejecución, en función del uso que las aplicaciones clientes hagan de ellas. Como una "estructura" definida dentro de un programa en cualquier lenguaje de lenguaje de programación. Evidentemente aportan una flexibilidad enorme a los modelos que ya se encuentran en producción, pero también un descontrol enorme si no se documenta exhaustivamente el uso que se quiere hacer de ellos.

- Row: Es una agregación de columns o super columns que se referencian con un nombre. No hay más, sólo un nombre contenido dentro de un vulgar String. Ese nombre es la "clave" (key) que identifica de forma unívoca a un registro.

En el siguiente ejemplo puede verse una row que agrupa tres columns, o lo que es lo mismo, tres columns almacenadas bajo una misma clave:

{ baskerville: {
    title: { name: "title", value: "Baskerville", timestamp: 0 },
    year: { name: "year", value: "1902", timestamp: 0 },
    publisher: { name: "publisher", value: "George", timestamp: 0 }
  }
}

En este punto es importante no confundir una row con una super column. La row es sólo una palabra "clave", no es una estructura, carece de atributos.

La mayoría de la gente parece sentirse cómoda pensando en las rows como el equivalente a la clave primaria de cada registro en las tablas del modelo relacional.

- Column Family: Es una agregación de rows que se referencia con un nombre. Y tampoco hay más, sólo un nombre contenido dentro de un vulgar String.

En el siguiente ejemplo puede verse una instancia de este tipo de colección, que consta de dos rows de super columns que a su vez contienen columns (he añadido comentarios por claridad):

{ friends: {  <-- Column Family
    watson: {  <-- Row
      profile: {  <-- Super Column
        name: "profile",
        columns: {  <-- Columns
          firstName: { name: "firstName", value: "John", timestamp: 0 },
          lastName: { name: "lastName", value: "Watson", timestamp: 0 }
        }
      },
      statistics: {  <-- Super Column
        name: "statistics",
        columns: {  <-- Columns
          marriages: { name: "marriages", value: "3", timestamp: 0 }
        }
      }
    },
    lestrade: {  <-- Row
      profile: {  <-- Super Column
        name: "profile",
        columns: {  <-- Columns
          lastName: { name: "lastName", value: "Lestrade", timestamp: 0 },
          phone: { name: "phone", value: "555-123", timestamp: 0 }
        }
      }
    }
  }
}

Insisto. No hay estructura predefinida. Cassandra lo gestiona todo como arrays asociativos. Como las propiedades de los objetos en JavaScript. O como las clases Dictionary en ActionScript. O como las clases HashMap en Java. Cassandra mantiene colecciones ordenadas de objetos a los que se accede por un nombre (clave), que en la terminología de Cassandra se llama row (key). El modelo en realidad es un hash de varias dimensiones, donde las columns se sitúan en el nivel más bajo, las super columns son un hash de columns, las rows son un hash de columns o super columns, y las column families son un hash de rows. Teniendo la cadena de nombres (claves) se puede acceder a todos los elementos que la componen.

Es importante destacar que los nombres de las column families que quieran utilizarse en Cassandra han de definirse en su fichero de configuración "storage-conf.xml". Y cuando se añade o elimina una column family en este fichero es necesario reiniciar el servidor. Además, para cada column family se debe especificar un método de ordenación de sus columns. O dos, si se compone de super columns, para indicar como se ordenan por defecto en primer lugar las super columns, y luego las columns que contienen cada una de ellas.

Eso de las ordenaciones es algo bastante importante, ya que Cassandra almacena físicamente cada column family en un fichero distinto ordenado por los criterios dados. En consecuencia, se debe tratar de almacenar la información relacionada en una misma column family, y ordenada de la forma en la que más habitualmente se vaya a acceder a ella.

La mayoría de la gente parece sentirse cómoda pensando en las column families como el equivalente a las tablas en el modelo relacional.

- Keyspace: Es una agregación de column families que puede referenciarse por un nombre. No es una estructura con atributos, es tan sólo otro contenedor al que se accede por un String con su nombre.

La mayoría de la gente parece sentirse cómoda pensando en las keyspaces como el equivalente a los esquemas (conjunto de tablas) en el modelo relacional.

- Cluster: Es el elemento de más alto nivel que puede referenciarse por un nombre. Es de naturaleza más física que los anteriores, más relacionado con el hardware, ya que agrupa los nodos (máquinas) sobre los que se ejecuta Cassandra. Puede contener uno o más keyspaces.

Y esa es a muy grandes rasgos toda la estructura del modelo. Que no es poco. Y espero que haya quedado claro a estas alturas que Cassandra almacena pares de datos compuestos por una clave y un valor (más un timestamp), que no asegura ningún tipo de integridad referencial, que no permite hacer joins, y es más, que no permite ejecutar ningún tipo de sentencia SQL. Todos los pares de valores que almacena se insertan/modifican/borran/recuperan con una cadena de la forma keyspace.columnfamily[row][column] o keyspace.columnfamily[row][supercolumn][column], aunque se permite operar de una sola vez con todas las columns de una clave o de una supercolumn, y también con rangos de claves.

No obstante, utilizar este esquema no significa renunciar a las buenas prácticas aprendidas a lo largo de años de uso de los tradicionales modelos relacionales. Por ejemplo, para las rows (claves) nada nos impide seguir utilizando por comodidad algún tipo de secuencial o UUID generado automáticamente, como se hace en la actualidad para las claves primarias con las columnas AUTOINCREMENT o las SEQUENCES de Oracle:

{ users: {  <-- Column Family
    359701f8-a75b-b801-05e7-86a970002701: {  <-- Row
      userName: { name: "userName", value: "jsmith", timestamp: 0 },
      firstName: { name: "firstName", value: "John", timestamp: 0 },
      lastName: { name: "lastName", value: "Smith", timestamp: 0 }
    },
    7805640e-e92c-52d7-a836-352755992000: {  <-- Row
      userName: { name: "userName", value: "ssmith", timestamp: 0 },
      firstName: { name: "firstName", value: "Sarah", timestamp: 0 },
      lastName: { name: "lastName", value: "Smith", timestamp: 0 }
    }
  }
}

De igual forma, el concepto de foreign key se debe seguir utilizando (aunque en ningún caso se garantice su integridad). Cassandra no pone ninguna restricción a la hora de que una misma clave que identifica una row aparezca en varias column families distintas. Es más, los desarrolladores enfatizan este hecho, ya que teniendo la clave de un objeto se pueden obtener todos los objetos relacionados en cualquier otra column family:

{ friends: {  <-- Column Family
    359701f8-a75b-b801-05e7-86a970002701: {  <-- Row
      7805640e-e92c-52d7-a836-352755992000: {  <-- Column
        name: "7805640e-e92c-52d7-a836-352755992000", value: "Girlfriend", ...},
      a9780021-4b91-786a-ee02-458a23761202: {  <-- Column
        name: "a9780021-4b91-786a-ee02-458a23761202", value: "Old chap", ...}
    },
    7805640e-e92c-52d7-a836-352755992000: {  <-- Row
      359701f8-a75b-b801-05e7-86a970002701: {  <-- Column
        name: "359701f8-a75b-b801-05e7-86a970002701", value: "Boyfriend", ...},
    }
  }
}

Y la necesidad de índices la podemos satisfacer creando column families específicas a nuestras necesidades. Para, por ejemplo, dado un "nombre de usuario" averiguar cual es su "clave":

{ usersIndex: {  <-- Column Family
    jsmith: {  <-- Row
      rowid: { name: "rowid", value: "359701f8-a75b-b801-05e7-86a970002701", ...}
    },
    ssmith: {  <-- Row
      rowid: { name: "rowid", value: "7805640e-e92c-52d7-a836-352755992000", ...}
    }
  }
}

Pero, ¿por qué las webs más grandes de Internet no utilizan base de datos tradicionales? ¿Acaso no tienen "usuarios", "artículos", "comentarios", ... y todo ese tipo de entidades que tan bien casan tabla por tabla con el modelo relacional? ¿MySQL y Oracle se están quedando obsoletos? ¿Por qué se necesita otro paradigma? No creo que se pueda responder con una sola palabra clave a todas estas preguntas. De hecho, si tuviera que hacerlo yo, no me decidiría por una única palabra, sino por dos: "rendimiento" y "escalabilidad".

Las base de datos relacionales no están respondiendo de forma adecuada a los retos que los grandes sitios webs actuales les plantean. Y por tanto se supone que tampoco lo harán a las necesidades de los futuros desarrollos que seguirán la línea marcada por estos. El problema no es que los modelos relacionales no puedan almacenar de forma ordenada la ingente cantidad de información que gestionan estos sitios. El problema es que sus tiempos de respuesta no son los más adecuados para lo que los usuarios demandan, y sus requerimientos de hardware no son lo suficientemente flexibles como para poder adecuarlos de forma óptima a las cambiantes necesidades del negocio. Las grandes webs se hartaron de desnormalizar sus modelos de datos e invertir dinero en mejorar el hardware de sus ordenadores, así que optaron por utilizar directamente modelos desnormalizados y altamente escalables horizontalmente (añadir más ordenadores a la red para aumentar el rendimiento).

Sin embargo, el párrafo anterior no viene a significar que haya que desechar todo el conocimiento adquirido hasta la fecha y rehacer los desarrollos actuales sobre modelos relacionales. No es eso. Lo que trata de resaltar es que a los grandes sitios de Internet las soluciones tradicionales no les valen. No hay ningún incoveniente en que las pequeñas, medianas y grandes empresas, que no tengan que manejar Petabytes de datos en tiempo real, puedan seguir trabajando con sus base de datos actuales. Lo importante es saber que esas propuestas alternativas están ahí, y conocer un mínimo acerca de la filosofía de funcionamiento de las mismas. Dentro un tiempo puede llegar a ser tan natural trabajar en una base de datos "NoSQL" como lo es hoy en día trabajar con un "RDBMS".

Para terminar, algunas páginas de referencia:

- http://wiki.apache.org/cassandra/API: API de Cassandra. Muy recomendable echarle un vistazo a las funciones, parámetros y valores retornados.

- http://wiki.apache.org/cassandra/ClientExamples: Ejemplos de clientes en varios lenguajes de programación. Muy recomendable para ver que en la práctica todo se reduce al uso del API.

- http://arin.me/blog/wtf-is-a-supercolumn-cassandra-data-model: Un artículo de un ingeniero de Digg describiendo el modelo de datos de Cassandra con bastantes ejemplos.

- http://blog.evanweaver.com/articles/2009/07/06/up-and-running-with-cassandra: Un artículo de naturaleza bastante práctica en el que se puede ver a Cassandra en funcionamiento.

Juan Mellado, 29 Marzo, 2007 - 20:26

OracleUn Database Link (DBLink) en Oracle es un tipo de objeto que permite realizar una conexión desde una base de datos a otra. Su principal objetivo es ocultar el detalle de los parámetros de conexión necesarios, facilitándonos un sencillo acceso a los recursos disponibles en otras bases de datos, independientemente de que estas se encuentren instaladas en el mismo servidor o no.

Aunque es un concepto sencillo de entender, suele generar bastante confusión cuando uno se tropieza con él por primera vez, sobre todo porque hay varios elementos involucrados en su creación y suelen plantearse dudas acerca de en qué base de datos concreta, de entre las dos que se quieren conectar, tienen que crearse dichos elementos.

Supongamos que tenemos dos bases de datos: productos y usuarios. La base de datos de productos almacena el inventario de una empresa, con los detalles de los artículos que oferta y entre los que se incluye el tipo de público al que se dirige cada artículo en particular. Por su parte la base de datos de usuarios contiene un ficha detallada de cada uno de los usuarios registrados en una web en la que se incluye la edad, sexo y lugar de residencia. En un determinado momento, la web decide realizar un estudio para comprobar que producto dentro de los ofertados por la empresa serían del agrado de sus usuarios en función de sus datos personales. Para realizar tal estudio decide contrastar la información de las dos bases de datos, y para ello deciden utilizar un DBLink que permita consultar datos de los productos desde la base de datos de usuarios. ¿Cuales serían los pasos a seguir?

En primer lugar, en la base de datos de productos, debería crearse un usuario nuevo, que será el que se utilice para acceder a los datos de productos. Dicho usuario será un usuario normal de Oracle, y debería tener como mínimo permiso para conectar e iniciar sesión en la base de datos de productos, y por supuesto para acceder a los objetos (tablas, vistas, ...) que contengan los datos que se quieren poder consultar desde la base de datos de usuarios. En un caso bastante típico de este tipo de accesos lo que se hará será crear un role para aglutinar en él los permisos, en vez de asignárselos directamente al usuario, y crear vistas específicas para los datos que deben poder consultarse, en vez de permitir que pueda accederse directamente a toda la base de datos.

CREATE ROLE rol;
GRANT CONNECT TO rol;
GRANT SELECT ON vista1 TO rol;
GRANT SELECT ON vista2 TO rol;
CREATE USER usuario IDENTIFIED BY clave;
GRANT rol TO usuario;

Y en segundo lugar, en la base de datos de usuarios, deberá crearse el database link para acceder a la base de datos de productos utilizando el usuario y clave recién creados en la otra base de datos.

CREATE PUBLIC DATABASE LINK db_productos
CONNECT TO usuario
IDENTIFIED BY clave
USING 'connect_string_productos';

Una vez creado el DBLink puede empezar a usarse sin más en la base de datos de usuarios con la siguiente sintaxis:

SELECT * FROM vista1@db_productos;

Aunque para simplificar las sentencias se puede crear un sinónimo en la base de datos de usuarios:

CREATE PUBLIC SYNONYM vista1_productos FOR vista1@db_productos;

De forma que pueda escribirse:

SELECT * FROM vista1_productos;
Juan Mellado, 7 Marzo, 2007 - 19:02

Continuando con la disertación de ayer acerca del modelado de una jerarquía, hoy voy a hablar de la implementación de la relación de agregación.

En el sistema de archivos que ponía ayer como ejemplo, la jerarquía en forma de árbol es bastante sencilla de implementar, ya que todos los elementos tienen un único padre y no se permiten ciclos. Para modelar tal tipo de relación basta con que cada elemento guarde una referencia a su padre. La cuestión a considerar es si resulta más conveniente añadir un nueva columna a las tablas de entidades o crear tablas aparte para guardar esas relaciones.

Si se decidiera modelar la herencia con dos tablas, entonces cada una de ellas debería tener una columna que hiciera referencia a la tabla de directorios para indicar cual es su padre; la tabla de ficheros haría referencia a la tabla de directorios, y la tabla de ficheros haría una auto-referencia a sí misma. Por el contrario, si se decidiera modelar la herencia con una sola tabla, entonces sólo existiría una auto-referencia a la propia tabla. En ambos casos la única excepción sería el nodo raíz de la jerarquía, que no tiene padre, y que por lo tanto obligaría a dejar vacía la columna y permitir que fuera NULL.

Las soluciones del párrafo anterior presentan una serie de inconvenientes. La primera, y que debería llamar enseguida nuestra atención, es el hecho de tener que dejar que la columna pueda tomar valor NULL. Hay algunos diseñadores que opinan que por lo general no es recomendable diseñar tablas con columnas que permitan valores nulos. Sobre todo si, como en este caso, lo que se quiere indicar con el valor nulo es que no existe una determinada relación entre entidades. La forma más correcta de modelar relaciones es a través de tablas intermedias que reflejen tal relación. Si no existe una determinada relación, entonces simplemente no existirá el registro correspondiente en la tabla de relación.

Juan Mellado, 6 Marzo, 2007 - 17:41

Un tipo de organización que suele utilizarse con bastante frecuencia en el mundo real es la jerarquía. Por ejemplo, la mayoría tenemos un jefe del que dependemos, y este a su vez tiene otro jefe. Desde nuestro punto de vista, nuestro jefe es nuestro responsable y nosotros sus empleados. Pero desde el punto de vista de nuestro jefe, él es a su vez un empleado de su propio jefe. La característica común que compartimos dentro de la jerarquía, y que nos hace pertenecer a ella, es que en realidad todos somos empleados. Nuestro puesto concreto de trabajo simplemente determina nuestra posición dentro de la jerarquía.

Con las familias ocurre algo parecido. Los padres tienen hijos que acaban convirtiéndose a su vez en padres, al tiempo que todos también son a un mismo tiempo hermanos, primos, tíos, cuñados, suegros, abuelos, ... Familiares, en resumidas cuentas.

El ejemplo paradigmático de este tipo de estructuras en informática son los sistemas de archivos, como los que utilizamos normalmente para organizar el contenido de nuestros discos duros, en donde un archivo puede ser, o bien un fichero, o bien un directorio. El quid del asunto está en que un directorio a su vez puede contener otros ficheros o directorios, lo que lo convierte por si mismo en un nuevo sistema de archivos dentro del sistema de archivos que lo contiene. La parte se convierte en un todo, de ahí el nombre de este tipo de estructuras.

Juan Mellado, 23 Febrero, 2007 - 22:20

SQLA veces tengo la impresión de que elegir las columnas que constituirán la clave primaria de una tabla es una decisión que suele tomarse muy a la ligera, cuando la realidad es que la correcta elección de las claves primarias es un factor decisivo para poder construir un modelo relacional de base de datos bien formado, coherente, mantenible y fácilmente ampliable. Lo más irónico del asunto es que en realidad resulta una tarea muy sencilla si se comprende que ningún valor tomado del dominio que estamos intentando modelar sirve para identificar unívocamente a los registros en base de datos. Dicho de otra forma, no puede utilizarse como clave primaria una columna que contenga información que tenga significado para los usuarios; hay que crear una columna expresamente para la clave primaria. O dicho de una tercera forma, si tiene que crear una tabla de personas no utilice DNI como clave primaria.

¿Pero por qué no es una buena idea elegir DNI como clave primaria?, ¿acaso ese número no nos identifica de forma unívoca?. Pues sí, pero rotundamente NO. Piense por ejemplo que los menores de edad o los ciudadanos de otros paises no tienen normalmente un carnet de identidad, y todo ello sin mencionar además el hecho de que los números del DNI no son únicos, históricamente la misma numeración se ha reutilizado en distintas personas.

Puede que en este momento esté pensando en alternativas muy inteligentes para tratar de resolver los problemas que se plantean en el párrafo anterior, posiblemente mediante la incorporación de nuevas columnas a la clave primaria, pero créame, déjelo, el esfuerzo será inútil, la realidad es que los problemas no desaparecerán, se incrementarán. Por ejemplo, podemos pensar que podremos almacenar menores de edad en nuestra tabla si añadimos una nueva columna a la clave primaria que nos indique si en realidad el DNI es suyo o de su representante legal. Pero ocurre que una persona puede tener varios hijos, con lo cual necesitariamos otra nueva columna para distinguirlos. También podemos pensar que podremos almacenar personas de distintos paises si añadimos una nueva columna que nos indique la nacionalidad de cada persona, permitiendo así además que un mismo número se repita para personas de distintos paises. Pero ocurre que una persona puede tener varias nacionalidades. Y así, vuelta a empezar. Lo normal es que siguiendo esta línea de razonamiento se acabe necesitando facilmente 7 u 8 columnas más.