Compartir a través de


Cómo modelar y crear particiones de datos mediante un ejemplo real

Este artículo se basa en varios conceptos de Azure Cosmos DB, como el modelado de datos, la creación de particiones y el rendimiento aprovisionado para demostrar cómo abordar un ejercicio de diseño de datos del mundo real.

Si normalmente trabaja con bases de datos relacionales, probablemente haya desarrollado hábitos para diseñar modelos de datos. Debido a las restricciones específicas, pero también a los puntos fuertes únicos de Azure Cosmos DB, la mayoría de estos procedimientos recomendados no se traducen bien y pueden arrastrarle a soluciones poco óptimas. El objetivo de este artículo es guiarle por el proceso completo de modelado de un caso de uso real en Azure Cosmos DB, desde el modelado de elementos hasta la colocación de entidades y la creación de particiones de contenedores.

Para obtener un ejemplo que ilustra los conceptos de este artículo, descargue o vea este código fuente generado por la comunidad.

Importante

Un colaborador de la comunidad ha contribuido a este ejemplo de código. El equipo de Azure Cosmos DB no respalda el mantenimiento de la base de datos.

Escenario

En este ejercicio, vamos a considerar el dominio de una plataforma de blogs donde los usuarios pueden crear publicaciones. Los usuarios también pueden gustar y agregar comentarios a esas publicaciones.

Sugerencia

Algunas palabras se resaltan en cursiva para identificar el tipo de "cosas" que nuestro modelo manipula.

Agregar más requisitos a nuestra especificación:

  • Una página principal muestra una fuente de publicaciones creadas recientemente.
  • Podemos capturar todas las publicaciones de un usuario, todos los comentarios de una publicación y todos los likes de una publicación.
  • Las publicaciones se devuelven con el nombre de usuario de sus autores y un recuento del número de comentarios y likes que tienen.
  • Los comentarios y los likes también se devuelven con el nombre de usuario de los usuarios que los crearon.
  • Cuando se muestran como listas, las publicaciones solo tienen que presentar un resumen truncado de su contenido.

Identificación de los patrones de acceso principales

Para empezar, proporcionamos cierta estructura a nuestra especificación inicial mediante la identificación de los patrones de acceso de la solución. Al diseñar un modelo de datos para Azure Cosmos DB, es importante comprender qué solicitudes tiene que servir nuestro modelo para asegurarse de que el modelo atiende esas solicitudes de forma eficaz.

Para que el proceso general sea más fácil de seguir, clasificamos esas distintas solicitudes como comandos o consultas, tomando prestados algunos vocabularios de la segregación de responsabilidades de consulta de comandos (CQRS). En CQRS, los comandos son solicitudes de escritura (es decir, intenciones para actualizar el sistema) y las consultas son solicitudes de solo lectura.

Esta es la lista de solicitudes que expone nuestra plataforma:

  • [C1] Creación o edición de un usuario
  • [Q1] Recuperación de un usuario
  • [C2] Crear o editar una publicación
  • [Q2] Recuperar una publicación
  • [Q3] Enumerar las publicaciones de un usuario en formato corto
  • [C3] Crear un comentario
  • [Q4] Enumerar los comentarios de una publicación
  • [C4] Gustar una publicación
  • [Q5] Enumerar los likes de una publicación
  • [Q6] Enumerar las entradas más recientes creadas en formato corto (fuente)

En esta fase, no hemos pensado en los detalles de lo que contiene cada entidad (usuario, publicación, etc.). Este paso suele estar entre los primeros en abordarse al diseñar en base a un almacén relacional. Comenzamos con este paso en primer lugar porque tenemos que averiguar cómo se traducen esas entidades en términos de tablas, columnas, claves externas, etc. Es mucho menos preocupante con una base de datos de documentos que no impone ningún esquema durante la escritura.

Es importante identificar nuestros patrones de acceso desde el principio porque esta lista de solicitudes va a ser nuestro conjunto de pruebas. Cada vez que recorremos en iteración nuestro modelo de datos, pasamos por cada una de las solicitudes y comprobamos su rendimiento y escalabilidad. Calculamos las unidades de solicitud (RU) consumidas en cada modelo y las optimizamos. Todos estos modelos usan la directiva de indexación predeterminada y se puede invalidar mediante la indexación de propiedades específicas, lo que puede mejorar aún más el consumo de RU y la latencia.

V1: una primera versión

