Capítulo 4. El lenguaje de consulta de Cassandra

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

En este capítulo, comprenderás el modelo de datos de Cassandra y cómo se implementa mediante el Lenguaje de Consulta Cassandra (CQL). Mostraremos cómo CQL soporta los objetivos de diseño de Cassandra y veremos algunas características generales de su comportamiento .

Para los desarrolladores y administradores procedentes del mundo relacional, el modelo de datos Cassandra puede resultar difícil de entender inicialmente. Algunos términos, como keyspace, son completamente nuevos, y otros, como column, existen en ambos mundos pero tienen significados ligeramente distintos. La sintaxis de CQL es similar en muchos aspectos a SQL, pero con algunas diferencias importantes. Para quienes estén familiarizados con tecnologías NoSQL como Dynamo o Bigtable, también puede resultar confuso, porque aunque Cassandra se base en esas tecnologías, su propio modelo de datos es significativamente diferente.

Así que en este capítulo, partiremos de la terminología de las bases de datos relacionales e introduciremos la visión del mundo de Cassandra. Por el camino te familiarizarás con CQL y aprenderás cómo implementa este modelo de datos.

El modelo relacional de datos

En una base de datos relacional, la propia base de datos es el contenedor más externo que puede corresponder a una única aplicación. La base de datos contiene tablas. Las tablas tienen nombres y contienen una o varias columnas, que también tienen nombres. Cuando añades datos a una tabla, especificas un valor para cada columna definida; si no tienes un valor para una columna concreta, utilizas null. Esta nueva entrada añade una fila a la tabla, que más tarde podrás leer si conoces el identificador único de la fila (clave primaria), o utilizando una sentencia SQL que exprese algún criterio que pueda cumplir esa fila. Si quieres actualizar valores de la tabla, puedes actualizar todas las filas o sólo algunas de ellas, dependiendo del filtro que utilices en una cláusula "where" de tu sentencia SQL.

Después de este repaso, estás en buena forma para examinar el modelo de datos de Cassandra en cuanto a sus similitudes y diferencias.

El modelo de datos de Cassandra

En esta sección, adoptaremos un enfoque ascendente para comprender el modelo de datos de Cassandra.

El almacén de datos más sencillo con el que posiblemente quieras trabajar podría ser una matriz o lista. Tendría el aspecto de la Figura 4-1.

cdg3 0401
Figura 4-1. Una lista de valores

Si persistieras en esta lista, podrías consultarla más tarde, pero tendrías que examinar cada valor para saber lo que representa, o almacenar siempre cada valor en el mismo lugar de la lista y luego mantener externamente la documentación sobre qué celda de la matriz contiene cada valor. Eso significaría que tendrías que suministrar valores marcadores de posición vacíos (nulos) para mantener el tamaño predeterminado de la matriz en caso de que no tuvieras un valor para un atributo opcional (como un número de fax o de apartamento). Un array es una estructura de datos claramente útil, pero no rica semánticamente.

Ahora vamos a añadir una segunda dimensión a esta lista: nombres que coincidan con los valores. Dale nombres a cada celda, y ahora tienes una estructura de mapa, como se muestra en la Figura 4-2.

cdg3 0402
Figura 4-2. Un mapa de pares nombre/valor

Esto es una mejora porque puedes conocer los nombres de tus valores. Así, si decidieras que tu mapa contendría información del usuario, podrías tener nombres de columna como first_name, last_name, phone, email, etc. Se trata de una estructura algo más rica con la que trabajar.

Pero la estructura que has construido hasta ahora sólo funciona si tienes una instancia de una entidad determinada, como una sola persona, usuario, hotel o tuit. No te aporta gran cosa si quieres almacenar varias entidades con la misma estructura, que es sin duda lo que quieres hacer. No hay nada que unifique una colección de pares nombre/valor, ni forma de repetir los mismos nombres de columna. Así que necesitas algo que agrupe algunos de los valores de las columnas en un grupo claramente direccionable. Necesitas una clave que haga referencia a un grupo de columnas que deban tratarse juntas como un conjunto. Necesitas filas. Entonces, si obtienes una sola fila, puedes obtener todos los pares nombre/valor de una sola entidad a la vez, o sólo obtener los valores de los nombres que te interesan. Podrías llamar columnas a estos pares nombre/valor. Podrías llamar filas a cada entidad independiente que contenga algún conjunto de columnas. Y el identificador único de cada fila podría llamarse clave de fila o clave primaria. La Figura 4-3 muestra el contenido de una fila simple: una clave primaria, que es a su vez una o más columnas, y columnas adicionales. Volvamos enseguida a la clave primaria.

cdg3 0403
Figura 4-3. Una fila de Cassandra

Cassandra define una tabla como una división lógica que asocia datos similares. Por ejemplo, puedes tener una tabla user, una tabla hotel, una tabla address book, etc. De este modo, una tabla Cassandra es análoga a una tabla en el mundo relacional.

No necesitas almacenar un valor para cada columna cada vez que almacenas una nueva entidad. Puede que no conozcas los valores de cada columna para una entidad determinada. Por ejemplo, algunas personas tienen un segundo número de teléfono y otras no, y en un formulario online respaldado por Cassandra, puede haber algunos campos que sean opcionales y otros obligatorios. No pasa nada. En lugar de almacenar null para esos valores que no conoces, lo que desperdiciaría espacio, simplemente no almacenas esa columna en absoluto para esa fila. Así que ahora tienes una estructura de matriz multidimensional dispersa con el aspecto de la Figura 4-4. Esta estructura de datos flexible es característica de Cassandra y de otras bases de datos clasificadas como almacenes de columnas anchas.

Volvamos ahora a la discusión sobre las claves primarias en Cassandra, ya que se trata de un tema fundamental que afectará a tu comprensión de la arquitectura y el modelo de datos de Cassandra, de cómo Cassandra lee y escribe datos, y de cómo es capaz de escalar.

