Saltar al contenido principal
ClickHouse ofrece compatibilidad completa con JOIN, con una amplia variedad de algoritmos de JOIN. Para maximizar el rendimiento, recomendamos seguir las sugerencias de optimización de JOIN que se enumeran en esta guía.
  • Para un rendimiento óptimo, conviene reducir el número de JOIN en las consultas, especialmente en cargas de trabajo analíticas en tiempo real donde se requiere latencia de milisegundos. Intenta no superar los 3 o 4 joins por consulta. En la sección de modelado de datos detallamos varios cambios para minimizar los joins, como la desnormalización, los diccionarios y las vistas materializadas.
  • A partir de ClickHouse 24.12, el planificador de consultas reordena automáticamente los joins entre dos tablas para situar la tabla más pequeña en el lado derecho y así obtener un rendimiento óptimo. En la versión 25.9, esto se amplió para optimizar el orden de join en consultas que combinan tres o más tablas.
  • Si tu consulta requiere un direct join, es decir, un LEFT ANY JOIN, como se muestra a continuación, recomendamos usar Dictionaries siempre que sea posible.
  • Si realizas inner joins, a menudo resulta más eficiente escribirlos como subconsultas con la cláusula IN. Considera las siguientes consultas, que son funcionalmente equivalentes. Ambas encuentran el número de posts que no mencionan ClickHouse en la pregunta, pero sí en los comments.
SELECT count()
FROM stackoverflow.posts AS p
ANY INNER `JOIN` stackoverflow.comments AS c ON p.Id = c.PostId
WHERE (p.Title != '') AND (p.Title NOT ILIKE '%clickhouse%') AND (p.Body NOT ILIKE '%clickhouse%') AND (c.Text ILIKE '%clickhouse%')

┌─count()─┐
86
└─────────┘

1 row in set. Elapsed: 8.209 sec. Processed 150.20 million rows, 56.05 GB (18.30 million rows/s., 6.83 GB/s.)
Peak memory usage: 1.23 GiB.
Tenga en cuenta que usamos un ANY INNER JOIN en lugar de un simple INNER JOIN, ya que no queremos el producto cartesiano; es decir, queremos solo una coincidencia por cada post. Este join puede reescribirse mediante una subconsulta, lo que mejora significativamente el rendimiento:
SELECT count()
FROM stackoverflow.posts
WHERE (Title != '') AND (Title NOT ILIKE '%clickhouse%') AND (Body NOT ILIKE '%clickhouse%') AND (Id IN (
        SELECT PostId
        FROM stackoverflow.comments
        WHERE Text ILIKE '%clickhouse%'
))
┌─count()─┐
86
└─────────┘

1 row in set. Elapsed: 2.284 sec. Processed 150.20 million rows, 16.61 GB (65.76 million rows/s., 7.27 GB/s.)
Peak memory usage: 323.52 MiB.
Aunque ClickHouse intenta propagar las condiciones a todas las cláusulas JOIN y subconsultas, recomendamos que los usuarios las apliquen siempre manualmente a todas las subcláusulas cuando sea posible, minimizando así el tamaño de los datos que llegan a JOIN. Considere el siguiente ejemplo, en el que queremos calcular el número de votos positivos de las publicaciones relacionadas con Java desde 2020. Una consulta ingenua, con la tabla más grande en el lado izquierdo, se completa en 56 s:
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.posts AS p
INNER JOIN stackoverflow.votes AS v ON p.Id = v.PostId
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 56.642 sec. Processed 252.30 million rows, 1.62 GB (4.45 million rows/s., 28.60 MB/s.)
Reordenar este join mejora drásticamente el rendimiento, hasta 1,5 s:
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.votes AS v
INNER JOIN stackoverflow.posts AS p ON v.PostId = p.Id
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 1.519 sec. Processed 252.30 million rows, 1.62 GB (166.06 million rows/s., 1.07 GB/s.)
Añadir un filtro a la tabla izquierda mejora aún más el rendimiento, hasta 0,5 s.
SELECT countIf(VoteTypeId = 2) AS upvotes
FROM stackoverflow.votes AS v
INNER JOIN stackoverflow.posts AS p ON v.PostId = p.Id
WHERE has(arrayFilter(t -> (t != ''), splitByChar('|', p.Tags)), 'java') AND (p.CreationDate >= '2020-01-01') AND (v.CreationDate >= '2020-01-01')

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 0.597 sec. Processed 81.14 million rows, 1.31 GB (135.82 million rows/s., 2.19 GB/s.)
Peak memory usage: 249.42 MiB.
Esta consulta puede optimizarse aún más moviendo el INNER JOIN a una subconsulta, como se indicó anteriormente, manteniendo el filtro tanto en la consulta externa como en la interna.
SELECT count() AS upvotes
FROM stackoverflow.votes
WHERE (VoteTypeId = 2) AND (PostId IN (
        SELECT Id
        FROM stackoverflow.posts
        WHERE (CreationDate >= '2020-01-01') AND has(arrayFilter(t -> (t != ''), splitByChar('|', Tags)), 'java')
))

┌─upvotes─┐
261915
└─────────┘

1 row in set. Elapsed: 0.383 sec. Processed 99.64 million rows, 804.55 MB (259.85 million rows/s., 2.10 GB/s.)
Peak memory usage: 250.66 MiB.

Elección de un algoritmo de JOIN

