Databricks — Qué es y qué no es “Bucketing”

Fermín Martínez
LaLiga Tech
Published in
16 min readFeb 5, 2021

Introducción

Al igual que en muchos otros negocios, en LaLiga ha aumentado de forma exponencial el volumen de datos que se almacenan, procesan y explotan. Esto nos plantea a todos aquellos que trabajamos en el ámbito del dato la necesidad de utilizar herramientas que nos permitan seguir modelando y estructurando la información de forma que pueda estar disponible para su explotación, para que sea útil, independientemente del volumen o la latencia de esta información.

Para este artículo hablaremos de un conjunto de información muy concreto que se está utilizando dentro de LaLiga y que se explota a través de Azure Databricks: estamos hablando de datos como aquellos que se generan cuando cualquiera de nosotros utilizamos una aplicación móvil, aquella información relativa a la analítica digital de las aplicaciones.

Las tablas que utilizaremos son básicamente dos:

tel_hit: Tabla que almacena cualquier evento ocurrido en una aplicación de LaLiga. Actualmente estamos hablando 7 TB de datos.

tel_guest_client_properties: Cada dispositivo que utiliza una aplicación está identificado mediante un número. Esta tabla contiene todos los posibles números de dispositivos junto con algunas propiedades, tales como: Plataforma en la que se usa el dispositivo, tipo de dispositivo, marca, modelo, versión, etc. Esta tabla tiene, a día de hoy 2 GB de datos.

Como vemos, el volumen de datos que se utiliza no es trivial. Los procesos que tenemos corriendo sobre la plataforma Databricks funcionan correctamente, y nos permiten explotar la información tanto para el cálculo de variables complejas como segmentaciones, o a través de Cuadros de Mando, que a día de hoy tenemos implementados en Power BI.

Sin embargo, cada día se añade más y más información, se añaden nuevos orígenes o surge la necesidad de aplicar nuevos algoritmos o modelos matemáticos sobre los datos, etc, por lo que nunca está de más investigar de qué forma podemos mejorar y optimizar la forma en que estamos estructurando y almacenando estos datos.

La forma en que, actualmente, se organiza la tabla tel_hit, es la siguiente:

IdClient: Identifica a cada una de las aplicaciones en las que pueden ocurrir eventos.

DtEvent: Fecha en la que ocurre un evento.

Estrategia de particionamiento de tel_hit

Es decir, los datos se organizan en un primer nivel de partición, que identifica a la aplicación en la que se dan los eventos de analítica digital. Existe un segundo nivel dentro de cada partición de aplicación, que es la fecha en la que ocurre el evento.

En cuanto a la tabla tel_guest_client_properties que utilizaremos para los ejemplos, es una tabla que no se encuentra particionada.

Particionamientos

Es importante distinguir entre Particiones de Tabla, y Particiones de Spark.

Cuando hablamos de Particiones de Tabla estamos hablando de la separación física que crea Spark en la estructura de directorios de en la que se almacena el contenido. En la figura de arriba, los campos IdClient y DtEvent son los campos de particionamiento de la tabla. Este tipo de particionamiento es muy útil porque Spark es capaz de detectar si en una consulta existe un predicado WHERE que filtre por estos campos, seleccionando así los datos que cuelgan de esas particiones y obviando directamente el resto.

Cuando hablamos de Particiones Spark estamos hablando de algo completamente distinto. El conjunto de datos total que Spark tiene que procesar se divide en una agrupación lógica denominada Particiones RDD. Normalmente cada fichero de datos que existe en un directorio se podría considerar como una partición, aunque una partición RDD puede estar formada por N ficheros de datos.

El concepto de particiones RDD es muy importante, porque el grado de paralelismo posible en la ejecución de un proceso dependerá directamente de este número de particiones.

Por ejemplo, hemos creado una tabla sin particiones de tabla en el datalake, y se vería de esta forma (al no tener particiones no hay subdirectorios):