Cassandra utiliza un tipo especial de clave primaria llamada clave compuesta (o clave compuesta) para representar grupos de filas relacionadas, también llamadas particiones. La clave compuesta consta de una clave de partición, más un conjunto opcional de columnas de agrupación. La clave de partición se utiliza para determinar los nodos en los que se almacenan las filas y puede estar formada por varias columnas. Las columnas de agrupación se utilizan para controlar cómo se ordenan los datos para su almacenamiento dentro de una partición. Cassandra también admite una construcción adicional denominada columna estática, que sirve para almacenar datos que no forman parte de la clave primaria, pero que comparten todas las filas de una partición.

cdg3 0404
Figura 4-4. Una tabla Cassandra

La Figura 4-5 muestra cómo cada partición se identifica de forma única mediante una clave de partición, y cómo se utilizan las claves de agrupación para identificar de forma única las filas dentro de una partición. Observa que en el caso de que no se proporcionen columnas de agrupación, cada partición consta de una única fila.

cdg3 0405
Figura 4-5. Una tabla Cassandra con particiones

Juntando todos estos conceptos, tenemos las estructuras de datos básicas de Cassandra:

  • La columna, que es un par nombre/valor

  • La fila, que es un contenedor de columnas referenciadas por una clave primaria

  • La partición, que es un grupo de filas relacionadas que se almacenan juntas en los mismos nodos

  • La tabla, que es un contenedor de filas organizadas por particiones

  • El espacio de claves, que es un contenedor de tablas

  • El clúster, que es un contenedor de espacios de claves que abarca uno o varios nodos

Ése es el enfoque ascendente para ver el modelo de datos de Cassandra. Ahora que conoces la terminología básica, examinemos cada estructura con más detalle.

Agrupaciones

Como ya se ha dicho, la base de datos Cassandra está diseñada específicamente para distribuirse en varias máquinas que operan juntas y que aparecen como una única instancia para el usuario final. Así pues, la estructura más externa de Cassandra es el clúster, a veces llamado anillo, porque Cassandra asigna los datos a los nodos del clúster organizándolos en un anillo.

Espacios para llaves

Un clúster es un contenedor de espacios de claves. Un espacio de claves es el contenedor más externo para datos en Cassandra, que se corresponde estrechamente con una base de datos en el modelo relacional. Del mismo modo que una base de datos es un contenedor de tablas en el modelo relacional, un espacio de claves es un contenedor de tablas en el modelo de datos de Cassandra. Al igual que una base de datos relacional, un espacio de claves tiene un nombre y un conjunto de atributos que definen el comportamiento de todo el espacio de claves, como la replicación.

Como en este momento nos estamos centrando en el modelo de datos, dejaremos para más adelante las cuestiones relativas a la creación y configuración de clusters y espacios de claves. Examinaremos estos temas en el Capítulo 10.

Tablas

Una tabla es un contenedor para una colección ordenada de filas, cada una de las cuales es a su vez una colección ordenada de columnas. Las filas se organizan en particiones y se asignan a los nodos de un clúster Cassandra según la columna o columnas designadas como clave de partición. La ordenación de los datos dentro de una partición viene determinada por las columnas de agrupación.

Cuando escribes datos en una tabla de Cassandra, especificas valores para una o varias columnas. Esa colección de valores se denomina fila. Debes especificar un valor para cada una de las columnas contenidas en la clave primaria, ya que esas columnas juntas identificarán de forma única la fila.

Volvamos a la tabla user del capítulo anterior. Recuerda cómo escribiste una fila de datos y luego la leíste utilizando el comando SELECT en cqlsh:

cqlsh:my_keyspace> SELECT * FROM user where last_name = 'Nguyen';

 last_name | first_name | title
-----------+------------+-------
    Nguyen |       Bill |   Mr.

(1 rows)

Observarás en la última línea de salida que se ha devuelto una fila. Resulta ser la fila identificada por last_name "Nguyen" y first_name "Bill". Ésta es la clave primaria que identifica unívocamente esta fila.

Un punto interesante de la consulta anterior es que sólo especifica la clave de partición, lo que la convierte en una consulta que potencialmente podría devolver varias filas. Para ilustrar este punto, añadamos otro usuario con el mismo last_name y repitamos el comando SELECT anterior:

cqlsh:my_keyspace> INSERT INTO user (first_name, last_name, title)
  VALUES ('Wanda', 'Nguyen', 'Mrs.');
cqlsh:my_keyspace> SELECT * FROM user WHERE last_name='Nguyen';

 last_name | first_name | title
-----------+------------+-------
    Nguyen |       Bill |   Mr.
    Nguyen |      Wanda |  Mrs.

(2 rows)

Como puedes ver, al particionar los usuarios por last_name, has hecho posible cargar toda la partición en una sola consulta proporcionando ese last_name. Para acceder a una sola fila, tendrías que especificar toda la clave primaria:

cqlsh:my_keyspace> SELECT * FROM user WHERE last_name='Nguyen' AND
  first_name='Bill';

 last_name | first_name | title
-----------+------------+-------
    Nguyen |       Bill |   Mr.

(1 rows)

El acceso a los datos requiere una clave primaria

Para resumir este importante detalle: los comandos SELECT, INSERT, UPDATE, y DELETE en CQL operan todos en términos de filas. Para los comandos INSERT y UPDATE, todas las columnas de clave primaria deben especificarse mediante la cláusula WHERE para identificar la fila concreta que se ve afectada. Los comandos SELECT y DELETE pueden operar en términos de una o más filas dentro de una partición, una partición entera o incluso múltiples particiones utilizando las cláusulas WHERE y IN. Exploraremos estos comandos con más detalle en el Capítulo 9.

Aunque tienes que proporcionar un valor para cada columna de clave primaria cuando añadas una nueva fila a la tabla, no estás obligado a proporcionar valores para las columnas de clave no primaria. Para ilustrarlo, vamos a insertar otra fila sin title:

cqlsh:my_keyspace> INSERT INTO user (first_name, last_name)
               ... VALUES ('Mary', 'Rodriguez');
cqlsh:my_keyspace> SELECT * FROM user WHERE last_name='Rodriguez';

 last_name | first_name | title
