Compartir a través de


Procedimientos para modelar y crear particiones de datos en Azure Cosmos DB mediante un ejemplo real

SE APLICA A: NoSQL

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

Si suele trabajar con bases de datos relacionales, es probable que haya desarrollado hábitos e intuiciones acerca de cómo diseñar un modelo de datos. Dadas no solo las restricciones específicas, sino también los puntos fuertes exclusivos de Azure Cosmos DB, la mayoría de estos procedimientos recomendados no se traduce bien y es posible que le lleve a soluciones que no llegan a ser óptimas. El objetivo de este artículo es guiarle por todo el proceso de modelado de un caso de uso real en Azure Cosmos DB, desde el modelado de elementos a la colocación de entidades y la creación de particiones en contenedores.

Descargue o vea un código fuente generado por la comunidad que ilustre los conceptos de este artículo.

Importante

Un colaborador de la comunidad ha contribuido a este ejemplo de código y el equipo de Azure Cosmos DB no admite su mantenimiento.

Escenario

Para este ejercicio, vamos a tener en cuenta el dominio de una plataforma de blogs en las que los usuarios pueden crear publicaciones. Los usuarios también pueden indicar que dichas publicaciones les gustan y agregarles comentarios.

Sugerencia

Hemos resaltado algunas palabras en cursiva; dichas palabras identifican el tipo de "cosas" que nuestro modelo va a tener que manipular.

Incorporación de más requisitos a la especificación:

  • Una página frontal muestra una fuente de publicaciones recientemente creadas.
  • Podemos capturar todas las publicaciones de un usuario, todos los comentarios de una publicación y todos los "Me gusta" de una publicación.
  • Las publicaciones se devuelven con el nombre de usuario de sus autores y el número de comentarios y "Me gusta" que tienen.
  • Los comentarios y "Me gusta" también se devuelven con el nombre de usuario de los usuarios que los han creado.
  • Cuando se muestran en forma de 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 nuestra solución. Al diseñar un modelo de datos para Azure Cosmos DB, es importante saber qué solicitudes tendrá que atender nuestro modelo para tener la certeza de que el modelo va a hacerlo de manera eficiente.

Para que el proceso general sea más fácil de seguir, categorizamos las diferentes solicitudes ya sea como comandos o consultas, y tomamos prestado parte del vocabulario de CQRS. En CQRS, los comandos son solicitudes de escritura (es decir, intenciones de actualizar el sistema) y las consultas son solicitudes de solo lectura.

Esta es la lista de solicitudes que expone nuestra plataforma:

  • [C1] Crear o editar un usuario
  • [Q1] Recuperar un usuario
  • [C2] Crear o editar una publicación
  • [Q2] Recuperar una publicación
  • [Q3] Enumerar las publicaciones de un usuario en forma abreviada
  • [C3] Crear un comentario
  • [P4] Enumerar los comentarios de una publicación
  • [C4] Gustar una publicación
  • [Q5] Enumerar los "Me gusta" de una publicación
  • [P6] Enumerar las x publicaciones más recientes creadas en formato abreviado (fuente)

En esta fase, aún no se ha pensado en los detalles de lo que va a contener 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 porque hay que averiguar cómo se van a traducir esas entidades en términos de tablas, columnas, claves externas, etc. Es una preocupación mucho menor con una base de datos de documentos que no aplica ningún esquema al escribir.

El motivo principal por el que es importante identificar los patrones de acceso desde el principio, es que esta lista de solicitudes va a ser nuestro conjunto de pruebas. Cada vez que iteramos el modelo de datos, pasamos por todas y cada una de las solicitudes y comprobamos su rendimiento y escalabilidad. Se calculan las unidades de solicitud consumidas en cada modelo y se optimizan. Todos estos modelos utilizan la directiva de indexación predeterminada, que se puede invalidar mediante la indexación de propiedades específicas. Esto puede mejorar aún más el consumo y la latencia de RU.

V1: una primera versión

Comenzamos con dos contenedores: users y posts.

Contenedor users

Este contenedor solo almacena elementos de usuario:

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

La partición de este contenedor la realizamos por id, lo que significa que cada partición lógica del contenedor solo contiene un elemento.

Contenedor posts