Empezamos con dos contenedores: users y posts.

Contenedor de usuarios

Este contenedor solo almacena elementos de usuario:

{
    "id": "<user-id>",
    "username": "<username>"
}

Particionamos este contenedor mediante id, lo que significa que cada partición lógica dentro de ese contenedor solo contiene un elemento.

Contenedor de publicaciones

Este contenedor hospeda entidades como publicaciones, comentarios y similares:

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "title": "<post-title>",
    "content": "<post-content>",
    "creationDate": "<post-creation-date>"
}

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "creationDate": "<like-creation-date>"
}

Particionamos este contenedor por postId, lo que significa que cada partición lógica dentro de ese contenedor contiene una publicación, todos los comentarios de esa publicación y todos los likes de esa publicación.

Hemos introducido una type propiedad en los elementos almacenados en este contenedor para distinguir entre los tres tipos de entidades que hospeda este contenedor.

Además, decidimos hacer referencia a datos relacionados en lugar de insertarlos porque:

  • No hay ningún límite superior para el número de publicaciones que puede crear un usuario.
  • Las publicaciones pueden ser arbitrariamente largas.
  • No hay ningún límite superior para el número de comentarios y likes que puede tener una publicación.
  • Queremos poder agregar un comentario o un like a una publicación sin tener que actualizar la propia publicación.

Para más información sobre estos conceptos, consulte Modelado de datos en Azure Cosmos DB.

¿Qué tan bien funciona nuestro modelo?

Ahora es el momento de evaluar el rendimiento y la escalabilidad de nuestra primera versión. Para cada una de las solicitudes identificadas anteriormente, se mide su latencia y el número de unidades de solicitud que consume. Esta medida se realiza con un conjunto de datos ficticio que contiene 100 000 usuarios con 5 a 50 publicaciones por usuario y hasta 25 comentarios y 100 likes por publicación.

[C1] Creación o edición de un usuario

Esta solicitud es sencilla de implementar, ya que solo creamos o actualizamos un elemento en el users contenedor. Las solicitudes se distribuyen perfectamente entre todas las particiones gracias a la id clave de partición.

Diagrama de escritura de un solo elemento en el contenedor de usuarios.

Latencia Unidades de solicitud Rendimiento
7 ms 5.71 RU

[Q1] Recuperación de un usuario

Para recuperar un usuario, lea el elemento correspondiente del users contenedor.

Diagrama de recuperación de un solo elemento del contenedor de usuarios.

Latencia Unidades de solicitud Rendimiento
2 ms 1 RU

[C2] Crear o editar una publicación

De forma similar a [C1], solo tenemos que escribir en el posts contenedor.

Diagrama de escritura de un único elemento de publicación en el contenedor de publicaciones.

Latencia Unidades de solicitud Rendimiento
9 ms 8.76 RU

[Q2] Recuperar una publicación

Empezamos recuperando el documento correspondiente del posts contenedor. Pero eso no es suficiente, según nuestra especificación, también tenemos que agregar el nombre de usuario del autor de la publicación, recuentos de comentarios y recuentos de likes para la publicación. Las agregaciones enumeradas requieren que se emita tres consultas SQL más.

Diagrama de recuperación de una publicación y agregación de datos adicionales.

Cada una de las consultas filtra la clave de partición de su contenedor respectivo, que es exactamente lo que queremos maximizar el rendimiento y la escalabilidad. Pero eventualmente tenemos que realizar cuatro operaciones para devolver una publicación individual, lo que mejoraremos en una iteración posterior.

Latencia Unidades de solicitud Rendimiento
9 ms 19.54 RU

[Q3] Enumerar las publicaciones de un usuario en formato corto

En primer lugar, tenemos que recuperar las publicaciones deseadas con una consulta SQL que capture las publicaciones correspondientes a ese usuario determinado. Pero también tenemos que emitir más consultas para agregar el nombre de usuario del creador y el número de comentarios y "Me gusta".

Diagrama de cómo recuperar todas las publicaciones de un usuario y agregar sus datos adicionales.

Esta implementación presenta muchos inconvenientes:

  • Las consultas que agregan el número de comentarios y "Me gusta" deben emitirse para cada publicación que devuelve la primera consulta.
  • La consulta principal no se filtra en la clave de partición del contenedor posts, lo que provoca una distribución ramificada y un examen de las particiones en el contenedor.