-----------+------------+-------
 Rodriguez |       Mary |  null

(1 rows)

Como no has establecido un valor para title, el valor devuelto es null.

Ahora bien, si más adelante decides que también te gustaría llevar un registro de las iniciales del segundo nombre de los usuarios, puedes modificar la tabla user utilizando el comando ALTER TABLE y luego ver los resultados utilizando el comando DESCRIBE TABLE:

cqlsh:my_keyspace> ALTER TABLE user ADD middle_initial text;
cqlsh:my_keyspace> DESCRIBE TABLE user;

CREATE TABLE my_keyspace.user (
    last_name text,
    first_name text,
    middle_initial text,
    title text,
    PRIMARY KEY (last_name, first_name)
) ...

Verás que se ha añadido la columna middle_initial. Observa que hemos acortado la salida para omitir los distintos ajustes de la tabla. En el resto del libro aprenderás más sobre estos ajustes y cómo configurarlos.

Ahora, escribamos algunas filas adicionales, rellenemos diferentes columnas para cada una y leamos los resultados:

cqlsh:my_keyspace> INSERT INTO user (first_name, middle_initial, last_name,
  title)
  VALUES ('Bill', 'S', 'Nguyen', 'Mr.');
cqlsh:my_keyspace> INSERT INTO user (first_name, middle_initial, last_name,
  title)
  VALUES ('Bill', 'R', 'Nguyen', 'Mr.');
cqlsh:my_keyspace> SELECT * FROM user WHERE first_name='Bill' AND
  last_name='Nguyen';

 last_name | first_name | middle_initial | title
-----------+------------+----------------+-------
    Nguyen |       Bill |              R |   Mr.

(1 rows)

¿Era éste el resultado que esperabas? Si estás siguiéndolo de cerca, te habrás dado cuenta de que ambas sentencias INSERT especifican aquí una fila anterior identificada unívocamente por las columnas de clave primaria first_name y last_name. Como resultado, Cassandra ha actualizado fielmente la fila que indicaste, y tu SELECT sólo devolverá la fila única que coincide con esa clave primaria. Las dos sentencias INSERT sólo han servido para establecer primero y sobrescribir después la middle_initial.

Insertar, actualizar y volver a insertar

Como Cassandra utiliza un modelo de anexión, no hay ninguna diferencia fundamental entre las operaciones de inserción y actualización. Si insertas una fila que tiene la misma clave primaria que una fila existente, la fila se sustituye. Si actualizas una fila y la clave primaria no existe, Cassandra la crea.

Por esta razón, a menudo se dice que Cassandra admite upsert, lo que significa que las inserciones y las actualizaciones se tratan igual, con una pequeña excepción que trataremos en "Transacciones ligeras".

Visualicemos los datos que has insertado hasta este punto en la Figura 4-6. Observa que hay dos particiones, identificadas por los valores last_name de "Nguyen" y "Rodríguez". La partición "Nguyen" contiene las dos filas, "Bill" y "Wanda", y la fila de "Bill" contiene valores en las columnas title y middle_initial, mientras que "Wanda" sólo tiene especificado title y no middle_initial.

cdg3 0406
Figura 4-6. Datos insertados en la tabla user

Ahora que has aprendido más sobre la estructura de una tabla y has hecho algo de modelado de datos, vamos a profundizar en las columnas.

Columnas

Una columna es la unidad más básica de estructura de datos en el modelo de datos de Cassandra. Hasta ahora has visto que una columna contiene un nombre y un valor. Al definir la columna, obligas a que cada uno de los valores sea de un tipo determinado. Querrás profundizar en los distintos tipos disponibles para cada columna, pero antes echemos un vistazo a otros atributos de una columna de los que aún no hemos hablado: las marcas de tiempo y el tiempo de vida. Estos atributos son clave para comprender cómo Cassandra utiliza el tiempo para mantener los datos actualizados.

Marcas de tiempo

Cada vez que escribes datos en Cassandra, se genera una marca de tiempo, en microsegundos, para cada valor de columna que se inserta o actualiza. Internamente, Cassandra utiliza estas marcas de tiempo para resolver cualquier cambio conflictivo que se realice sobre el mismo valor, en lo que se conoce como un enfoque de última escritura gana.

Veamos las marcas de tiempo que se generaron para las escrituras anteriores añadiendo la función writetime() al comando SELECT para la columna title, además de un par de valores más para contextualizar:

cqlsh:my_keyspace> SELECT first_name, last_name, title, writetime(title)
  FROM user;

 first_name | last_name | title | writetime(title)
------------+-----------+-------+------------------
       Mary | Rodriguez |  null |             null
       Bill |    Nguyen |   Mr. | 1567876680189474
      Wanda |    Nguyen |  Mrs. | 1567874109804754

(3 rows)

Como era de esperar, no hay marca de tiempo para una columna que no se ha establecido. Podrías esperar que si preguntas por la marca de tiempo en first_name o last_name, obtendrías un resultado similar a los valores obtenidos para la columna title. Sin embargo, resulta que no puedes preguntar por la marca de tiempo en columnas de clave primaria:

cqlsh:my_keyspace> SELECT WRITETIME(first_name) FROM user;
InvalidRequest: code=2200 [Invalid query] message="Cannot use
  selection function writeTime on PRIMARY KEY part first_name"

Cassandra también te permite especificar una marca de tiempo que quieras utilizar al realizar escrituras. Para ello, utilizarás por primera vez el comando CQL UPDATE. Utiliza la opción opcional USING TIMESTAMP para establecer manualmente una marca de tiempo (ten en cuenta que la marca de tiempo debe ser posterior a la de tu comando SELECT, o se ignorará UPDATE ):

cqlsh:my_keyspace> UPDATE user USING TIMESTAMP 1567886623298243
  SET middle_initial = 'Q' WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT first_name, middle_initial, last_name,
  WRITETIME(middle_initial) FROM user WHERE first_name = 'Mary' AND
  last_name = 'Rodriguez';

 first_name | middle_initial | last_name | writetime(middle_initial)
