Esquema y modelo de datos

Resumen del modelo de datos

Una base de datos de Cloud Spanner puede contener una o más tablas. Estas tablas son similares a las de las bases de datos relacionales, ya que están estructuradas con filas, columnas y valores, y contienen claves primarias. En Cloud Spanner, se aplican tipos estrictos de datos. Debes definir un esquema para cada base de datos, en el que se especifiquen los tipos de datos de todas las columnas de cada tabla. Entre los tipos de datos permitidos, se incluyen los escalares y de arreglo, que se explican con más detalle en este artículo. También puedes definir uno o más índices secundarios en una tabla.

Relaciones entre tablas superiores y secundarias

Existen dos formas de definir relaciones entre elementos primarios y secundarios en Cloud Spanner: intercalación de tablas y claves externas.

La intercalación de tablas de Cloud Spanner es una buena opción para muchas relaciones entre tablas principales y secundarias en las que la clave primaria de la tabla secundaria incluye las columnas de clave primaria de la tabla principal. La ubicación conjunta de las filas secundarias con sus filas principales puede mejorar significativamente el rendimiento. Por ejemplo, si tienes las tablas Customers y Invoices, y tu aplicación recupera con frecuencia todas las facturas de un cliente determinado, puedes definir Invoices como tabla secundaria de Customers. De esta manera, declaras una relación de localidad de datos entre dos tablas que son independientes en cuanto a lógica; es decir, le indicas a Cloud Spanner que almacene físicamente una o más filas de Invoices junto a una fila de Customers.

Para obtener un análisis más detallado sobre la intercalación, consulta Crea tablas intercaladas a continuación.

Las claves externas son una solución principal-secundaria más general y abordan casos prácticos adicionales. No se limitan a las columnas de clave primaria, y las tablas pueden tener varias relaciones de clave externa, tanto en una relación principal como secundaria en otras. Sin embargo, una relación de clave externa no implica la ubicación conjunta de las tablas en la capa de almacenamiento.

Para obtener más información sobre las claves externas y su comparación con las tablas intercaladas, consulta Descripción general de las claves externas.

Claves primarias

¿Cómo puedes indicarle a Cloud Spanner qué filas de Invoices y Customers deben almacenarse juntas? Mediante la clave primaria de ambas tablas. Cada tabla debe tener una clave primaria, y esta clave puede estar compuesta por cero o más columnas de esa tabla. Si declaras que una tabla es la secundaria de otra, las columnas de clave primaria de la tabla superior deben ser el prefijo de la clave primaria de la tabla secundaria. Esto significa que si la clave primaria de una tabla superior está compuesta por N columnas, la clave primaria de cada una de sus tablas secundarias también debe estar compuesta por esas mismas N columnas, en el mismo orden y comenzando con la misma columna.

Cloud Spanner almacena las filas ordenadas por valores de clave primaria, con filas secundarias insertadas entre las filas superiores que comparten el mismo prefijo de clave primaria. Esta inserción de filas secundarias entre filas superiores a lo largo de la dimensión de la clave primaria se denomina intercalación, y las tablas secundarias también se denominan tablas intercaladas (consulta una ilustración de filas intercaladas en la sección Crea tablas intercaladas de más abajo).

En resumen, Cloud Spanner puede ubicar físicamente juntas las filas de las tablas relacionadas. En los ejemplos de esquema que aparecen más abajo, se ilustra esta disposición física.

Elige una clave primaria

La clave primaria identifica de manera única cada fila de una tabla. Si quieres actualizar o borrar las filas existentes de una tabla, esta debe tener una clave primaria compuesta por una o más columnas (una tabla sin columnas de clave primaria solo puede tener una fila). Es posible que tu aplicación ya tenga un campo que sirva naturalmente como clave primaria. En el ejemplo anterior de la tabla Customers, podría haber un CustomerId proporcionado por la aplicación que puede funcionar bien como clave primaria. En otros casos, es posible que debas generar una clave primaria cuando insertes la fila, como un valor INT64 único que generes.