ClickHouse admite varios algoritmos de JOIN. Estos algoritmos suelen intercambiar uso de memoria por rendimiento. A continuación se ofrece una visión general de los algoritmos de JOIN de ClickHouse en función de su consumo relativo de memoria y su tiempo de ejecución:

Estos algoritmos determinan cómo se planifica y ejecuta una consulta con JOIN. De forma predeterminada, ClickHouse utiliza el algoritmo direct o hash join en función del tipo de JOIN y la strictness utilizados, así como del engine de las tablas involucradas. Como alternativa, ClickHouse puede configurarse para elegir de forma adaptativa y cambiar dinámicamente el algoritmo de JOIN que se usará en tiempo de ejecución, según la disponibilidad y el uso de recursos: cuando join_algorithm=auto, ClickHouse prueba primero el algoritmo hash join y, si se supera el límite de memoria de ese algoritmo, cambia sobre la marcha a partial merge join. Puede ver qué algoritmo se eligió mediante trace logging. ClickHouse también le permite especificar directamente el algoritmo de JOIN deseado mediante la configuración join_algorithm. A continuación se muestran los tipos de JOIN admitidos por cada algoritmo de JOIN, y deben tenerse en cuenta antes de realizar la optimización:

Puede encontrar aquí una descripción detallada de cada algoritmo JOIN, incluidas sus ventajas, desventajas y propiedades de escalado. La elección de los algoritmos de JOIN adecuados depende de si busca optimizar la memoria o el rendimiento.

Optimización del rendimiento de JOIN

Si su métrica de optimización principal es el rendimiento y busca ejecutar el join lo más rápido posible, puede usar el siguiente árbol de decisión para elegir el algoritmo de join adecuado:

  • (1) Si los datos de la tabla del lado derecho pueden precargarse en una estructura de datos key-value en memoria y de baja latencia, por ejemplo, un diccionario, y si la clave de join coincide con el atributo clave del almacenamiento key-value subyacente, y si la semántica de LEFT ANY JOIN es adecuada, entonces puede aplicarse el direct join, que ofrece el enfoque más rápido.
  • (2) Si el orden físico de las filas de su tabla coincide con el orden de clasificación de la clave de join, entonces depende. En este caso, el full sorting merge join omite la fase de ordenación, lo que reduce significativamente el uso de memoria y, según el tamaño de los datos y la distribución de valores de la clave de join, puede ofrecer tiempos de ejecución más rápidos que algunos de los algoritmos hash join.
  • (3) Si la tabla derecha cabe en memoria, incluso con la sobrecarga adicional de uso de memoria del parallel hash join, entonces este algoritmo o el hash join pueden ser más rápidos. Esto depende del volumen de datos, de los tipos de datos y de la distribución de valores de las columnas de la clave de join.
  • (4) Si la tabla derecha no cabe en memoria, entonces vuelve a depender. ClickHouse ofrece tres algoritmos de join que no están limitados por la memoria. Los tres hacen spill de datos a disco de forma temporal. Full sorting merge join y partial merge join requieren una ordenación previa de los datos. En cambio, grace hash join construye tablas hash a partir de los datos. Según el volumen de datos, los tipos de datos y la distribución de valores de las columnas de la clave de join, puede haber casos en los que construir tablas hash a partir de los datos sea más rápido que ordenarlos. Y viceversa.
Partial merge join está optimizado para minimizar el uso de memoria al unir tablas grandes, a costa de una velocidad de join bastante baja. Esto ocurre especialmente cuando el orden físico de las filas de la tabla izquierda no coincide con el orden de clasificación de la clave de join. Grace hash join es el más flexible de los tres algoritmos de join no limitados por memoria y ofrece un buen control del equilibrio entre uso de memoria y velocidad de join con su configuración grace_hash_join_initial_buckets. Según el volumen de datos, grace hash puede ser más rápido o más lento que el algoritmo partial merge cuando la cantidad de buckets se elige de modo que el uso de memoria de ambos algoritmos quede aproximadamente alineado. Cuando el uso de memoria de grace hash join se configura para que quede aproximadamente alineado con el de full sorting merge, full sorting merge siempre fue más rápido en nuestras pruebas. Cuál de los tres algoritmos no limitados por memoria es el más rápido depende del volumen de datos, los tipos de datos y la distribución de valores de las columnas de la clave de join. Siempre es mejor ejecutar algunos benchmarks con volúmenes de datos realistas para determinar qué algoritmo es el más rápido.

Optimización para minimizar el uso de memoria

Si quiere optimizar un join para reducir al mínimo el uso de memoria, en lugar de priorizar el tiempo de ejecución más rápido, puede usar este árbol de decisión:

  • (1) Si el orden físico de las filas de su tabla coincide con el orden de clasificación de la clave del join, el uso de memoria del full sorting merge join será el mínimo posible. Además, ofrece una buena velocidad de join porque la fase de ordenación está deshabilitada.
  • (2) El grace hash join puede ajustarse para lograr un uso de memoria muy bajo configurando un número elevado de buckets, a costa de la velocidad del join. El partial merge join usa intencionadamente poca memoria principal. El full sorting merge join con la ordenación externa habilitada suele usar más memoria que el partial merge join (suponiendo que el orden de las filas no coincida con el orden de clasificación de la clave), con la ventaja de ofrecer un tiempo de ejecución del join significativamente mejor.
Si necesita más detalles sobre lo anterior, le recomendamos la siguiente serie de blogs.
Última modificación el 10 de junio de 2026