------------+----------------+-----------+---------------------------
       Mary |              Q | Rodriguez |          1567886623298243

(1 rows)

Esta sentencia tiene el efecto de añadir la columna middle_initial y establecer la marca de tiempo en el valor que has proporcionado.

Trabajar con marcas de tiempo

Establecer la marca de tiempo no es necesario para las escrituras. Esta funcionalidad se utiliza normalmente para escrituras en las que existe la preocupación de que algunas de las escrituras puedan hacer que los datos frescos se sobrescriban con datos antiguos. Se trata de un comportamiento avanzado y debe utilizarse con precaución.

Actualmente no hay forma de convertir las marcas de tiempo producidas por writetime() a un formato más amigable en cqlsh.

Tiempo de vida (TTL)

Una característica muy potente que ofrece Cassandra es la posibilidad de caducar los datos que ya no se necesitan. Esta caducidad es muy flexible y funciona a nivel de valores de columna individuales. El tiempo de vida (o TTL) es un valor que Cassandra almacena para cada valor de columna para indicar cuánto tiempo debe conservarse el valor.

El valor TTL por defecto es null, lo que significa que los datos que se escriban no caducarán. Vamos a demostrarlo añadiendo la función TTL() a un comando SELECT en cqlsh para ver el valor TTL del título de María:

cqlsh:my_keyspace> SELECT first_name, last_name, TTL(title)
  FROM user WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

 first_name | last_name | ttl(title)
------------+-----------+------------
       Mary | Rodriguez |       null

(1 rows)

Ahora vamos a establecer el TTL de la columna middle_initial en una hora (3.600 segundos) añadiendo la opción USING TTL a tu comando UPDATE:

cqlsh:my_keyspace> UPDATE user USING TTL 3600 SET middle_initial =
  'Z' WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT first_name, middle_initial,
  last_name, TTL(middle_initial)
  FROM user WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

 first_name | middle_initial | last_name | ttl(middle_initial)
------------+----------------+-----------+---------------------
       Mary |              Z | Rodriguez |                3574

(1 rows)

Como puedes ver, el reloj ya está realizando la cuenta atrás de tu TTL, reflejando los varios segundos que tardaste en escribir el segundo comando. Si vuelves a ejecutar este comando dentro de una hora, el middle_initial de María se mostrará como null. También puedes establecer el TTL en INSERTS utilizando la misma opción USING TTL, en cuyo caso caducará toda la fila .

Puedes probar a insertar una fila utilizando TTL de 60 segundos y comprobar que la fila está ahí inicialmente:

cqlsh:my_keyspace> INSERT INTO user (first_name, last_name)
  VALUES ('Jeff', 'Carpenter') USING TTL 60;
cqlsh:my_keyspace> SELECT * FROM user WHERE first_name='Jeff' AND
  last_name='Carpenter';

 last_name | first_name | middle_initial | title
-----------+------------+----------------+-------
 Carpenter |       Jeff |           null |  null

(1 rows)

Después de esperar un minuto, la fila ya no está ahí:

cqlsh:my_keyspace> SELECT * FROM user WHERE first_name='Jeff' AND
  last_name='Carpenter';

 last_name | first_name | middle_initial | title
-----------+------------+----------------+-------

(0 rows)

Utilizar TTL

Recuerda que el TTL se almacena a nivel de columna para las columnas que no son de clave primaria. Actualmente no existe ningún mecanismo para establecer el TTL a nivel de fila directamente después de la inserción inicial; en su lugar, tendrías que volver a insertar la fila, aprovechando el comportamiento upsert de Cassandra. Al igual que con la marca de tiempo, no hay forma de obtener o establecer el valor TTL de una columna de clave primaria, y el TTL sólo puede establecerse para una columna cuando proporcionas un valor para la columna.

El comportamiento de la función TTL de Cassandra puede ser algo poco intuitivo, especialmente en los casos en los que estás actualizando una fila existente. El blog de Rahul Kumar "Cassandra TTL intricacies and usage by examples" hace un gran trabajo al resumir los efectos del TTL en varios casos diferentes.

Tipos CQL

Ahora que hemos profundizado en la forma en que Cassandra representa las columnas, incluidos los metadatos basados en el tiempo, veamos los distintos tipos que tienes a tu disposición para representar valores.

Como has visto anteriormente, cada columna de una tabla es de un tipo determinado. Hasta ahora, sólo has utilizado el tipo varchar, pero hay muchas otras opciones disponibles en CQL, así que vamos a explorarlas.

CQL admite un conjunto flexible de tipos de datos, incluidos tipos numéricos y de caracteres simples, colecciones y tipos definidos por el usuario. Describiremos estos tipos de datos y daremos algunos ejemplos de cómo pueden utilizarse para que aprendas a hacer la elección correcta para tu modelo de datos.

Tipos de datos numéricos

CQL admite los tipos numéricos que cabría esperar, incluidos los números enteros y los de coma flotante. Estos tipos son similares a los tipos estándar de Java y otros lenguajes:

int

Un entero con signo de 32 bits (como en Java)

bigint

Un entero largo con signo de 64 bits (equivalente a un Java long)

smallint

Un entero con signo de 16 bits (equivalente a un Java short)

tinyint

Un entero con signo de 8 bits (como en Java)

varint

Un entero con signo de precisión variable (equivalente a java.math.BigInteger)

float

Un punto flotante IEEE-754 de 32 bits (como en Java)

double

Un punto flotante IEEE-754 de 64 bits (como en Java)

decimal

Un decimal de precisión variable (equivalente a java.math.BigDecimal)

Tipos de enteros adicionales

Los tipos smallint y tinyint se añadieron en la versión Cassandra 2.2.

Aunque los tipos enumerados son habituales en muchos lenguajes, no existe un equivalente directo en CQL. Una práctica habitual es almacenar los valores enumerados como cadenas. Por ejemplo, en Java podrías utilizar el método Enum.name() para convertir un valor enumerado en un String para escribirlo en Cassandra como texto, y el método Enum.valueOf() para volver a convertir de texto al valor enumerado.