En todos los casos, debes tener cuidado de no crear hotspots con la clave primaria que elijas. Por ejemplo, si insertas registros con una clave que es un número entero que aumenta monótonamente, siempre se insertará al final del espacio de claves. Esta opción es poco recomendable, ya que Cloud Spanner divide los datos entre servidores según los intervalos de claves. Esto significa que las inserciones se dirigirán a un solo servidor y se creará un hotspot. Existen técnicas que pueden distribuir la carga entre varios servidores y evitar los hotspots:

  • Genera un hash de la clave y almacénalo en una columna. Usa la columna de hash (o la columna de hash más la columna de la clave única) como clave primaria.
  • Cambia el orden de las columnas en la clave primaria.
  • Usa un identificador único universal (UUID). Se recomienda usar un UUID de versión 4, ya que esta utiliza valores aleatorios en los bits de orden superior. No uses un algoritmo de UUID (como uno de versión 1) que almacene la marca de tiempo en los bits de orden superior.
  • Revierte los bits de los valores secuenciales.

Divisiones de bases de datos

Puedes definir jerarquías de relaciones superiores y secundarias entre tablas de hasta siete capas de profundidad, lo que significa que puedes ubicar juntas las filas de siete tablas independientes en cuanto a lógica. Si el tamaño de los datos de las tablas es pequeño, es probable que un solo servidor de Cloud Spanner administre tu base de datos. Pero ¿qué sucede cuando crecen las tablas relacionadas y comienzan a alcanzar los límites de recursos de un servidor individual? Cloud Spanner es una base de datos distribuida, lo que significa que, a medida que crecen las bases de datos, Cloud Spanner las divide en fragmentos llamados “divisiones”. Cada división se puede mover de forma independiente y asignarse a diferentes servidores, que pueden encontrarse en ubicaciones físicas distintas. Una división es un rango de filas de una tabla de nivel superior (en otras palabras, no intercalada), en la que las filas se ordenan según la clave primaria. Las claves de inicio y fin de este rango se denominan “límites de división”. Cloud Spanner agrega y quita automáticamente estos límites de división, lo que cambia la cantidad de divisiones de la base de datos.

Cloud Spanner divide los datos en función de la carga; es decir, agrega automáticamente límites de división cuando detecta una gran cantidad de operaciones de lectura o escritura en muchas claves de una división. Tienes cierto control sobre cómo se dividen los datos, ya que Cloud Spanner solo puede crear límites de división entre las filas de las tablas que encuentran en la raíz de una jerarquía (es decir, tablas que no estén intercaladas en una tabla superior). Además, las filas de una tabla intercalada no se pueden separar de su fila correspondiente en la tabla superior porque las filas de la tabla intercalada se almacenan en orden de clave primaria junto con la fila de su tabla superior que comparte el mismo prefijo de clave primaria (consulta la ilustración de filas intercaladas de la sección Cómo crear una jerarquía de tablas intercaladas). Por lo tanto, las relaciones de tablas superiores y secundarias que definas, junto con los valores de clave primaria que establezcas para las filas de las tablas relacionadas, te permiten controlar cómo se dividen los datos de forma interna.

División basada en la carga

Veamos un ejemplo de cómo Cloud Spanner realiza la división basada en la carga para mitigar los hotspots de lectura. Supongamos que tienes una base de datos que contiene una tabla con 10 filas que se leen con más frecuencia que el resto. Siempre y cuando esa tabla esté en la raíz de la jerarquía de la base de datos (es decir, que no sea una tabla intercalada), Cloud Spanner puede agregar límites de división entre cada una de esas 10 filas para que un servidor diferente las administre, en lugar de permitir que todas las operaciones de lectura de esas filas consuman los recursos de un único servidor.

Como regla general, si sigues las prácticas recomendadas para el diseño de esquemas, Cloud Spanner puede mitigar los hotspots de las operaciones de lectura que se dirigen a filas de una tabla no intercalada, de modo que la capacidad de procesamiento de lectura debería mejorar cada pocos minutos hasta que se saturen los recursos de la instancia o te encuentres con casos en los que no se puedan agregar nuevos límites de división (por ejemplo, si tienes una división que abarca solo una fila y sus elementos secundarios intercalados).

Ejemplos de esquemas

En los siguientes ejemplos de esquema, se muestra cómo crear tablas de Cloud Spanner con y sin relaciones jerárquicas y se ilustran las disposiciones físicas correspondientes de los datos.