Tabla sin particionar. Directamente un conjunto de ficheros parquet

El conteo total de ficheros existentes dentro del directorio de la tabla es de: 6.619 ficheros parquet. Sin embargo si preguntamos a Spark cuantas particiones RDD tiene esta tabla:

Número de particiones de un dataframe

La mayor parte de las particiones RDD se corresponderán con un fichero, pero hay algunas que se componen de un puñado de estos ficheros.

Spark siempre asignará el procesamiento de datos de una partición RDD a un único worker o hilo de ejecución. Por eso, como veremos más adelante, el tamaño y la distribución de los datos dentro de estas particiones RDD es muy importante.

Objetivo

Me planteo durante este artículo explorar de qué forma sería posible reorganizar estos datos de forma que:

  1. Se reduzca el número de particiones totales a utilizar. Es importante que el número de particiones de una tabla se encuentre parcialmente acotada (más allá de la dimensión temporal, que obviamente no se encuentra acotada).
  2. Se mejore el tiempo de consulta en entornos exploratorios, es decir, consultas interactivas.
  3. Se mejore el tiempo de los procesos que utilizan estas tablas.

Para este objetivo investigaremos el uso de bucketing como técnica de optimización disponible en Spark. Muchas veces he leído en diferentes artículos de internet sobre esta técnica. Básicamente consiste en agrupar los datos de una tabla en base al valor de una o varias columnas, de forma que registros con igual valor caigan en el mismo bucket (aunque un bucket puede tener valores distintos de la columna sobre la que hacemos bucketing).

Gráficamente:

Técnica de Bucketing

Esta información sobre como se agrupa la tabla se almacena en el Metastore de Databricks. De esta forma, al consultar la tabla, spark sabe, en función del valor del campo Id, dónde tiene que buscar los datos, y se evita procesar el resto de buckets.

El bucketing, en el sistema de almacenamiento, no se percibe a primera vista. Los buckets no se distinguen porque impliquen la creación de directorios en el Datalake. Es posible saber a qué bucket pertenece un fichero de datos por la nomenclatura que se le da al fichero parquet que se almacena:

Tabla “bucketizada” en el Datalake

En la figura de arriba se muestra un ejemplo de algunos de los ficheros de una tabla en la que se han dividido los datos en 64 buckets.

Entendiendo el concepto de Bucketing, la pregunta que nos podemos hacer es: ¿podríamos utilizar esta técnica como una forma de particionamiento de los datos?.

Empecemos por ahí nuestra exploración.

Bucketing vs Particionamiento

Bucketing como forma de particionamiento

Para esta exploración vamos a crear una tabla análoga a tel_hit en la que vamos a crear buckets según el campo de fecha DtEvent. La duda que me propongo resolver es, ¿podríamos tener esta tabla estructurada tal y como se presenta en la siguiente imagen?

tel_hit sin particiones y con “Buckets” por fecha

Es decir, yéndonos a un extremo: ¿podríamos no particionar la tabla y simplemente crear buckets según las columnas que nos interesen?

Por lo tanto, creamos esta tabla en nuestro entorno de exploración:

(   
sdf_tel_hit
.write
.mode('overwrite')
.bucketBy(30, 'DtEvent')
.sortBy('DtEvent')
.option('path', 'adl://XXXdatalake/sandboxbi/blog_tel_hit_ordered') .saveAsTable('sandboxbi.blog_tel_hit_ordered')
)

Una vez creada la tabla vamos a intentar utilizar los buckets tal y como solemos utilizar el particionamiento de tablas, es decir, creamos una consulta en cuyo predicado WHERE filtramos por una fecha concreta:

(   
spark
.read
.table('sandboxbi.blog_tel_hit_ordered')
.where('DtEvent = "2020-10-15"')
.count()
)

Y vemos la ejecución de la consulta:

Consulta tabla bucketizada por fecha