Tipos de datos textuales

CQL proporciona dos tipos de datos para representar texto, uno de los cuales ya has utilizado bastante (text):

text, varchar

Sinónimos de una cadena de caracteres UTF-8

ascii

Una cadena de caracteres ASCII

UTF-8 es el estándar de texto más reciente y ampliamente utilizado y admite la internacionalización, por lo que recomendamos utilizar text en lugar de ascii al construir tablas para datos nuevos. El tipo ascii es más útil si tratas con datos heredados que están en formato ASCII .

Establecer la configuración regional en cqlsh

Por defecto, cqlsh imprime el control y otros caracteres no imprimibles utilizando un escape de barra invertida. Puedes controlar cómo muestra cqlsh los caracteres no ASCII configurando la configuración regional con la variable de entorno $LANG antes de ejecutar la herramienta. Consulta el comando cqlsh HELP TEXT_OUTPUT para obtener más información.

Tipos de datos de tiempo e identidad

La identidad de los elementos de datos, como filas y particiones, es importante en cualquier modelo de datos para poder acceder a ellos. Cassandra proporciona varios tipos que resultan bastante útiles para definir claves de partición únicas. Dediquemos algún tiempo (juego de palabras intencionado) a profundizar en ellas:

timestamp

Aunque antes hemos señalado que cada columna tiene una marca de tiempo que indica cuándo se modificó por última vez, también puedes utilizar una marca de tiempo como valor de una columna en sí. La hora puede codificarse como un entero con signo de 64 bits, pero suele ser mucho más útil introducir una marca de tiempo utilizando uno de los varios formatos de fecha ISO 8601 admitidos. Por ejemplo:

2015-06-15 20:05-0700
   2015-06-15 20:05:07-0700
   2015-06-15 20:05:07.013-0700
   2015-06-15T20:05-0700
   2015-06-15T20:05:07-0700
   2015-06-15T20:05:07.013+-0700

La mejor práctica es proporcionar siempre las zonas horarias en lugar de confiar en la configuración de la zona horaria del sistema operativo.

date, time

Las versiones anteriores a Cassandra 2.1 sólo disponían del tipo timestamp para representar las horas, que incluían tanto una fecha como una hora del día. La versión 2.2 introdujo los tipos date y time que permitían representarlas independientemente; es decir, una fecha sin hora, y una hora del día sin referencia a una fecha concreta. Al igual que timestamp, estos tipos admiten los formatos ISO 8601.

Aunque hay nuevos tipos java.time disponibles en Java 8, el tipo date mapea a un tipo personalizado en Cassandra para preservar la compatibilidad con JDKs antiguos. El tipo time corresponde a un tipo Java long que representa el número de nanosegundos transcurridos desde medianoche.

uuid

Un identificador único universal (UUID) es un valor de 128 bits en el que los bits se ajustan a uno de varios tipos, de los cuales los más utilizados se conocen como Tipo 1 y Tipo 4. El tipo CQL uuid es un UUID de Tipo 4, que se basa totalmente en números aleatorios. Los UUID suelen representarse como secuencias de dígitos hexadecimales separadas por guiones. Por ejemplo

1a6300ca-0572-4736-a393-c0b7229e193e

El tipo uuid se utiliza a menudo como clave sustituta, por sí mismo o en combinación con otros valores.

Como los UUID tienen una longitud finita, no está absolutamente garantizado que sean únicos. Sin embargo, la mayoría de los sistemas operativos y lenguajes de programación proporcionan utilidades para generar UUIDs que proporcionan una unicidad adecuada. También puedes obtener un valor UUID de Tipo 4 mediante la función CQL uuid() y utilizar este valor en un INSERT o UPDATE.

timeuuid

Se trata de un UUID de Tipo 1, que se basa en la dirección MAC del ordenador, la hora del sistema y un número de secuencia utilizado para evitar duplicados. Este tipo se utiliza frecuentemente como marca de tiempo libre de conflictos. CQL proporciona varias funciones de conveniencia para interactuar con el tipo timeuuid: now(), dateOf(), y unixTimestampOf().

La disponibilidad de estas funciones de conveniencia es una de las razones por las que timeuuid tiende a utilizarse con más frecuencia que uuid.

Basándote en los ejemplos anteriores, podrías determinar que te gustaría asignar un ID único a cada usuario, ya que first_name quizá no sea una clave suficientemente única para la tabla user. Después de todo, es muy probable que en algún momento te encuentres con usuarios con el mismo nombre de pila. Si partieras de cero, podrías haber optado por hacer de este identificador tu clave primaria, pero por ahora lo añadirás como una columna más.

Las claves primarias son para siempre

Después de crear una tabla, no hay forma de modificar la clave primaria, porque ésta controla cómo se distribuyen los datos dentro del clúster y, lo que es aún más importante, cómo se almacenan en el disco.

Añadamos el identificador utilizando un uuid:

cqlsh:my_keyspace> ALTER TABLE user ADD id uuid;

A continuación, inserta un ID para María utilizando la función uuid() y luego visualiza los resultados:

cqlsh:my_keyspace> UPDATE user SET id = uuid() WHERE first_name =
  'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT first_name, id FROM user WHERE
  first_name = 'Mary' AND last_name = 'Rodriguez';

 first_name | id
------------+--------------------------------------
       Mary | ebf87fee-b372-4104-8a22-00c1252e3e05

(1 rows)

Observa que id está en formato UUID.

Ahora tienes un diseño de tabla más robusto, que puedes ampliar con aún más columnas a medida que conozcas más tipos.

Otros tipos de datos simples

CQL proporciona otros tipos de datos sencillos que no encajan bien en ninguna de las categorías anteriores:

boolean

Se trata de un simple valor verdadero/falso. El cqlsh no distingue entre mayúsculas y minúsculas al aceptar estos valores, pero da como salida True o False.

blob