Este contenedor hospeda entidades como publicaciones, comentarios y "Me gusta":

{
    "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>"
}

La partición de este contenedor la realizamos por postId, lo que significa que cada partición lógica de dicho contenedor contiene solo una publicación, junto con todos los comentarios y "Me gusta" de la misma.

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

Además, hemos elegido hacer referencia a los datos relacionados, en lugar de incrustarlo (consulte esta sección para más información acerca de estos conceptos) porque:

  • no hay límite superior en el número de publicaciones que puede crear un usuario,
  • las publicaciones pueden ser arbitrariamente largas,
  • no límite superior con respecto al número de comentarios y "Me gusta" que puede tener una publicación
  • queremos poder agregar un comentario o un "Me gusta" a una publicación sin tener que actualizar la propia publicación.

¿Hasta qué punto funciona bien nuestro modelo?

Ahora es el momento de evaluar el rendimiento y la escalabilidad de nuestra primera versión. En cada una de las solicitudes que ha identificado anteriormente, medimos su latencia y el número de unidades de solicitud que consume. Esta medida se realiza en un conjunto de datos ficticio que contiene 100 000 usuarios con entre 5 y 50 publicaciones por usuario y hasta 25 comentarios y 100 "Me gusta" por publicación.

[C1] Crear o editar un usuario

Esta solicitud es fácil de implementar, ya que acabamos de crear o actualizar un elemento en el contenedor users. Las solicitudes se esparcen entre todas las particiones gracias a la clave de partición id.

Diagrama de escritura de un elemento individual en el contenedor del usuario.

Latency Carga de unidad de solicitud Rendimiento
7 ms 5.71 RU

[Q1] Recuperar un usuario

La recuperación de los usuarios se realiza mediante la lectura del elemento correspondiente del contenedor users.

Diagrama de recuperación de un elemento individual del contenedor del usuario.

Latency Carga de unidad de solicitud Rendimiento
2 ms 1 RU

[C2] Crear o editar una publicación

Del mismo modo que [C1] , solo tenemos que escribir en el contenedor posts.

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

Latency Carga de unidad de solicitud Rendimiento
9 ms 8.76 RU

[Q2] Recuperar una publicación

Empezaremos por recuperar el documento correspondiente del contenedor posts. Pero eso no es suficiente, ya que de acuerdo con nuestra especificación, también debemos agregar el nombre de usuario del creador de la publicación, el recuento de comentarios y el recuento de Me gusta de la publicación. Las agregaciones enumeradas requieren que se emitan 3 consultas SQL más.

Diagrama de recuperación de una publicación e incorporación de datos adicionales.

Cada uno de los filtros de consultas adicionales de la clave de partición de su respectivo contenedor, que es exactamente lo que deseamos para 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.

Latency Carga de unidad de solicitud Rendimiento
9 ms 19.54 RU

[Q3] Enumerar las publicaciones de un usuario en forma abreviada

En primer lugar, tenemos que recuperar las publicaciones deseadas con una consulta SQL que captura las publicaciones correspondientes al usuario concreto. 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 recuperación de todas las publicaciones de un usuario e incorporación de sus datos adicionales.

Esta implementación presenta muchas desventajas:

  • 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.
Latency Carga de unidad de solicitud Rendimiento
130 ms 619.41 RU

[C3] Crear un comentario

Los comentarios se crean mediante la escritura del elemento correspondiente en el contenedor posts.

Diagrama de escritura de un único elemento de comentario en el contenedor de publicaciones.

Latency Carga de unidad de solicitud Rendimiento
7 ms 8.57 RU

[Q4] Enumerar los comentarios de una publicación

Comenzamos con una consulta que captura todos los comentarios de la publicación y, una vez más, es preciso agregar los nombres de usuario agregados por separado para cada comentario.

Diagrama de recuperación de todos los comentarios de una publicación y agregación de 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. Eso lo mejoraremos más adelante.

Latency Carga de unidad de solicitud Rendimiento
23 ms 27.72 RU

[C4] Gustar una publicación

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

Diagrama de escritura de un único elemento (Me gusta) en el contenedor de publicaciones.

Latency Carga de unidad de solicitud Rendimiento
6 ms 7.05 RU

