sql >> Base de Datos >  >> RDS >> Mysql

Partición de una tabla de mil millones de filas de datos de fútbol utilizando el contexto de datos

En este artículo, aprenderá cómo usar la semántica detrás de sus datos cuando particione su base de datos. Esto puede mejorar drásticamente el rendimiento de su aplicación. Y, lo que es más importante, descubrirá que debe adaptar sus criterios de partición a su dominio de aplicación único.

He colaborado con una startup para desarrollar una aplicación web para que los expertos en deportes tomen decisiones y exploren datos. La aplicación es compatible con cualquier deporte, pero estamos ubicados en Europa, y a los europeos les encanta el fútbol. Cada uno de los cientos de juegos que se juegan todos los días en todo el mundo viene con miles de filas. ¡En solo unos meses, la tabla de eventos de nuestra aplicación alcanzó los quinientos millones de filas!

Al comprender cómo los expertos en fútbol consultaron nuestros datos, pudimos particionar la base de datos de manera inteligente. La mejora de tiempo promedio en esta nueva tabla fue entre 20 y 40 veces más rápida. La mejora de tiempo promedio en todas las consultas fue de 5X a 10X.

Profundicemos ahora en este escenario y aprendamos por qué no puede ignorar su contexto de datos al particionar una base de datos.

Presentación del contexto

Nuestra aplicación deportiva ofrece datos tanto brutos como agregados, aunque los profesionales que la adoptaron prefieren estos últimos. La base de datos subyacente contiene terabytes de datos complejos, no estructurados y heterogéneos de varios proveedores. Por lo tanto, el mayor desafío fue diseñar una base de datos confiable, rápida y fácil de explorar.

Dominio de la aplicación

En esta industria, muchos proveedores ofrecen a sus clientes acceso a los eventos de los partidos de fútbol más importantes. En concreto, te proporcionan datos relacionados con lo sucedido durante un partido, como goles, asistencias, tarjetas amarillas, pases y mucho más. La tabla que contiene estos datos es, con mucho, la más grande con la que tuvimos que trabajar.

Especificaciones, tecnologías y arquitectura de VPS

Mi equipo ha estado desarrollando la aplicación de back-end que proporciona las funciones de exploración de datos más importantes. Adoptamos Kotlin v1.6 ejecutándose sobre una JVM (Java Virtual Machine) como lenguaje de programación, Spring Boot 2.5.3 como marco e Hibernate 5.4.32.Final como ORM (Mapeo relacional de objetos). La razón principal por la que optamos por esta pila de tecnología es que la velocidad es uno de los requisitos comerciales más cruciales. Por lo tanto, necesitábamos una tecnología que pudiera aprovechar el procesamiento intensivo de subprocesos múltiples, y Spring Boot resultó ser una solución confiable.

Implementamos nuestro backend en un VPS de 8 CPU y 16 GB a través de un contenedor Docker administrado por Dokku. Puede usar 15 GB de RAM como máximo. Esto se debe a que un GB de RAM está dedicado a un sistema de almacenamiento en caché basado en Redis. Lo agregamos para mejorar el rendimiento y evitar sobrecargar el backend con operaciones repetidas.

Base de datos y estructura de tablas

En cuanto a la base de datos, decidimos optar por MySQL 8. Actualmente, un VPS de 8 GB y 2 CPU alberga el servidor de la base de datos, que admite hasta 200 conexiones simultáneas. La aplicación back-end y la base de datos están en la misma granja de servidores para evitar la sobrecarga de comunicación. Diseñamos la estructura de la base de datos para evitar la duplicación y teniendo en cuenta el rendimiento. Decidimos adoptar una base de datos relacional porque queríamos tener una estructura consistente para convertir los datos recibidos de los proveedores. De esta forma, estandarizamos los datos deportivos, facilitando su exploración y presentación a los usuarios finales.

La base de datos contiene cientos de tablas en el momento de escribir este artículo y no puedo presentarlas todas debido al NDA que firmé. Afortunadamente, una tabla es suficiente para analizar a fondo por qué terminamos adoptando la partición de datos basada en el contexto que está a punto de ver. El verdadero desafío llegó cuando comenzamos a realizar consultas pesadas en la tabla Eventos. Pero antes de profundizar en eso, veamos cómo se ve la tabla de Eventos:

Como puede ver, no involucra muchas columnas, pero tenga en cuenta que tuve que omitir algunas de ellas por razones de confidencialidad. Pero lo que realmente los asuntos aquí son el parameterId y gameId columnas Usamos estas dos claves foráneas para seleccionar un tipo de parámetro (por ejemplo, gol, tarjeta amarilla, pase, penalti) y los juegos en los que sucedió.

Problemas de rendimiento

La tabla de eventos alcanzó los quinientos millones de filas en solo unos meses. Como ya hemos cubierto en profundidad en esta publicación de blog, el principal problema es que necesitamos realizar operaciones agregadas utilizando consultas IN lentas. Esto se debe a que lo que sucede durante un juego no es tan importante. En cambio, los expertos en deportes quieren analizar datos agregados para encontrar tendencias y tomar decisiones basadas en ellas.

Además, aunque generalmente analizan toda la temporada o los últimos 5 o 10 partidos, los usuarios suelen querer excluir algunos partidos concretos de su análisis. Esto se debe a que no quieren que un juego se juegue particularmente mal o bien para polarizar sus resultados. No podemos pregenerar los datos agregados porque tendríamos que hacerlo en todas las combinaciones posibles, lo cual no es factible. Entonces, tenemos que almacenar todos los datos y agregarlos sobre la marcha.

Comprender el problema de rendimiento

Ahora, profundicemos en el aspecto central que condujo a los problemas de rendimiento que tuvimos que enfrentar.

Las tablas de millones de filas son lentas

Si alguna vez ha tratado con tablas que contienen cientos de millones de filas, sabe que son inherentemente lentas. Ni siquiera puede pensar en ejecutar JOIN en tablas tan grandes. Sin embargo, puede realizar consultas SELECT en un tiempo razonable. Esto es particularmente cierto cuando estas consultas involucran condiciones DONDE simples. Por otro lado, se vuelven terriblemente lentos cuando se usan funciones agregadas o cláusulas IN. En estos casos, pueden tardar fácilmente hasta 80 segundos, lo que es simplemente demasiado.

Los índices no son suficientes

Para mejorar el rendimiento, decidimos definir algunos índices. Este fue nuestro primer enfoque para encontrar una solución a los problemas de rendimiento. Pero, desafortunadamente, esto llevó a otro problema. Los índices toman tiempo y espacio. Esto es generalmente insignificante, pero no cuando se trata de tablas tan grandes. Resultó que definir índices complejos basados ​​en las consultas más comunes tomó varias horas y GB de espacio. Además, los índices son útiles pero no mágicos.

Particionamiento de bases de datos basado en contexto de datos como solución

Dado que no pudimos resolver el problema de rendimiento con índices personalizados, decidimos probar un nuevo enfoque. Hablamos con otros expertos, buscamos soluciones en línea, leímos artículos basados ​​en escenarios similares y finalmente decidimos que particionar la base de datos era el enfoque correcto a seguir.

Por qué la partición tradicional puede no ser el enfoque correcto

Antes de particionar todas nuestras tablas más grandes, estudiamos el tema tanto en la documentación oficial de MySQL como en artículos interesantes. Aunque todos estuvimos de acuerdo en que este era el camino a seguir, también nos dimos cuenta de que aplicar la partición sin tener en cuenta nuestro dominio de aplicación en particular sería un error. Específicamente, entendimos lo crucial que era encontrar los criterios adecuados al particionar una base de datos. Algunos expertos en particiones nos enseñaron que el enfoque tradicional es particionar según el número de filas. Pero queríamos encontrar algo más inteligente y eficiente que eso.

Profundizar en el dominio de la aplicación para encontrar los criterios de partición

Aprendimos una lección esencial al analizar el dominio de la aplicación y entrevistar a nuestros usuarios. Los expertos en deportes tienden a analizar datos agregados de juegos en la misma competencia. Por ejemplo, una competición de fútbol puede ser una liga, un torneo o un partido único en el que puedes ganar un trofeo. Hay miles de competiciones diferentes. Las más importantes de Europa son la Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 y Primeira Liga.

Esto significa que nuestros usuarios tienen en cuenta datos provenientes de diferentes competiciones muy raramente. Además, prefieren explorar los datos temporada por temporada. En otras palabras, rara vez salen del contexto representado por una competencia deportiva que se juega en una temporada en particular. La estructura de nuestra base de datos expresó este concepto con una tabla llamada SeasonCompetition , cuyo objetivo es asociar una competición a una temporada concreta. Entonces, nos dimos cuenta de que un buen enfoque sería dividir nuestras tablas más grandes en subtablas relacionadas con una SeasonCompetition particular. instancia.