Crea una tabla

Supongamos que quieres crear una aplicación de música y necesitas una tabla simple que almacene filas de datos de cantantes:

Tabla de cantantes con 5 filas y 4 columnas

Vista lógica de las filas de una tabla sencilla llamada Singers. La columna de la clave primaria aparece a la izquierda de la línea en negrita.

Ten en cuenta que la tabla contiene una columna de clave primaria, SingerId, que aparece a la izquierda de la línea en negrita, y que las tablas están organizadas por filas, columnas y valores.

Puedes definir la tabla con un esquema de Cloud Spanner como este:

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

Ten en cuenta lo siguiente sobre el esquema de ejemplo:

  • Singers es una tabla que se encuentra en la raíz de la jerarquía de la base de datos (porque no está definida como secundaria de otra tabla).
  • Las columnas de clave primaria suelen tener la anotación NOT NULL (aunque puedes omitirla si deseas permitir valores NULL en columnas de clave. Obtén más información en Columnas de clave).
  • Las columnas que no se incluyen en la clave primaria se denominan columnas sin clave y pueden tener una anotación NOT NULL opcional.
  • Las columnas que usan los tipos STRING o BYTES deben definirse con una longitud, que representa la cantidad máxima de caracteres Unicode que se pueden almacenar en el campo (obtén más detalles en la sección sobre los tipos de datos escalares).

¿Cómo es la disposición física de las filas de la tabla Singers? En el siguiente diagrama, se muestran las filas de la tabla Singers almacenadas por clave primaria contigua (o sea, ordenadas según ese valor). Es decir, “Singers(1)”, luego “Singers(2)”, y así sucesivamente, en donde “Singers(1)” representa la fila de la tabla Singers que tiene la clave 1.

Filas de ejemplo de una tabla almacenada en orden de clave contigua

Disposición física de las filas de la tabla Singers, con un límite de división de ejemplo que causa que distintos servidores administren las divisiones

En el diagrama anterior, también se muestran posibles límites de división, que pueden producirse entre cualquier fila de Singers, porque Singers está en la raíz de la jerarquía de la base de datos. También se muestra un límite de división de ejemplo entre las filas con las claves Singers(3) y Singers(4), que causa que se asignen a distintos servidores los datos de las divisiones creadas. Esto significa que a, medida que aumenta esta tabla, es posible que se almacenen en diferentes ubicaciones las filas de datos de Singers.

Crea varias tablas

Supongamos que ahora quieres agregar algunos datos básicos sobre los álbumes de cada cantante a la aplicación de música:

Tabla Albums con 5 filas y 3 columnas

Vista lógica de las filas de la tabla Albums. Las columnas de claves primarias aparecen a la izquierda de la línea en negrita.

Ten en cuenta que la clave primaria de Albums se compone de dos columnas: SingerId y AlbumId. Esto permite asociar cada álbum con su cantante. En el siguiente esquema de ejemplo, se definen las tablas Albums y Singers en la raíz de la jerarquía de la base de datos, lo que las convierte en tablas del mismo nivel:

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

La disposición física de las filas Singers y Albums es similar al diagrama. Las filas de la tabla Albums se almacenan por clave primaria contigua y, luego, se almacenan de la misma manera las filas de Singers:

Disposición física de las filas: Las filas de Albums y Singers se almacenan según el valor de la clave

Disposición física de las filas de las tablas Singers y Albums, ambas ubicadas en la raíz de la jerarquía de la base de datos

Un detalle importante sobre el esquema anterior es que Cloud Spanner no supone ninguna relación de localidad de datos entre las tablas Singers y Albums, ya que son tablas de nivel superior. A medida que crece la base de datos, Cloud Spanner puede agregar límites de división entre cualquiera de las filas que se muestran arriba. Esto significa que las filas de la tabla Albums podrían terminar en una división distinta a las de las filas de la tabla Singers, y ambas divisiones podrían moverse independientemente una de otra.