[Q5] Enumerar los "Me gusta" de una publicación

Al igual que [Q4] , se consulta los "Me gusta" para la publicación y, después, se agregan sus nombres de usuario.

Diagrama de recuperación de todos los Me gusta de una publicación y agregación de sus datos adicionales.

Latency Carga de unidad de solicitud Rendimiento
59 ms 58.92 RU

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

Para capturar las publicaciones más recientes, consultamos el contenedor posts ordenado por fecha de creación orden, de forma descendente, y, después, los nombres de usuario agregados y el número de comentarios y "Me gusta" de cada una de las publicaciones.

Diagrama de recuperación de las publicaciones más recientes e incorporació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 distribución ramificada. Esta es incluso peor, ya que nos dirigimos a un conjunto de resultados más grande y ordenamos los resultados con una cláusula ORDER BY, lo que la hace más cara en términos de unidades de solicitud.

Latency Carga de unidad de solicitud Rendimiento
306 ms 2063.54 RU

Reflexión en el rendimiento de V1

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

  • algunas solicitudes requieren que se emitan varias consultas para recopilar todos los datos que hay que 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 estos problemas, empezando por el primero.

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

El motivo por el que en algunos casos es preciso emitir más solicitudes es que los resultados de la solicitud inicial no contienen todos los datos que necesitamos devolver. La desnormalización de datos resuelve este tipo de problema en nuestro conjunto de datos al trabajar con un almacén de datos no relacional como Azure Cosmos DB.

En nuestro ejemplo, modificamos los elementos de la publicación para agregar el nombre de usuario del autor de la publicación y el número de comentarios y "Me gusta":