Un objeto binario grande (blob) es un término informático coloquial para referirse a una matriz arbitraria de bytes. El tipo blob CQL es útil para almacenar archivos multimedia u otros tipos de archivos binarios. Cassandra no valida ni examina los bytes de un blob. CQL representa los datos como dígitos hexadecimales -por ejemplo, 0x00000ab83cf0. Si quieres codificar datos textuales arbitrarios en el blob, puedes utilizar la función textAsBlob() para especificar valores de entrada. Consulta la función de ayuda cqlsh HELP BLOB_INPUT para obtener más información.

inet

Este tipo representa direcciones de Internet IPv4 o IPv6. cqlsh acepta cualquier formato legal para definir direcciones IPv4, incluidas representaciones con o sin puntos que contengan valores decimales, octales o hexadecimales. Sin embargo, los valores se representan utilizando el formato decimal punteado en la salida cqlsh -por ejemplo, 192.0.2.235.

Las direcciones IPv6 se representan como ocho grupos de cuatro dígitos hexadecimales, separados por dos puntos: por ejemplo, 2001:0db8:85a3:0000:0000:8a2e:0370:7334. La especificación IPv6 permite el colapso de valores hexadecimales consecutivos de cero, por lo que el valor anterior se representa de la siguiente manera cuando se lee utilizando SELECT: 2001: db8:85a3:a::8a2e:370:7334.

counter

El tipo de datos counter proporciona un entero con signo de 64 bits, cuyo valor no puede establecerse directamente, sino sólo incrementarse o decrementarse. Cassandra es una de las pocas bases de datos que proporciona incrementos race-free entre centros de datos. Los contadores se utilizan con frecuencia para el seguimiento de estadísticas como el número de páginas vistas, tweets, mensajes de registro, etc. El tipo counter tiene algunas restricciones especiales. No puede utilizarse como parte de una clave primaria. Si se utiliza un contador, todas las columnas que no sean de clave primaria deben ser contadores.

Por ejemplo, podrías crear una tabla adicional para contar el número de veces que un usuario ha visitado un sitio web:

cqlsh:my_keyspace> CREATE TABLE user_visits (
  user_id uuid PRIMARY KEY, visits counter);

A continuación, incrementarías el valor para el usuario "María" según el ID único asignado previamente cada vez que visite el sitio:

cqlsh:my_keyspace> UPDATE user_visits SET visits = visits + 1
  WHERE user_id=ebf87fee-b372-4104-8a22-00c1252e3e05;

Y podrías leer el valor del contador igual que lees cualquier otra columna:

cqlsh:my_keyspace> SELECT visits from user_visits WHERE
  user_id=ebf87fee-b372-4104-8a22-00c1252e3e05;

 visits
--------
      1

(1 rows)

No existe ninguna operación para reiniciar un contador directamente, pero puedes aproximarte a una reinicialización leyendo el valor del contador y decrementando en ese valor. Por desgracia, no está garantizado que esto funcione perfectamente, ya que el contador puede haber cambiado en otro lugar entre la lectura y la escritura.

Advertencia sobre la idempotencia

Los operadores de incremento y decremento del contador no son idempotentes. Una operación idempotente es aquella que producirá el mismo resultado cuando se ejecute varias veces. Incrementar y decrementar no son idempotentes porque ejecutarlos varias veces podría dar lugar a resultados diferentes al aumentar o disminuir el valor almacenado.

Para ver cómo es posible, considera que Cassandra es un sistema distribuido en el que las interacciones a través de una red pueden fallar cuando un nodo no responde a una solicitud indicando éxito o fracaso. Una respuesta típica del cliente a esta petición es reintentar la operación. El resultado de reintentar una operación no idempotente, como incrementar un contador, no es predecible. Como no se sabe si el primer intento tuvo éxito, el valor puede haberse incrementado dos veces. Esto no es un fallo fatal, pero es algo que debes tener en cuenta cuando utilices contadores.

La única otra operación CQL que no es idempotente, además de incrementar o decrementar un contador, es añadir un elemento a list, de la que hablaremos a continuación.

Colecciones

Supongamos que quieres ampliar la tabla user para que admita varias direcciones de correo electrónico. Una forma de hacerlo sería crear columnas adicionales como email2, email3, etc. Aunque este enfoque funcionará, no es muy escalable y podría causar mucho trabajo de reelaboración. Es mucho más sencillo tratar las direcciones de correo electrónico como un grupo o "colección". CQL proporciona tres tipos de colecciones para ayudarte en estas situaciones: conjuntos, listas y mapas. Veamos ahora cada uno de ellos:

set

El tipo de datos set almacena una colección de elementos. Los elementos no están ordenados cuando se almacenan, pero se devuelven ordenados. Por ejemplo, los valores de texto se devuelven en orden alfabético. Los conjuntos pueden contener los tipos simples que has aprendido anteriormente, así como tipos definidos por el usuario (de los que hablaremos momentáneamente) e incluso otras colecciones. Una ventaja de utilizar set es la posibilidad de insertar elementos adicionales sin tener que leer primero el contenido.

Puedes modificar la tabla user para añadir un conjunto de direcciones de correo electrónico:

cqlsh:my_keyspace> ALTER TABLE user ADD emails set<text>;

A continuación, añade una dirección de correo electrónico para María y comprueba que se ha añadido correctamente:

cqlsh:my_keyspace> UPDATE user SET emails = { 'mary@example.com' }
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT emails FROM user WHERE first_name =
  'Mary' AND last_name = 'Rodriguez';

 emails
----------------------
 {'mary@example.com'}

(1 rows)

Ten en cuenta que al añadir esa primera dirección de correo electrónico, has sustituido el contenido anterior del conjunto, que en este caso era null. Puedes añadir otra dirección de correo electrónico más adelante sin sustituir todo el conjunto utilizando la concatenación:

cqlsh:my_keyspace> UPDATE user
  SET emails = emails + {'mary.rodriguez.AZ@gmail.com' }
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT emails FROM user
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

 emails
---------------------------------------------------
 {'mary.mcdonald.AZ@gmail.com', 'mary@example.com'}