Específicamente, definimos el siguiente formato de nombre para estas nuevas tablas:<tableName>_<seasonCompetitionId> .

En consecuencia, si tuviéramos 100 filas en la SeasonCompetition tabla, tendríamos que dividir los grandes Events tabla en el Events_1 más pequeño , Events_2 , …, Events_100 mesas. Según nuestro análisis, este enfoque conduciría a un aumento considerable del rendimiento en el caso promedio, aunque introduciría algunos gastos generales en los casos más raros.

Coincidencia de los criterios con las consultas más comunes

Antes de codificar y lanzar los scripts para ejecutar esta operación compleja y potencialmente sin retorno, validamos nuestros estudios observando las consultas más comunes realizadas por nuestra aplicación de back-end. Pero al hacerlo, descubrimos que la gran mayoría de las consultas involucraban solo juegos jugados dentro de una competencia de temporada. Esto nos convenció de que teníamos razón. Así que particionamos todas las tablas grandes en la base de datos con el enfoque que acabamos de definir.


SELECT AVG('value') as 'value', SUM('minutes') as 'minutes'
FROM 'Events'
WHERE 'parameterId' = 15 AND 'gameId' IN(223,241,245,212,201,299,187,304,187,205)
GROUP BY 'teamId'

Ahora, estudiemos los pros y los contras de esta decisión.

Ventajas

  • Ejecutar consultas en una tabla que contiene como máximo medio millón de filas es mucho más eficaz que hacerlo en una tabla de quinientos millones de filas, especialmente cuando se trata de consultas agregadas.
  • Las tablas más pequeñas son más fáciles de administrar y actualizar. Agregar una columna o índice ni siquiera es comparable a antes en términos de tiempo y espacio. Además, cada SeasonCompetition es diferente y requiere análisis diferentes. En consecuencia, puede requerir columnas e índices especiales, y la partición antes mencionada nos permite manejar esto fácilmente.
  • El proveedor podría modificar algunos datos. Esto nos obliga a realizar consultas de eliminación y actualización, que son infinitamente más rápidas en tablas tan pequeñas. Además, siempre se refieren solo a algunos juegos de una SeasonCompetition particular. , por lo que ahora solo necesitamos operar en una sola tabla.

Contras

  • Antes de realizar una consulta en estas subtablas, necesitamos saber el seasonCompetitionId asociado a los juegos de interés. Esto se debe a que seasonCompetitionId El valor se utiliza en el nombre de la tabla. Por lo tanto, nuestro backend necesita recuperar esta información antes de ejecutar la consulta mirando los juegos en análisis, lo que representa una pequeña sobrecarga.
  • Cuando una consulta involucra un conjunto de juegos que involucran muchas SeasonCompetitions , la aplicación de fondo debe ejecutar una consulta en cada subtabla. Entonces, en estos casos, ya no podemos agregar los datos a nivel de base de datos, y debemos hacerlo a nivel de aplicación. Esto introduce cierta complejidad en la lógica de back-end. Al mismo tiempo, podemos ejecutar estas consultas en paralelo. Además, podemos agregar los datos recuperados de manera eficiente y en paralelo.
  • Administrar una base de datos con miles de tablas no es fácil y puede ser un desafío explorarlo en un cliente. De manera similar, agregar una nueva columna o actualizar una columna existente en cada tabla es engorroso y requiere un script personalizado.

Efectos de la partición de datos basada en el contexto sobre el rendimiento

Veamos ahora la mejora de tiempo lograda al ejecutar una consulta en la nueva base de datos particionada.

  • Mejora de tiempo en el caso promedio (consulta que involucra solo una SeasonCompetition ):de 20x a 40x
  • Mejora de tiempo en el caso general (consulta que involucra una o más SeasonCompetitions ):de 5x a 10x

Pensamientos finales

Particionar su base de datos es, sin duda, una excelente manera de mejorar el rendimiento, especialmente en bases de datos grandes. Sin embargo, hacerlo sin considerar su dominio de aplicación en particular podría ser un error o llevar a una solución ineficiente. En cambio, tomarse su tiempo para estudiar el dominio entrevistando a expertos y sus usuarios y observando las consultas más ejecutadas es crucial para concebir criterios de partición altamente eficientes. Este artículo le mostró cómo hacerlo y demostró los resultados de dicho enfoque a través de un estudio de caso del mundo real.