{
    "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 los elementos de comentarios y, "Me gusta" para agregar el nombre de usuario del usuario que los ha creado:

{
    "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>"
}

Desnormalización del número de comentarios y, "Me gusta"

Lo que queremos conseguir es que cada vez que agregamos un comentario o un "Me gusta", también aumentamos commentCount o likeCount en la publicación correspondiente. A medida que postId particiona nuestro contenedor posts, el nuevo elemento (comentario o "Me gusta") y su publicación correspondiente se colocan en la misma partición lógica. Como resultado, podemos usar un procedimiento almacenado para realizar dicha operación.

Cuando crea un comentario ([C3]), en lugar de simplemente agregar un nuevo elemento al contenedor posts llamamos al siguiente procedimiento almacenado de dicho 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 luego:

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

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

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

Desnormalización de nombres de usuario

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

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

Diagrama de desnormalización de los 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 luego:

  • recupera todos los elementos que coinciden con userId (que puede ser publicaciones, comentarios, o "Me gusta")
  • en cada uno de los elementos
    • reemplaza el valor de userUsername
    • reemplaza el elemento

Importante

Esta operación es costosa porque requiere que este procedimiento almacenado se ejecute en todas las particiones del contenedor posts. Suponemos que la mayoría de los usuarios eligen un nombre de usuario adecuado en el registro y que nunca lo cambiará, por lo que esta actualización se ejecutará con muy poca frecuencia.

¿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 único elemento del contenedor de publicaciones desnormalizadas.

Latency Carga de unidad 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.

Latency Carga de unidad de solicitud Rendimiento
4 ms 7.72 RU

[Q5] Enumerar los "Me gusta" de una publicación

Exactamente la misma cuando se enumeran los "Me gusta".

Diagrama de recuperación de todos los Me gusta de una publicación desnormalizada.

Latency Carga de unidad de solicitud Rendimiento
4 ms 8.92 RU

V3: asegurarse de que todas las solicitudes se pueden escalar

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 forma abreviada

Esta solicitud ya se beneficia de las mejoras introducidas en V2, que comparte más consultas.

Diagrama que muestra la consulta para enumerar las publicaciones desnormalizadas de un usuario en forma abreviada.

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:

  1. Esta solicitud tiene que filtrar por userId, ya que deseamos recuperar todas las publicaciones de un usuario en concreto.
  2. No funciona bien porque se ejecuta en el contenedor posts, que no se particiona mediante userId.
  3. Empezando por lo obvio, podríamos resolver nuestro problema de rendimiento mediante la ejecución de esta solicitud en un contenedor particionado con userId.
  4. Resulta que ya tenemos ese contenedor: el contenedor users.

Por tanto, introducimos un segundo nivel de desnormalización mediante la duplicación de publicaciones completas en el 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 contenedor users ahora tiene 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 campo type en el elemento de usuario para distinguir a los usuarios de las publicaciones,
  • También hemos agregado un campo userId en el elemento de usuario, que es redundante con el campo id, pero es obligatorio, ya que el contenedor users ahora está particionado con userId (no id como antes)

Para lograr dicha desnormalización, usamos una vez más la fuente de cambios. Esta vez reaccionamos ante la fuente 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 del contenedor del usuario.

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 para un usuario desnormalizado.

Latency Carga de unidad de solicitud Rendimiento
4 ms 6.46 RU

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

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 enumerar las x publicaciones más recientes creadas en formato abreviado.

Siguiendo el mismo enfoque, la maximización del rendimiento y escalabilidad de esta solicitud requiere que solo acceda a una partición. Solo se puede alcanzar una sola partición porque solo tenemos que devolver un número limitado de elementos. Con el fin de rellenar la página principal de nuestra plataforma de blogs, solo debemos obtener las cien publicaciones más recientes, sin necesidad de paginar en todo el conjunto de datos.

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

{
    "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. Eso garantiza que todos los elementos de este contenedor se encontrará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. Hay algo importante que se debe tener en cuenta, 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 la fuente.

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 para volver a enrutar nuestra consulta a nuestro nuevo contenedor feed:

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

Latency Carga de unidad de solicitud Rendimiento
9 ms 16.97 RU

Conclusión

Echemos un vistazo a las mejoras en el rendimiento y escalabilidad generales 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 en el que se realizan muchas lecturas

Es posible que haya observado que hemos concentrado nuestros esfuerzos en mejorar el rendimiento de las solicitudes de lectura (consultas) a costa de las solicitudes de escritura (comandos). En muchos casos, las operaciones de escritura desencadenan una desnormalización posteriores a través de fuentes de cambios, lo que hace que requieran más procesos computacionales y tarden más tiempo en materializarse.

Justificamos este enfoque en el rendimiento de lectura por el hecho de que una plataforma de blogs (como la mayoría de las aplicaciones sociales) es de lectura intensiva. Una carga de trabajo de lectura intensiva indica que la cantidad de solicitudes de lectura que tiene que servir suele ser órdenes de magnitudes más alta que la cantidad de solicitudes de escritura. Por consiguiente, tiene sentido realizar solicitudes de escritura cuya ejecución sea más costosas, con el fin de que las solicitudes de lectura sean mejores y más baratas.

Si examinamos la optimización más extrema que hemos realizado, [Q6] pasó de más de 2000 RU a solo 17 RU; esto lo hemos logrado mediante la desnormalización de publicaciones con un costo de aproximadamente 10 RU por elemento. Como se atenderían muchas más solicitudes de fuentes que de creación o actualizaciones de publicaciones, el costo de esta desnormalización es nimio, si se tiene en cuenta el ahorro general.

La desnormalización se puede aplicar de forma incremental

Las mejoras de escalabilidad que se han analizado en este implican la desnormalización y duplicación de datos en el conjunto de datos. Debe tenerse en cuenta que estas optimizaciones no necesariamente deben entrar en vigor el día 1. Las consultas que filtran por claves de partición funcionan mejor a gran escala, pero las consultas entre particiones pueden ser aceptables si se llama muy de vez en cuando o en un conjunto de datos limitado. Si solo está compilando un prototipo o iniciando un producto con una base de usuarios pequeña y controlada, probablemente pueda reservar esas mejoras para más adelante. Lo que es importante entonces es supervisar el rendimiento del modelo para poder decidir si es momento de implementarlas y cuándo implementarlas.

La fuente de cambios que usamos para distribuir las actualizaciones a otros contenedores almacena todas las actualizaciones sistemáticamente. Esta persistencia permite solicitar todas las actualizaciones desde la creación del contenedor y arrancar las vistas desnormalizadas como una operación de puesta al día que se realiza una sola vez, incluso si el sistema ya tiene muchos datos.

Pasos siguientes

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