(1 rows)

Otras operaciones de conjunto

También puedes borrar elementos del conjunto utilizando el operador de resta: SET emails = emails - {'mary@example.com'}.

También puedes vaciar todo el conjunto utilizando la notación de conjunto vacío: SET emails = {}.

list

El tipo de datos list contiene una lista ordenada de elementos. Por defecto, los valores se almacenan en orden de inserción. Puedes modificar la tabla user para añadir una lista de números de teléfono:

cqlsh:my_keyspace> ALTER TABLE user ADD phone_numbers list<text>;

A continuación, añade un número de teléfono para María y comprueba que se ha añadido correctamente:

cqlsh:my_keyspace> UPDATE user SET phone_numbers = ['1-800-999-9999' ]
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE
  first_name = 'Mary' AND last_name = 'Rodriguez';

 phone_numbers
--------------------
 ['1-800-999-9999']

(1 rows)

Añadamos un segundo número añadiéndolo:

cqlsh:my_keyspace> UPDATE user SET phone_numbers =
  phone_numbers + [ '480-111-1111' ]
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE
  first_name = 'Mary' AND last_name = 'Rodriguez';

 phone_numbers
------------------------------------
 ['1-800-999-9999', '480-111-1111']

(1 rows)

El segundo número que has añadido aparece ahora al final de la lista.

Nota

