BookinglyTech News
Infraestructura

ClickHouse: De 85 segundos a menos de un milisegundo optimizando consultas

Un caso real de tuning de base de datos columnar que demuestra cómo cambios incrementales en particionado y ordenación resuelven latencias críticas.

3 min de lecturaLobsters0 vistas

Jordi Villar ha publicado un desglose técnico detallado sobre cómo su equipo logró reducir la latencia de una consulta crítica en ClickHouse de 85,7 segundos a menos de un segundo. La historia no involucra trucos magísticos ni hardware nuevo, sino una serie de mejoras incrementales en el diseño de la tabla y la lógica de la consulta, acumuladas durante cuatro meses.

El problema inicial

La consulta original procesaba 1,96 mil millones de filas y 198,69 GB de datos, consumiendo 23,05 GiB de memoria pico. Era una de doce consultas ejecutadas en paralelo en la primera pantalla de la aplicación, lo que significaba que los clientes veían un spinner durante al menos dos minutos al iniciar sesión. El cuello de botella principal era el uso intensivo del motor ReplacingMergeTree con la cláusula FINAL, necesario porque los eventos mutables se actualizaban a un ritmo similar al de los nuevos datos.

La primera gran intervención fue cambiar el esquema de particionado. Originalmente, la tabla estaba particionada por mes (toYYYYMM(event_created_at)), una decisión estándar pero contraproducente en este caso. Las llegaban en lotes que abarcaban múltiples meses, generando muchos archivos por inserción y forzando a ClickHouse a construir pipelines complejos para gestionar las intersecciones de rangos en la fusión FINAL. A veces, esto resultaba en una fusión serializada por un solo hilo.

El cambio propuesto fue particionar por client_customer_id % 36. Era contraintuitivo porque sacrificaban el recorte de particiones por tiempo (no filtran por ese campo), pero ganaban en tres aspectos clave: controlaban el número de partes por inserción, distribuían la carga de forma uniforme evitando "ballenas" de datos en una sola partición, y aseguraban paralelismo en el proceso FINAL sin necesidad de calcular intersecciones de rangos. Esto permitió deshabilitar configuraciones costosas como split_parts_ranges_into_intersecting_and_non_intersecting_final.

Otra mejora fue reordenar la clave de ordenación. Al descubrir que el 16% de las filas correspondían a tipos de eventos que no contribuían a las métricas calculadas, el equipo movió event_type a una posición anterior en la clave de ordenación. Como la clave de ordenación es inmutable tras la creación de la tabla, esto requirió crear una nueva tabla y reescribir todos los datos. El resultado fue una reducción del 15% en la latencia y del 7% en filas leídas, ya que el filtro por clave de ordenación operaba a nivel de granulos, saltando bloques enteros de datos irrelevantes.

También eliminaron un JOIN costoso que permanecía en la consulta sin contribuir al resultado, aprovechando que la parte derecha era una subconsulta vacía condicionalmente. Al quitarlo, simplificaron el plan de ejecución sin cambiar la semántica del resultado.

La lección central no es una receta universal, sino una metodología: identificar cuellos de botella específicos del motor (como la serialización en FINAL), cuestionar decisiones de diseño "obvias" (como particionar por tiempo) y medir el impacto de cada cambio incremental. Para equipos que operan ClickHouse en producción con datos mutables, este caso sirve como recordatorio de que el rendimiento a menudo se gana en el diseño del esquema y la comprensión profunda de cómo el motor gestiona las particiones y las claves de ordenación.