Según las necesidades de la aplicación, podría ser aceptable permitir que los datos de Albums se ubiquen en divisiones diferentes a las de los datos de Singers. Sin embargo, si en la aplicación necesitas recuperar frecuentemente datos sobre todos los álbumes de un cantante determinado, debes crear la tabla Albums como secundaria de Singers, lo que ubica juntas las filas de ambas tablas con la dimensión de la clave primaria. En el siguiente ejemplo, se explica esta situación de forma más detallada.

Crea tablas intercaladas

Supongamos que, mientras diseñas la aplicación de música, te das cuenta de que necesitas acceder con frecuencia a las filas de las tablas Singers y Albums para obtener una clave primaria determinada (p. ej., cada vez que accedes a la fila Singers(1), también debes acceder a Albums(1, 1) y Albums(1, 2)). En otras palabras, Singers y Albums deben tener una relación estrecha de localidad de datos.

Para declarar esta relación de localidad de datos, crea Albums como tabla secundaria o “intercalada” de Singers. Como se mencionó en Claves primarias, una tabla intercalada es una tabla que declaras como secundaria de otra para que sus filas se almacenen físicamente juntas con la fila superior asociada. Además, el prefijo de la clave primaria de una tabla secundaria debe ser la clave primaria de la tabla superior.

La línea en negrita del esquema que aparece a continuación es un ejemplo de cómo crear Albums como tabla intercalada de Singers.

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

Notas sobre este esquema:

  • SingerId, que es el prefijo de la clave primaria de la tabla secundaria Albums, también es la clave primaria de su tabla superior Singers. Esto no es necesario si Singers y Albums están en el mismo nivel de jerarquía, pero es necesario en este esquema porque Albums se declara como tabla secundaria de Singers.
  • La anotación ON DELETE CASCADE indica que, cuando se borra una fila de la tabla superior, también se borran automáticamente las filas secundarias de esta tabla (es decir, todas las filas que comienzan con la misma clave primaria). Si una tabla secundaria no la incluye (o si agregas la anotación ON DELETE NO ACTION), debes borrar manualmente las filas secundarias para borrar la fila superior.
  • Las filas intercaladas se ordenan primero según las filas de la tabla superior y, luego, según las filas contiguas de la tabla secundaria que comparten la clave primaria de la tabla superior, es decir, “Singers(1)”, seguida de “Albums(1, 1)”, “Albums(1, 2)” y así sucesivamente.
  • Si se dividiera esta base de datos, se conservarían la relación de localidad de datos de cada cantante y los datos de sus álbumes, ya que las divisiones solo se pueden insertar entre las filas de la tabla Singers.
  • La fila superior debe existir antes de que puedas insertar filas secundarias.

Disposición física de las filas: Las filas de Albums se intercalan entre las filas de Singers

Disposición física de las filas de la tabla Singers y su tabla secundaria Albums

Crea una jerarquía de tablas intercaladas

La relación de superior y secundaria entre Singers y Albums se puede extender a más tablas descendientes. Por ejemplo, puedes crear una tabla intercalada llamada Songs como secundaria de Albums para almacenar la lista de pistas de cada álbum:

Tabla Songs con 6 filas y 4 columnas

Vista lógica de las filas de la tabla Songs. Las columnas de claves primarias aparecen a la izquierda de la línea en negrita.

Songs debe tener una clave primaria compuesta por todas las claves primarias de las tablas que se encuentran en niveles superiores de la jerarquía, es decir, SingerId y AlbumId.

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

En la vista física de las filas intercaladas, se muestra que la relación de localidad de datos se conserva entre un cantante y los datos de sus álbumes y canciones:

Vistas físicas de las filas: Los datos de Songs se intercalan entre los de Albums, que, a su vez, se intercalan entre los datos de Singers.

Disposición física de las filas de las tablas Singers, Albums y Songs, que forman una jerarquía de tablas intercaladas

En resumen, una tabla superior junto con todas sus tablas secundarias y descendientes forman una jerarquía de tablas en el esquema. Si bien cada tabla de la jerarquía es independiente en cuanto a lógica, intercalarlas físicamente de esta manera puede mejorar el rendimiento, ya que las tablas se unen de forma previa, lo que te permite acceder a filas relacionadas al mismo tiempo y minimizar los accesos al disco.