También podrías haber antepuesto el número al principio de la lista invirtiendo el orden de los valores: SET phone_numbers = [‘4801234567'] + phone_numbers.

Puedes sustituir un elemento individual de la lista cuando hagas referencia a él por su índice:

cqlsh:my_keyspace> UPDATE user SET phone_numbers[1] = '480-111-1111'
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

Al igual que con los conjuntos, también puedes utilizar el operador resta para eliminar elementos que coincidan con un valor especificado:

cqlsh:my_keyspace> UPDATE user SET phone_numbers =
  phone_numbers - [ '480-111-1111' ]
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

Por último, puedes eliminar directamente un elemento concreto utilizando su índice:

cqlsh:my_keyspace> DELETE phone_numbers[0] from user WHERE
  first_name = 'Mary' AND last_name = 'Rodriguez';

Operaciones de lista caras

Dado que una lista almacena valores según su posición, existe la posibilidad de que la actualización o eliminación de un elemento concreto de una lista requiera que Cassandra lea toda la lista, realice la operación solicitada y vuelva a escribir toda la lista. Esto podría ser una operación costosa si tienes un gran número de valores en la lista. Por esta razón, muchos usuarios prefieren utilizar los tipos set o map, especialmente en los casos en los que existe la posibilidad de actualizar el contenido de la colección.

map

El tipo de datos map contiene una colección de pares clave-valor. Las claves y los valores pueden ser de cualquier tipo excepto counter. Vamos a probarlo utilizando un map para almacenar información sobre los inicios de sesión de los usuarios. Crea una columna para registrar el tiempo de la sesión de inicio de sesión, en segundos, con un timeuuid como clave:

cqlsh:my_keyspace> ALTER TABLE user ADD
  login_sessions map<timeuuid, int>;

Luego puedes añadir un par de sesiones de acceso para María y ver los resultados:

cqlsh:my_keyspace> UPDATE user SET login_sessions =
  { now(): 13, now(): 18}
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT login_sessions FROM user
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

 login_sessions
-----------------------------------------------
 {839b2660-d1c0-11e9-8309-6d2c86545d91: 13,
  839b2661-d1c0-11e9-8309-6d2c86545d91: 18}

(1 rows)

También podemos hacer referencia a un elemento individual del mapa utilizando su clave.

Los tipos colección son muy útiles en los casos en que necesitamos almacenar un número variable de elementos dentro de una misma columna.

Tuplas

Ahora podrías decidir que necesitas hacer un seguimiento de las direcciones físicas de tus usuarios. Podrías utilizar una única columna de texto para almacenar estos valores, pero eso haría recaer sobre la aplicación la carga de analizar los distintos componentes de la dirección. Sería mejor que pudieras definir una estructura en la que almacenar las direcciones para mantener la integridad de los distintos componentes.

Afortunadamente, Cassandra proporciona dos formas distintas de gestionar estructuras de datos más complejas: las tuplas y los tipos definidos por el usuario.

En primer lugar, echemos un vistazo a las tuplas, que proporcionan una forma de tener un conjunto de longitud fija de valores de varios tipos. Por ejemplo, podrías añadir una columna de tupla a la tabla user que almacene una dirección. Podrías haber añadido una tupla para definir direcciones, suponiendo un formato de dirección de tres líneas y un código postal entero, como un código postal de EE.UU:

cqlsh:my_keyspace> ALTER TABLE user ADD
  address tuple<text, text, text, int>;

Entonces podrías rellenar una dirección utilizando la siguiente declaración:

cqlsh:my_keyspace> UPDATE user SET address =
  ('7712 E. Broadway', 'Tucson', 'AZ', 85715 )
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

Esto te proporciona la posibilidad de almacenar una dirección, pero puede resultar un poco incómodo intentar recordar los valores posicionales de los distintos campos de una tupla sin tener un nombre asociado a cada valor. Tampoco hay forma de actualizar los campos individuales de una tupla; hay que actualizar toda la tupla. Por estas razones, las tuplas se utilizan con poca frecuencia en la práctica, porque Cassandra ofrece una alternativa que proporciona una forma de nombrar y acceder a cada valor, que examinaremos a continuación.

Pero primero, vamos a utilizar el comando CQL DROP para deshacerte de la columna address y poder sustituirla por algo mejor:

cqlsh:my_keyspace> ALTER TABLE user DROP address;

Tipos definidos por el usuario

Cassandra te ofrece una forma de definir tus propios tipos para ampliar su modelo de datos. Estos tipos definidos por el usuario (UDT) son más fáciles de utilizar que las tuplas, ya que puedes especificar los valores por su nombre en lugar de por su posición. Crea tu propio tipo de dirección:

cqlsh:my_keyspace> CREATE TYPE address (
  street text,
  city text,
  state text,
  zip_code int);

Un UDT tiene el ámbito del espacio de claves en el que está definido. Podrías haber escrito CREATE TYPE my_keyspace.address. Si ejecutas el comando DESCRIBE KEYSPACE my_keyspace, verás que el tipo de dirección forma parte de la definición del espacio de claves.

Ahora que has definido el tipo address, puedes utilizarlo en la tabla user. En lugar de añadir simplemente una única dirección, puedes utilizar un mapa para almacenar varias direcciones a las que puedes dar nombres como "casa", "trabajo", etc. Sin embargo, inmediatamente te encuentras con un problema:

cqlsh:my_keyspace> ALTER TABLE user ADD
  addresses map<text, address>;
InvalidRequest: code=2200 [Invalid query] message="Non-frozen
  collections are not allowed inside collections: map<text,
  address>"

¿Qué ocurre aquí? Resulta que un tipo de datos definido por el usuario se considera una colección, ya que su implementación es similar a la de set, list o map. Le has pedido a Cassandra que anide una colección dentro de otra.

Congelar Colecciones

Las versiones de Cassandra anteriores a la 2.2 no admiten totalmente el anidamiento de colecciones. En concreto, aún no es posible acceder a los atributos individuales de una colección anidada, porque la implementación serializa la colección anidada como un único objeto. Por lo tanto, toda la colección anidada debe leerse y escribirse en su totalidad.

La congelación es un concepto que se introdujo como mecanismo de compatibilidad futura. Por ahora, puedes anidar una colección dentro de otra marcándola como frozen, lo que significa que Cassandra almacenará ese valor como un blob de datos binarios. En el futuro, cuando las colecciones anidadas sean totalmente compatibles, habrá un mecanismo para "descongelar" las colecciones anidadas, permitiendo acceder a los atributos individuales.

También puedes utilizar una colección como clave primaria si está congelada.

Ahora que hemos dado un pequeño rodeo para hablar de la congelación y las colecciones anidadas, volvamos a modificar tu tabla, esta vez marcando la dirección como congelada:

cqlsh:my_keyspace> ALTER TABLE user ADD addresses map<text,
  frozen<address>>;

Ahora vamos a añadir una dirección particular para María:

cqlsh:my_keyspace> UPDATE user SET addresses = addresses +
  {'home': { street: '7712 E. Broadway', city: 'Tucson',
  state: 'AZ', zip_code: 85715 } }
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';
cqlsh:my_keyspace> SELECT addresses FROM user
  WHERE first_name = 'Mary' AND last_name = 'Rodriguez';

 addresses
---------------------------------------------------------
 {'home': {street: '7712 E. Broadway',
           city: 'Tucson', state: 'AZ', zip_code: 85715}}

(1 rows)

Ahora que ya conoces los distintos tipos, vamos a dar un paso atrás y examinar las tablas que has creado hasta ahora describiendo my_keyspace:

cqlsh:my_keyspace> DESCRIBE KEYSPACE my_keyspace ;

CREATE KEYSPACE my_keyspace WITH replication = {'class':
  'SimpleStrategy', 'replication_factor': '1') AND
  durable_writes = true;

CREATE TYPE my_keyspace.address (
    street text,
    city text,
    state text,
    zip_code int
);

CREATE TABLE my_keyspace.user (
    last_name text,
    first_name text,
    addresses map<text, frozen<address>>,
    emails set<text>,
    id uuid,
    login_sessions map<timeuuid, int>,
    middle_initial text,
    phone_numbers list<text>,
    title text,
    PRIMARY KEY (last_name, first_name)
) WITH CLUSTERING ORDER BY (first_name ASC)
    AND bloom_filter_fp_chance = 0.01
    AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
    AND comment = ''
    AND compaction = {'class': 'org.apache.cassandra.db.compaction
      .SizeTieredCompactionStrategy', 'max_threshold': '32',
      'min_threshold': '4'}
    AND compression = {'chunk_length_in_kb': '16', 'class':
      'org.apache.cassandra.io.compress.LZ4Compressor'}
    AND crc_check_chance = 1.0
    AND dclocal_read_repair_chance = 0.1
    AND default_time_to_live = 0
    AND gc_grace_seconds = 864000
    AND max_index_interval = 2048
    AND memtable_flush_period_in_ms = 0
    AND min_index_interval = 128
    AND read_repair_chance = 0.0
    AND speculative_retry = '99PERCENTILE';

CREATE TABLE my_keyspace.user_visits (
    user_id uuid PRIMARY KEY,
    visits counter
) WITH bloom_filter_fp_chance = 0.01
    AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
    AND comment = ''
    AND compaction = {'class': 'org.apache.cassandra.db.compaction
      .SizeTieredCompactionStrategy', 'max_threshold': '32',
      'min_threshold': '4'}
    AND compression = {'chunk_length_in_kb': '16', 'class':
      'org.apache.cassandra.io.compress.LZ4Compressor'}
    AND crc_check_chance = 1.0
    AND dclocal_read_repair_chance = 0.1
    AND default_time_to_live = 0
    AND gc_grace_seconds = 864000
    AND max_index_interval = 2048
    AND memtable_flush_period_in_ms = 0
    AND min_index_interval = 128
    AND read_repair_chance = 0.0
    AND speculative_retry = '99PERCENTILE';

Practicar los comandos CQL

Los comandos enumerados en este capítulo para operar con la tabla user están disponibles como gist en GitHub para facilitarte su ejecución. El archivo se llama cqlsh_intro.cql.

Resumen

En este capítulo, has hecho un rápido recorrido por el modelo de datos de Cassandra de clusters, espacios de claves, tablas, claves, filas y columnas. En el proceso, aprendiste mucha sintaxis CQL y adquiriste más experiencia trabajando con tablas y columnas en cqlsh. Si te interesa profundizar en CQL, puedes leer la especificación completa del lenguaje.

Get Cassandra: La Guía Definitiva, (Revisada) Tercera Edición, 3ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.