¿Qué está ocurriendo aquí? En principio en el cluster de exploración en el que estamos ejecutando nuestra consulta hay disponibles 64 hilos de ejecución. Entonces, ¿por qué sólo está utilizando un hilo?

Analicemos el plan de ejecución de la consulta para ver si nos da alguna pista:

sdf_bucket = spark.read.table('sandboxbi.blog_tel_hit_ordered') sdf_bucket.where('DtEvent = "2020-10-15"').explain(extended=True)== Physical Plan == 
*(1) Project [IsEvent#12, CoHitType#13, ... 60 more fields]
+- *(1) Filter (isnotnull(DtEvent#95) && (DtEvent#95 = 2020-10-15)) +- *(1) FileScan parquet sandboxbi.blog_tel_hit_ordered[IsEvent#12.. 60 more fields]
Batched: true,
DataFilters: [isnotnull(DtEvent#95), (DtEvent#95 = 2020-10-15)], Format: Parquet,
Location: InMemoryFileIndex[adl://XXXdatalake/sandboxbi/blog_tel_hit_ordered],
PartitionFilters: [],
PushedFilters: [IsNotNull(DtEvent), EqualTo(DtEvent,2020-10-15)], ReadSchema: struct<IsEvent:boolean,CoHitType:string,QtHits:int,IsPageView:boolean,DsPageTitle:string,NaPageUR..., SelectedBucketsCount: 1 out of 30

La parte interesante de este plan de ejecución es:

SelectedBucketsCount: 1 out of 30

¿Qué está haciendo Spark realmente?

Ha analizado la consulta, detecta que se realiza un filtro por un campo por el que se han creado Buckets, asigna a cada bucket a procesar un hilo de ejecución.

De hecho, esta tabla se ha creado con 30 buckets, y Spark ha lanzado 30 hilos de ejecución:

30 hilos lanzados, sólo 1 efectivo

Lo que ocurre es que estos hilos hacen lo siguiente:

  1. Se preguntan: ¿tengo que procesar datos en mi bucket? Aplican una función hash al predicado de la consulta: WHERE DtEvent = ”2020–10–15”
  2. Comprueban si el resultado de la función hash es su bucket.
  3. En caso de no lo sea, termina el hilo. En caso de que sí lo sea, procesa los datos.

Por lo tanto sí se están procesando los datos en paralelo, pero el resultado final es que siempre se van a procesar los datos por un único hilo.

Quizás se observe mejor en la siguiente figura:

En una primera etapa (Stage 24, activa en el pantallazo) Spark lanza 30 tareas. El resultado de estas 30 tareas deben ser todos los datos leídos en los buckets.

En una segunda tarea (Stage 25) se realiza un count de los datos obtenidos en el primer Stage.

Si analizamos las tareas lanzadas en la primera etapa:

Sólo una tarea procesa datos

Vemos que sólo una de ellas ha realizado trabajo efectivo, tardando 3.7 minutos en completarse. El resto de tareas (29) se completan en 2 milisegundos (el tiempo que tardan en descartar que no hay nada que hacer en su bucket correspondiente).

Este no es el resultado que nosotros queremos. Lo que buscábamos es que Spark se centre únicamente en los datos del Bucket que queremos consultar (eso sí lo hace), pero además queríamos que ese conjunto de datos se procesara utilizando toda la capacidad de paralelismo del cluster (cosa que no está ocurriendo).

¿Podemos hacer algo para que ese bucket se procese en paralelo? En la tabla que hemos creado no hemos utilizado particiones de tabla, únicamente buckets. ¿Qué ocurriría si combináramos la tabla con particiones y dentro de cada partición existieran buckets? ¿procesaría Spark cada bucket de cada partición en paralelo?

Combinando Particionamiento de Tabla y Bucketing

Para responder a la pregunta que nos hemos hecho en el punto anterior vamos a crear una nueva tabla, esta vez utilizando Particionamiento y bucketing al mismo tiempo.

La tabla que vamos a crear tiene la siguiente estructura:

Tabla particionada y con Buckets
(   
sdf
.write
.mode('overwrite')
.partitionBy('IdClient')
.bucketBy(30, 'DtEvent')
.sortBy('DtEvent')
.option('path','adl://d/sandboxbi/blog_tel_hit_ordered_partitioned') .saveAsTable('sandboxbi.blog_tel_hit_ordered_partitioned') )

Y lanzamos nuestra consulta de prueba:

(   
spark
.read
.table('sandboxbi.blog_tel_hit_ordered_partitioned') .where('DtEvent = "2020-10-15"')
.count()
)

En un primer Job, Spark consulta qué particiones es necesario utilizar, descartando el resto:

Operación de listado de particiones

Después, vemos que la ejecución es análoga al caso anterior:

Sólo una tarea procesa datos

Donde se lanzan tantos hilos como buckets tenemos dentro de la tabla, y sólo uno de ellos procesa la información.

Lo que está ocurriendo en realidad es que spark agrupa cada Bucket 1, por ejemplo, de todas las particiones para crear un único Bucket 1, y así con todos los demás. Una vez que tiene las agrupaciones ejecuta los 30 hilos que tienen que procesar cada uno de los bucket y, como el bucket 2020–10–15 ya es único (el resultado de los bucket 2020–10–15 de todas las particiones) sólo un hilo acaba haciendo todo el trabajo.

Esto podemos observarlo en las estadísticas de ejecución del Stage:

Estadísticas de Tareas

Tanto el mínimo, como el percentil 25, 50 y 75, nos dan una duración muy pequeña, correspondiente a todos los hilos que han comprobado que no tienen que hacer nada en su bucket (29), y sólo un hilo ha realizado trabajo real, que se corresponde con lo que vemos en la columna Max de la figura de arriba.

Vemos entonces que, lo que nos estábamos planteando, utilizar bucketing como un mecanismo similar al particionamiento de tablas, no es posible. En vez de mejorar el rendimiento lo que estamos haciendo es empeorarlo, ya que desaprovechamos toda la capacidad distribuida de nuestro cluster.

Por lo tanto, ahora sabemos:

* Bucketing y Particionamiento no son lo mismo.

* Utilizar buckets en campos que normalmente particionaríamos empeora el rendimiento de las consultas.

Entonces, ¿cómo podemos utilizar bucketing para optimizar el rendimiento de nuestras consultas y el almacenamiento de nuestros datos? Si no lo utilizamos sobre un campo que normalmente particionaríamos, ¿puede ser que debamos utilizarlo con otro tipo de campos?

Si la técnica de bucketing lo que permite es localizar de forma muy eficiente las particiones RDD donde se encuentran los datos que queremos procesar, ¿sería posible utilizarlo a modo de índice clásico de una base de datos SQL?

Vamos a explorar esta vía en la siguiente sección.

Bucketing como “indexador” de datos

Funcionamiento de Operaciones Join en Spark

Antes de entender los siguientes pasos que vamos a dar, es necesario comprender qué ocurre dentro de Spark cuando se realizan operaciones join.

Siguiendo con el ejemplo de las dos tablas que utilizamos en estos ejemplos, supongamos que ejecutamos una consulta con la que queremos conocer: para cada evento de aplicación que ocurra, cruzar con nuestra dimensión de dispositivos para saber en qué tipo de plataforma se ha realizado el evento.

Esta consulta tendría esta pinta:

(   
spark.read.table('tel_hit')
.join(
spark.read.table('tel_guest_client_properties'),
on=['IdGuest']
)
)

¿Qué es lo que ocurre internamente en Spark? Al estar los datos particionados (particiones RDD), y al vivir cada una de esas particiones en workers o incluso máquinas distintas, Spark va a intentar juntar los datos que tengan las mismas claves de join en las mismas particiones y en los mismos workers. Una vez que esto ocurre ya sería posible comprobar las condiciones de igualdad de nuestra consulta join.

Esta operación se conoce como Shuffling, y es muy costosa, ya que implica un intercambio de datos intensivo entre workers a través de la red.

Gráficamente:

Shuffling

En Spark podemos visualizar esta situación analizando el plan de ejecución de nuestras consultas. Por ejemplo, para la consulta anterior:

Plan Ejecución

Nota: No todas las operaciones join que se realizan en Spark funcionan de esta forma. Lo que acabamos de explicar aplica a un SortMergeJoin, que es el caso más frecuente en tablas que tienen un cierto volumen.

Optimizando nuestro plan de ejecución con Bucketing

Como acabamos de ver, si no hacemos nada con nuestras tablas y lanzamos una operación Join, irremediablemente se va a producir Shuffling, y esta operación va a penalizar el tiempo de ejecución de nuestras consultas.

Por otro lado, tal y como hemos visto en las primeras secciones de este artículo, la técnica de bucketing nos permite agrupar datos con una misma clave en las mismas particiones. Entonces, ¿podemos usar bucketing para evitar el intercambio masivo de datos que se produce al reorganizar particiones cuando hacemos shuffling?

Efectivamente. Vamos a demostrarlo con un ejemplo. Vamos a crear dos tablas equivalentes a tel_hit y a tel_guest_client_properties, pero que se encuentren “bucketeadas” por la columna por la que hacemos join en nuestra consulta, en este caso IdGuest.

(   
spark.read.table('pg_dig_telemetry.tel_guest_client_properties')
.write
.mode('overwrite')
.bucketBy(512, 'IdGuest')
.sortBy('IdGuest')
.option('path', 'adl:///sandboxbi/blog_tel_guest_client_bucketed')
.saveAsTable('sandboxbi.blog_tel_guest_client_bucketed')
)
(
spark.read.table('mg_dig_telemetry.tel_hit')
.where('DtEvent = "2020-10-02"')
.write .mode('append')
.bucketBy(512, 'IdGuest')
.sortBy('IdClient')
.partitionBy('DtEvent')
.option('path', 'adl:///sandboxbi/blog_tel_hit_bucketed')
.saveAsTable('sandboxbi.blog_tel_hit_bucketed')
)

En la tabla equivalente a tel_hit (eventos de aplicación) estamos particionando por fecha del evento, y creando 512 buckets en cada partición de fecha.

Lanzamos nuestra consulta y analizamos el plan de ejecución:

(   
spark.read.table('sandboxbi.blog_tel_hit_bucketed')
.join(
spark.read.table('sandboxbi.blog_tel_guest_client_bucketed'),
on=['IdGuest']
)
.explain(extended=True)
)
Plan de ejecución sin exchange

Como vemos, ha desaparecido la fase de exchange de ambas consultas. ¿Por qué ocurre esto? En realidad lo que hemos hecho es mover esa fase de shuffling al momento en que guardamos los datos en la tabla, de esta forma, a la hora de ejecutar la consulta, es un paso que ya no es necesario.

Gráficamente, a la hora de ejecutar la consulta, esta es la disposición de los datos:

Datos con bucketing

Puesto que esto es así, nuestra consulta debería ir más rápido, ¿no? Vamos a comprobarlo.

Nuestra consulta utilizando las tablas originales tarda 1.68 minutos, ¡mientras que la consulta con las tablas construidas en base a buckets tarda 10 minutos! ¿Qué está ocurriendo? Nuestro plan de ejecución es mejor, y sin embargo el rendimiento de la consulta ha descendido drásticamente.

Bucketing y almacenamiento de los datos

Acabamos de ver cómo, efectivamente, el uso de tablas con buckets permite que spark aplique un plan de ejecución que, a priori, es más efectivo que el que se lanza cuando no usamos tablas con buckets.

Sin embargo, el plan de ejecución no es todo, y para comprender lo que está ocurriendo es importante entender cómo Spark almacena los datos en tablas con particiones y con buckets.

Vamos a buscar en nuestro datalake cómo está creada nuestra tabla original, y cómo está creada nuestra tabla con buckets.

Para este ejemplo, hemos sacado a una tabla llamada blog_tel_hit los datos que nos interesan de cara a realizar las pruebas. Esta tabla contiene 512 particiones rdd.

La tabla formada a partir de buckets contiene más de 250.000 ficheros parquet! ¿Por qué? En realidad, lo que está ocurriendo es que, a la hora de crear la tabla con buckets, spark genera, para cada partición RDD de datos que tenemos (no es exactamente, pero pensad en ello como cada uno de los ficheros parquet) 512 buckets, y cada uno de estos buckets en un fichero distinto. 512 x 512 = 262.144.

Un aprendizaje que sacamos de esto es: si queremos optimizar el uso de nuestras tablas y nuestros procesos:

  • Es necesario saber cómo Spark genera los planes de ejecución.
  • Es necesario saber cómo se almacenan las tablas en datalake.
  • Es necesario conocer tus datos.

Bien, vamos a ver cómo podemos solucionar esta situación. Podemos obligar a Spark a que el resultado de nuestra tabla tenga (aproximadamente) 512 ficheros parquet, correspondientes a los 512 buckets que queremos en la tabla final. Esto podemos hacerlo con repartition.

(   
spark.read.table('mg_dig_telemetry.tel_hit')
.where('DtEvent = "2020-10-01"')
.repartition(512, 'IdGuest')
.write
.mode('overwrite')
.bucketBy(512, 'IdGuest')
.sortBy('IdGuest')
.partitionBy('DtEvent')
.option('path', 'adl:///sandboxbi/blog_tel_hit_bucketed3')
.saveAsTable('sandboxbi.blog_tel_hit_bucketed3')
)

Después de crear nuestra tabla de esta forma, para dos días de datos tenemos:

Estadísticas directorio datalake

Es decir, 512 ficheros para cada día, que son los 1.024 que nos deberían salir + los ficheros de control que genera Spark. Ahora ya tenemos la tabla compuesta de muchos menos ficheros y mucho más compactos.

Vamos a probar ahora el rendimiento de nuestras consultas con algo un poco más complicado: queremos saber, por plataforma, cuantos hits tenemos.

Lanzamos la consulta con nuestras tablas originales:

1.35 minutos de ejecución

Obtenemos un tiempo de ejecución de 1.35 minutos, como se puede ver en la figura. En esta consulta filtramos los días que estamos utilizando para el ejemplo. Spark emplea muy poquito tiempo en descubrir cuales son esas particiones y ejecutar la consulta, por lo que no nos afecta apenas en nuestra comparativa.

Ejecutamos la consulta con nuestra tabla formada con buckets:

44 segundos de ejecución

Ahora sí, podemos ver que hemos reducido a la mitad el tiempo de ejecución de nuestra consulta sobre dos días de datos.

Esta optimización en el tiempo de ejecución se mantiene con un mayor volumen. Utilizando dos semanas de datos la consulta contra las tablas originales emplea 3.50 minutos, mientras que la consulta con las tablas que utilizan buckets emplea 1.88 minutos.

Por lo tanto, hemos conseguido ya dos de los objetivos que nos planteábamos inicialmente:

  1. Se reduzca el número de particiones totales a utilizar: Hemos visto que, mediante la utilización de Buckets, puede no ser necesario subparticionar las tablas y crear más de un nivel de particionamiento. Por ejemplo, podríamos particionar (partición de Tabla) nuestra tabla de eventos por fecha, y simplemente crear buckets dentro de cada partición. Al reducir el número de divisiones físicas (directorios) de los datos, tenemos menos ficheros que procesar, y más compactos, lo que beneficia al rendimiento. Aunque, como hemos visto también, es importante crear los buckets en campos que tengan una cardinalidad muy alta (como IdGuest), ya que si no el grado de paralelismo puede verse muy reducido.
    Como regla general: nunca crear buckets en un campo candidato a particionar.
  2. Se mejore el tiempo de los procesos que utilizan estas tablas: Hemos visto como aplicando bucketing podemos reducir a la mitad el tiempo de ejecución de nuestra consulta.

Nos queda un objetivo por cumplir: Mejora del rendimiento en consultas interactivas.

Bucketing y consultas interactivas

Si lanzamos consultas muy específicas, que buscan un registro muy concreto de toda la información que se encuentra almacenada, lo normal en un sistema de Big Data es que el tiempo empleado para encontrar ese registro sea de varios órdenes de magnitud mayor en comparación con una BBDD tradicional. Sin embargo, la técnica de Bucketing nos puede ayudar a reducir tiempos.

Vamos a ver en primer lugar qué hace Spark con una consulta con un filtro muy, muy específico, por ejemplo:

display( 
spark.read.table('mg_dig_telemetry.tel_hit')
.where('DtEvent = "2020-10-01"')
.where('IdGuest = "44a8245f-a97b-46d0-baf1-579a9ca15356"')
)

La ejecución de esta consulta se podría resumir en:

Vemos como, al haber definido buckets sobre el campo IdGuest, Spark sabe exactamente, dentro de todos los ficheros que componen su tabla, en cuales tiene que mirar porque, en caso de estar, va a estar en esos y no en otro ficheros.

Esto podemos verlo en el plan de ejecución de la consulta:

== Physical Plan == CollectLimit 1001 +- 
*(1) Project [IsEvent#7426, ... 60 more fields] +-
*(1) Filter (isnotnull(IdGuest#7439) && (IdGuest#7439 = 44a8245f-a97b-46d0-baf1-579a9ca15356)) +- *(1) FileScan parquet sandboxbi.blog_tel_hit_bucketed3[IsEvent#7426,... 60 more fields]
Batched: true,
DataFilters: [isnotnull(IdGuest#7439), (IdGuest#7439 = 44a8245f-a97b-46d0-baf1-579a9ca15356)],
Format: Parquet,
Location: PrunedInMemoryFileIndex[adl://XXXdatalake.azuredatalakestore.net/sandboxbi/blog_tel_hit_...,
PartitionCount: 1,
PartitionFilters: [isnotnull(DtEvent#7509), (DtEvent#7509 = 2020-10-01)],
PushedFilters: [IsNotNull(IdGuest), EqualTo(IdGuest,44a8245f-a97b-46d0-baf1-579a9ca15356)],
ReadSchema: struct<IsEvent:boolean,CoHitType:string,QtHits:int,IsPageView:boolean,DsPageTitle:string,NaPageUR...,
SelectedBucketsCount: 1 out of 512

Aparece un SelectedBucketsCount que nos dice en cuantos buckets se encuentra la información que buscamos.

De esta forma, gran parte de los escaneos que se realizaban con la versión anterior de la tabla, ya no se realizan.

Vamos a ver cómo se traduce esto en tiempos de ejecución.

Primero, nuestra consulta utilizando la tabla que no está compuesta de Buckets:

Consulta tabla no bucketizada

El tiempo de ejecución es cercano a 1 minuto.

Probamos nuestra consulta contra la tabla con buckets:

Consulta tabla bucketizada

Y vemos que el tiempo de ejecución mejora espectacularmente, quedando en apenas 4 segundos.

Resumen

Durante este artículo hemos visto:

  • Para qué NO es bucketing: No podemos utilizarlo como una técnica más de particionamiento, es más un indexador de datos que tienen mucha cardinalidad.
  • Cómo podemos utilizarlo para optimizar nuestras consultas y crear menos “jerarquías” de particiones.
  • Cómo podemos utilizarlo para mejorar el rendimiento en consultas interactivas.

--

--