Latencia Unidades de solicitud Rendimiento
130 ms 619.41 RU

[C3] Crear un comentario

Para crear un comentario, escriba el elemento correspondiente en el posts contenedor.

Diagrama de escritura de un solo elemento de comentario en el contenedor de publicaciones.

Latencia Unidades de solicitud Rendimiento
7 ms 8.57 RU

[Q4] Enumerar los comentarios de una publicación

Empezamos con una consulta que captura todos los comentarios de esa publicación y, una vez más, también necesitamos agregar nombres de usuario por separado para cada comentario.

Diagrama de cómo recuperar todos los comentarios de una publicación y agregar sus datos adicionales.

Aunque la consulta principal filtrar por la clave de partición del contenedor, agregar los nombres de usuario por separado penaliza el rendimiento general. Mejoramos eso más adelante.

Latencia Unidades de solicitud Rendimiento
23 ms 27.72 RU

[C4] Me gusta un post

Al igual que [C3], creamos el elemento correspondiente en el posts contenedor.

Diagrama de la escritura de un único elemento, como un 'me gusta', en el contenedor de publicaciones.

Latencia Unidades de solicitud Rendimiento
6 ms 7.05 RU

[Q5] Enumerar los likes de una publicación

Al igual que [Q4], consultamos los likes para esa publicación y, a continuación, agregamos sus nombres de usuario.

Diagrama de la recuperación de todos los likes para una publicación y la agregación de sus datos adicionales.

Latencia Unidades de solicitud Rendimiento
59 ms 58.92 RU

[Q6] Enumerar las x publicaciones más recientes creadas en formato corto (feed)

Capturamos las publicaciones más recientes consultando el posts contenedor ordenado por fecha de creación descendente y, a continuación, agregamos nombres de usuario y recuentos de comentarios y likes para cada una de las publicaciones.

Diagrama de recuperación de publicaciones más recientes y agregación de sus datos adicionales.

Una vez más, la consulta inicial no filtra por la clave de partición del contenedor posts, lo que desencadena una costosa operación de expansión. Este caso es aún peor, ya que el objetivo es un conjunto de resultados más grande y ordena los resultados con una cláusula ORDER BY, lo que hace que sea más caro en términos de unidades de solicitud.

Latencia Unidades de solicitud Rendimiento
306 ms 2063.54 RU

Reflexión sobre el rendimiento de V1

Al examinar los problemas de rendimiento a los que nos enfrentamos en la sección anterior, podemos identificar dos clases principales de problemas:

  • Algunas solicitudes requieren que se emita varias consultas para recopilar todos los datos que necesitamos devolver.
  • Algunas consultas no filtran por la clave de partición de los contenedores a los que van dirigidas, lo que da lugar a una distribución ramificada que impide la escalabilidad.

Vamos a resolver cada uno de esos problemas, empezando por el primero.

V2: Introducir la desnormalización para optimizar las consultas de lectura

La razón por la que tenemos que emitir más solicitudes en algunos casos es porque los resultados de la solicitud inicial no contienen todos los datos que necesitamos devolver. Denormalizar los datos resuelve este tipo de problemas en nuestro conjunto de datos cuando trabajamos con un almacén de datos no relacional como Azure Cosmos DB.

En nuestro ejemplo, modificamos los elementos de publicación para agregar el nombre de usuario del autor de la publicación, el recuento de comentarios y el recuento de likes:

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

También modificamos el comentario y los elementos similares para agregar el nombre de usuario del usuario que los creó:

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "userUsername": "<comment-author-username>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "userUsername": "<liker-username>",
    "creationDate": "<like-creation-date>"
}

Desnormalizar los recuentos de comentarios y me gusta

Lo que queremos lograr es que cada vez que agregamos un comentario o un like, también incrementamos el commentCount o el likeCount en la publicación correspondiente. Como postId particiona nuestro posts contenedor, el nuevo elemento (un comentario o un me gusta) y su publicación correspondiente se encuentran en la misma partición lógica. Como resultado, podemos usar un procedimiento almacenado para realizar esa operación.

Al crear un comentario ([C3]), en lugar de agregar un nuevo elemento en el posts contenedor, llamamos al siguiente procedimiento almacenado en ese contenedor:

function createComment(postId, comment) {
  var collection = getContext().getCollection();

  collection.readDocument(
    `${collection.getAltLink()}/docs/${postId}`,
    function (err, post) {
      if (err) throw err;

      post.commentCount++;
      collection.replaceDocument(
        post._self,
        post,
        function (err) {
          if (err) throw err;

          comment.postId = postId;
          collection.createDocument(
            collection.getSelfLink(),
            comment
          );
        }
      );
    })
}

Este procedimiento almacenado toma el identificador de la publicación y el cuerpo del nuevo comentario como parámetros y, a continuación,:

  • recupera la publicación.
  • incrementa el valor de commentCount.
  • reemplaza la publicación.
  • agrega el nuevo comentario.

A medida que los procedimientos almacenados se ejecutan como transacciones atómicas, el valor de commentCount y el número real de comentarios siempre permanecen sincronizados.

Obviamente llamamos a un procedimiento almacenado similar al agregar nuevos "Me gusta" para incrementar likeCount.

Desnormalizar nombres de usuario

Los nombres de usuario requieren un enfoque diferente, ya que los usuarios no solo se encuentran en distintas particiones, sino en un contenedor diferente. Cuando tenemos que desnormalizar los datos entre particiones y contenedores, podemos usar la fuente de cambios del contenedor de origen.

En nuestro ejemplo, usamos la fuente de cambios del users contenedor para reaccionar cada vez que los usuarios actualizan sus nombres de usuario. Cuando esto sucede, propagamos el cambio llamando a otro procedimiento almacenado en el posts contenedor:

Diagrama de desnormalización de nombres de usuario en el contenedor de publicaciones.

function updateUsernames(userId, username) {
  var collection = getContext().getCollection();
  
  collection.queryDocuments(
    collection.getSelfLink(),
    `SELECT * FROM p WHERE p.userId = '${userId}'`,
    function (err, results) {
      if (err) throw err;

      for (var i in results) {
        var doc = results[i];
        doc.userUsername = username;

        collection.upsertDocument(
          collection.getSelfLink(),
          doc);
      }
    });
}

Este procedimiento almacenado toma el identificador del usuario y el nuevo nombre de usuario del usuario como parámetros y, a continuación,:

  • captura todos los elementos que coinciden con userId (que pueden ser publicaciones, comentarios o "me gusta").
  • para cada uno de esos elementos:
    • reemplaza a userUsername.
    • reemplaza el elemento.

Importante

Esta operación es costosa porque requiere que este procedimiento almacenado se ejecute en cada partición del posts contenedor. Se supone que la mayoría de los usuarios eligen un nombre de usuario adecuado durante el registro y no lo cambiarán nunca, por lo que esta actualización se ejecuta muy rara vez.

¿Cuáles son las mejoras de rendimiento de V2?

Hablemos de algunas de las mejoras de rendimiento de V2.

[Q2] Recuperar una publicación

Ahora que la desnormalización está en vigor, solo tenemos que capturar un elemento para controlar la solicitud.

Diagrama de recuperación de un solo elemento del contenedor de publicaciones no normalizadas.

Latencia Unidades de solicitud Rendimiento
2 ms 1 RU

[Q4] Enumerar los comentarios de una publicación

Aquí podemos volver a compartir solicitudes adicionales que han capturado los el nombres de usuario y acabar con una sola consulta que filtra por la clave de partición.

Diagrama de recuperación de todos los comentarios de una publicación desnormalizada.

Latencia Unidades de solicitud Rendimiento
4 ms 7.72 RU

[Q5] Enumerar los likes de una publicación

Exactamente la misma situación al enumerar los likes.

Diagrama de recuperación de todos los likes para una publicación desnormalizada.

Latencia Unidades de solicitud Rendimiento
4 ms 8.92 RU

V3: Asegúrese de que todas las solicitudes son escalables

Todavía hay dos solicitudes que no hemos optimizado completamente al examinar nuestras mejoras generales de rendimiento. Estas solicitudes son [Q3] y [Q6]. Son las solicitudes que implican consultas que no filtran por la clave de partición de los contenedores a los que se dirige.

[Q3] Enumerar las publicaciones de un usuario en formato corto

Esta solicitud ya se beneficia de las mejoras introducidas en la versión 2, que ahorra más consultas.

Diagrama que muestra la consulta para enumerar las publicaciones desnormalizadas de un usuario en formato corto.

Pero la consulta restante no se filtra por la clave de partición del contenedor posts.