Si es posible, une los datos de las tablas intercaladas según la clave primaria. Se garantiza que cada fila intercalada se almacene físicamente en la misma división que su fila superior. Por lo tanto, Cloud Spanner puede realizar uniones localmente según la clave primaria, lo que minimiza el acceso al disco y el tráfico de red. En el siguiente ejemplo, Singers y Albums se unen según la clave primaria, SingerId:

SELECT s.FirstName, a.AlbumTitle
FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;

No es obligatorio intercalar tablas en Cloud Spanner, pero se recomienda hacerlo si tienes tablas con relaciones estrechas de localidad de datos. Evita intercalar tablas si existe la posibilidad de que el tamaño de una sola fila y sus descendientes crezca hasta superar unos pocos GB.

Columnas de clave

No es posible modificar las claves de una tabla. Es decir, no puedes agregar una columna de clave a una tabla existente ni quitar una columna de clave que ya exista.

Almacena valores NULL

Las columnas de clave primaria se pueden definir para almacenar valores NULL. Si quieres almacenar valores NULL en una columna de clave primaria, omite la cláusula NOT NULL para esa columna en el esquema.

Este es un ejemplo de omisión de la cláusula NOT NULL en la columna de clave primaria SingerId. Ten presente que, como SingerId es la clave primaria, solo puede haber una fila como máximo en la tabla Singers que almacene valores NULL en esa columna.

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

La propiedad para admitir valores NULL de la columna de clave primaria debe coincidir entre las declaraciones de las tablas superior y secundaria. En este ejemplo, no se permite usar Albums.SingerId INT64 NOT NULL. La declaración de clave debe omitir la cláusula NOT NULL porque también se omite en Singers.SingerId.

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,  -- NOT ALLOWED!
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

Tipos no permitidos

Estas columnas no pueden ser de tipo ARRAY:

  • Las columnas de clave de una tabla
  • Las columnas de clave de un índice

Diseña para multiusuario

Si almacenas datos que pertenecen a diferentes clientes, te recomendamos que proporciones la opción de multiusuario. Por ejemplo, es posible que un servicio de música requiera almacenar cada sello discográfico por separado.

Multiusuario clásico

La forma clásica de diseñar para multiusuario es crear una base de datos independiente para cada cliente. En este ejemplo, cada base de datos tiene su propia tabla Singers:

Base de datos 1: Ackworth Records
SingerId FirstName LastName
1MarcRichards
2CatalinaSmith
Base de datos 2: Cama Records
SingerId FirstName LastName
3MarcRichards
4GabrielWright
Base de datos 3: Eagan Records
SingerId FirstName LastName
1BenjamínMartínez
2HannahHarris

La forma recomendada de diseñar para multiusuario en Cloud Spanner es usar un valor de clave primaria distinto para cada cliente. Debes incluir una clave CustomerId o una similar en las tablas. Si defines CustomerId como primera columna de clave, los datos de cada cliente tendrán una buena localidad. Cloud Spanner divide automáticamente los datos en los nodos según el tamaño y los patrones de carga. En este ejemplo, existe una única tabla Singers para todos los clientes:

Base de datos de multiusuario de Cloud Spanner
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
23MarcRichards
24GabrielWright
31BenjamínMartínez
32HannahHarris

Existen restricciones que debes tener en cuenta en caso de que debas tener bases de datos separadas para cada usuario:

  • Existen límites para la cantidad de bases de datos por instancia y de tablas por base de datos. Según la cantidad de clientes, es posible que no puedas tener bases de datos ni tablas separadas.
  • Agregar nuevos índices no entrelazados y tablas puede tomar mucho tiempo. Es posible que no puedas conseguir el rendimiento que deseas si el diseño del esquema depende de agregar nuevos índices y tablas.

Si quieres crear bases de datos independientes, podrías tener más éxito si distribuyes las tablas en bases de datos de manera que cada base tenga una cantidad baja de cambios de esquema por semana.

Si creas índices y tablas separadas para cada cliente de tu aplicación, no pongas todos los índices y tablas en la misma base de datos. En su lugar, repártelos en muchas bases de datos para mitigar los problemas de rendimiento que se producen por la creación de una gran cantidad de índices. También existen límites para la cantidad de índices y tablas por base de datos.