La manera de pensar en esta situación es sencilla:

  • Esta solicitud tiene que filtrar por userId porque queremos capturar todas las publicaciones de un usuario determinado.
  • No funciona bien porque se ejecuta en el contenedor posts, que no se particiona mediante userId.
  • Indicando lo obvio, resolveríamos el problema de rendimiento ejecutando esta solicitud en un contenedor con particiones con userId.
  • Resulta que ya tenemos un contenedor como este: el users contenedor.

Por lo tanto, presentamos un segundo nivel de desnormalización al duplicar publicaciones completas dentro del contenedor users. Al hacerlo, obtenemos una copia de nuestras publicaciones, en las que solo se crean particiones en dimensiones diferentes, lo que hace que sea mucho más eficaz recuperarlas por userId.

El users contenedor ahora contiene dos tipos de elementos:

{
    "id": "<user-id>",
    "type": "user",
    "userId": "<user-id>",
    "username": "<username>"
}

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

En este ejemplo:

  • Hemos introducido un type campo en el elemento de usuario para distinguir a los usuarios de las publicaciones.
  • También hemos agregado un userId campo en el elemento de usuario, que es redundante con el id campo, pero es necesario, ya que el users contenedor ahora está particionado con userId (y no id como antes).

Para lograr esa desnormalización, una vez más usamos el feed de cambios. Esta vez, respondemos al flujo de cambios del contenedor posts para enviar cualquier publicación nueva o actualizada al contenedor users. Y como la enumeración de publicaciones no requiere devolver todo su contenido, podemos truncarlas en el proceso.

Diagrama de desnormalización de publicaciones en el contenedor de usuarios.

Ahora podemos enrutar nuestra consulta al contenedor users, filtrando por la clave de partición del contenedor.

Diagrama de recuperación de todas las publicaciones de un usuario desnormalizado.

Latencia Unidades de solicitud Rendimiento
4 ms 6.46 RU

[Q6] Enumerar las x publicaciones más recientes creadas en formato corto (feed)

Tenemos que tratar con una situación similar aquí: incluso después de compartir las consultas adicionales dejadas como innecesarias por la desnormalización introducida en V2, la consulta restante no se filtra por la clave de partición del contenedor:

Diagrama que muestra la consulta para mostrar las x entradas más recientes creadas en formato corto.

Siguiendo el mismo enfoque, maximizar el rendimiento y la escalabilidad de esta solicitud requiere que solo alcance una partición. Solo se puede alcanzar una sola partición porque solo tenemos que devolver un número limitado de elementos. Para rellenar la página de inicio de nuestra plataforma de blogs, solo tenemos que obtener las 100 publicaciones más recientes, sin necesidad de navegar por todo el conjunto de datos.

Por lo tanto, para optimizar esta última solicitud, presentamos un tercer contenedor para nuestro diseño, dedicado completamente a atender esta solicitud. Desnormalizamos nuestras publicaciones en ese nuevo feed contenedor:

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

El campo type particiona este contenedor, que siempre es post en nuestros elementos. Esto garantiza que todos los elementos de este contenedor se ubicarán en la misma partición.

Para lograr la desnormalización, solo tenemos que enlazar a la canalización de la fuente de cambios que hemos introducido anteriormente para enviar las publicaciones a ese nuevo contenedor. Una cosa importante que hay que tener en cuenta es que necesitamos asegurarnos de que solo almacenamos las 100 publicaciones más recientes; De lo contrario, el contenido del contenedor puede crecer más allá del tamaño máximo de una partición. Esta limitación se puede implementar llamando a un desencadenador posterior cada vez que se agrega un documento en el contenedor:

Diagrama de desnormalización de publicaciones en el contenedor de feed.

Este es el cuerpo del desencadenador posterior que trunca la colección:

function truncateFeed() {
  const maxDocs = 100;
  var context = getContext();
  var collection = context.getCollection();

  collection.queryDocuments(
    collection.getSelfLink(),
    "SELECT VALUE COUNT(1) FROM f",
    function (err, results) {
      if (err) throw err;

      processCountResults(results);
    });

  function processCountResults(results) {
    // + 1 because the query didn't count the newly inserted doc
    if ((results[0] + 1) > maxDocs) {
      var docsToRemove = results[0] + 1 - maxDocs;
      collection.queryDocuments(
        collection.getSelfLink(),
        `SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
        function (err, results) {
          if (err) throw err;

          processDocsToRemove(results, 0);
        });
    }
  }

  function processDocsToRemove(results, index) {
    var doc = results[index];
    if (doc) {
      collection.deleteDocument(
        doc._self,
        function (err) {
          if (err) throw err;

          processDocsToRemove(results, index + 1);
        });
    }
  }
}

El último paso es volver a enrutar la consulta a nuestro nuevo feed contenedor:

Diagrama de recuperación de las publicaciones más recientes.

Latencia Unidades de solicitud Rendimiento
9 ms 16.97 RU

Conclusión

Echemos un vistazo a las mejoras generales de rendimiento y escalabilidad que hemos introducido en las distintas versiones de nuestro diseño.

V1 V2 V3
[C1] 7 ms/ 5.71 RU 7 ms/ 5.71 RU 7 ms/ 5.71 RU
[Q1] 2 ms/ 1 RU 2 ms/ 1 RU 2 ms/ 1 RU
[C2] 9 ms/ 8.76 RU 9 ms/ 8.76 RU 9 ms/ 8.76 RU
[Q2] 9 ms/ 19.54 RU 2 ms/ 1 RU 2 ms/ 1 RU
[Q3] 130 ms/ 619.41 RU 28 ms/ 201.54 RU 4 ms/ 6.46 RU
[C3] 7 ms/ 8.57 RU 7 ms/ 15.27 RU 7 ms/ 15.27 RU
[Q4] 23 ms/ 27.72 RU 4 ms/ 7.72 RU 4 ms/ 7.72 RU
[C4] 6 ms/ 7.05 RU 7 ms/ 14.67 RU 7 ms/ 14.67 RU
[Q5] 59 ms/ 58.92 RU 4 ms/ 8.92 RU 4 ms/ 8.92 RU
[Q6] 306 ms/ 2063.54 RU 83 ms/ 532.33 RU 9 ms/ 16.97 RU

Hemos optimizado un escenario de lectura intensiva

Es posible que observe que hemos concentrado nuestros esfuerzos para mejorar el rendimiento de las solicitudes de lectura (consultas) a costa de las solicitudes de escritura (comandos). En muchos casos, las operaciones de escritura ahora desencadenan una posterior desnormalización a través de flujos de cambios, lo que las hace más costosas computacionalmente y tardan más en materializarse.

Justificamos este enfoque en la capacidad de lectura porque una plataforma de blogs, como la mayoría de las aplicaciones sociales, está orientada a la lectura. Una carga de trabajo de lectura intensiva indica que la cantidad de solicitudes de lectura que tiene que atender suele ser un orden de magnitud mayor que el número de solicitudes de escritura. Por lo tanto, tiene sentido hacer que las solicitudes de escritura sean más costosas de ejecutar para permitir que las solicitudes de lectura sean más baratas y de mejor rendimiento.

Si observamos la optimización más extrema que hemos hecho, [Q6] pasó de más de 2000 RU a solo 17 RU; hemos logrado que al desnormalizar las publicaciones a un costo de alrededor de 10 RU por elemento. Como serviremos muchas más solicitudes de feeds que la creación o las actualizaciones de publicaciones, el costo de esta desnormalización es insignificante considerando el ahorro global.

La desnormalización se puede aplicar de forma incremental

Las mejoras de escalabilidad que hemos explorado en este artículo implican la desnormalización y la duplicación de datos en el conjunto de datos. Debe tenerse en cuenta que estas optimizaciones no tienen que colocarse en vigor el día uno. Las consultas que filtran mediante clave de partición funcionan mejor a gran escala, pero las consultas entre particiones pueden ser aceptables si se realizan raramente o contra un conjunto de datos limitado. Si está creando un prototipo o iniciando un producto con una base de usuarios pequeña y controlada, es probable que pueda ahorrar esas mejoras más adelante. Lo que es importante es supervisar el rendimiento del modelo para que pueda decidir si y cuándo es el momento de incorporarlos.

La fuente de cambios que usamos para distribuir actualizaciones a otros contenedores almacena todas esas actualizaciones de forma persistente. Esta persistencia permite solicitar todas las actualizaciones desde la creación del contenedor e inicializar vistas desnormalizadas como una operación de sincronización única, aunque el sistema ya disponga de una gran cantidad de datos.

Pasos siguientes

Después de esta introducción al modelado y la creación de particiones de datos prácticos, es posible que desee consultar los siguientes artículos para revisar los conceptos: