Esta guía asume que los usuarios ya están familiarizados con el concepto de vistas materializadas incrementales y la carga de datos mediante funciones de tabla como s3 y gcs. También recomendamos leer nuestra guía sobre cómo optimizar el rendimiento de inserción desde el almacenamiento de objetos, cuyos consejos pueden aplicarse a las inserciones a lo largo de toda esta guía.
Conjunto de datos de ejemplo
pip.
Por ejemplo, el subconjunto abarca un único día, 2024-12-17, y está disponible públicamente en https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/2024-12-17/. Puede consultarlo con:
El conjunto de datos completo de PyPI, que consta de más de 1 billón de filas, está disponible en nuestro entorno público de demostración clickpy.clickhouse.com. Para obtener más información sobre este conjunto de datos, incluido cómo este demo aprovecha las vistas materializadas para mejorar el rendimiento y cómo se cargan los datos a diario, consulte aquí.
Escenarios de carga histórica
- Carga histórica con ingestión de datos existente - Se están cargando datos nuevos y es necesario hacer una carga histórica de datos anteriores. Estos datos históricos ya se han identificado.
- Agregar vistas materializadas a tablas existentes - Es necesario añadir nuevas vistas materializadas a una configuración en la que ya se han cargado datos históricos y los datos ya se están transmitiendo.
Uso de tablas duplicadas y vistas
pypi y vista materializada, que calcula el número de descargas por proyecto de Python:
{101..200}. Aunque podríamos insertar directamente en pypi, podemos realizar este backfill de forma aislada creando duplicate tables.
Si el backfill falla, no habremos afectado nuestras tablas principales y podremos simplemente hacer truncate en las tablas duplicadas y repetir el proceso.
Para crear nuevas copias de estas vistas, podemos usar la cláusula CREATE TABLE AS con el sufijo _v2:
pypi_v2 y pypi_downloads_v2 y repetir la carga de datos.
Una vez completada la carga de datos, podemos mover los datos de nuestras tablas duplicadas a las tablas principales mediante la cláusula ALTER TABLE MOVE PARTITION.
Nombres de particiónLa llamada
MOVE PARTITION anterior usa el nombre de partición (). Esto corresponde a la única partición de esta tabla (que no está particionada). En las tablas particionadas, deberá invocar varias llamadas a MOVE PARTITION, una por cada partición. El nombre de las particiones actuales puede obtenerse de la tabla system.parts, por ejemplo, SELECT DISTINCT partition FROM system.parts WHERE (table = 'pypi_v2').pypi y pypi_downloads contienen todos los datos. pypi_downloads_v2 y pypi_v2 pueden eliminarse con seguridad.
MOVE PARTITION es a la vez ligera (aprovecha enlaces físicos) y atómica; es decir, o bien falla o bien se completa correctamente, sin ningún estado intermedio.
Usamos este proceso de forma intensiva en los escenarios de carga histórica que se describen a continuación.
Tenga en cuenta que este proceso requiere que los usuarios elijan el tamaño de cada operación de inserción.
Las inserciones más grandes, es decir, con más filas, implican que se necesiten menos operaciones MOVE PARTITION. Sin embargo, esto debe equilibrarse con el coste de recuperación en caso de que falle una inserción, por ejemplo, debido a una interrupción de red. Puede complementar este proceso agrupando archivos en lotes para reducir el riesgo. Esto puede hacerse tanto con consultas de rango, por ejemplo, WHERE timestamp BETWEEN 2024-12-17 09:00:00 AND 2024-12-17 10:00:00, como con patrones glob. Por ejemplo,
ClickPipes utiliza este enfoque al cargar datos desde almacenamiento de objetos: crea automáticamente duplicados de la tabla de destino y de sus vistas materializadas, evitando que el usuario tenga que realizar los pasos anteriores. Además, al usar varios hilos de trabajo, cada uno encargado de distintos subconjuntos (mediante patrones glob) y con sus propias tablas duplicadas, los datos pueden cargarse rápidamente con semántica exactly-once. Si te interesa, puedes encontrar más detalles en este blog.
Escenario 1: Carga histórica de datos con ingestión de datos existente
- Identifique el punto de control: una marca de tiempo o un valor de columna a partir del cual es necesario restaurar los datos históricos.
- Cree duplicados de la tabla principal y de las tablas de destino de las vistas materializadas.
- Cree copias de las vistas materializadas que apunten a las tablas de destino creadas en el paso (2).
- Inserte los datos en la tabla principal duplicada creada en el paso (2).
- Mueva todas las particiones de las tablas duplicadas a sus versiones originales. Elimine las tablas duplicadas.
2024-12-17 09:00:00. Siguiendo el proceso anterior, creamos tablas y vistas duplicadas y cargamos el subconjunto mediante un filtro sobre el timestamp.
Filtrar por columnas timestamp en Parquet puede ser muy eficiente. ClickHouse solo leerá la columna timestamp para identificar los rangos completos de datos que deben cargarse, minimizando el tráfico de red. Los índices de Parquet, como min-max, también pueden ser aprovechados por el motor de consultas de ClickHouse.
Usa solo ClickPipes en ClickHouse CloudSi usas ClickHouse Cloud, debes usar ClickPipes para restaurar backups históricos si los datos pueden aislarse en su propio bucket (y no se requiere ningún filtro). Además de reducir el tiempo de carga al paralelizarla con varios workers, ClickPipes automatiza el proceso anterior y crea tablas duplicadas tanto para la tabla principal como para las vistas materializadas.
Escenario 2: Añadir vistas materializadas a tablas existentes
Evite
POPULATENo recomendamos usar el comando POPULATE para rellenar con datos históricos vistas materializadas, salvo en conjuntos de datos pequeños en los que la ingestión esté en pausa. Esta operación puede omitir filas insertadas en su tabla de origen, ya que la vista materializada se crea después de que finaliza POPULATE. Además, POPULATE se ejecuta sobre todos los datos y es vulnerable a interrupciones o a límites de memoria en conjuntos de datos grandes.Timestamp o columna monotónicamente creciente disponible
- Crear nuestra vista materializada con un filtro que solo considere filas posteriores a un instante arbitrario en un futuro próximo.
- Ejecutar una consulta
INSERT INTO SELECTque inserte en la tabla de destino de nuestra vista materializada, leyendo desde la tabla de origen mediante la consulta de agregación de la vista.
SELECT para incluir un filtro que solo tenga en cuenta las filas con una marca de tiempo posterior a una hora arbitraria en un futuro cercano; en este caso, asumimos que 2024-12-17 09:00:00 es dentro de unos minutos.
INSERT INTO SELECT. Por ejemplo, para la vista anterior:
En el ejemplo anterior, nuestra tabla de destino es una SummingMergeTree. En este caso, simplemente podemos usar nuestra consulta de agregación original. Para casos de uso más complejos que aprovechen AggregatingMergeTree, usarás funciones
-State para las agregaciones. Puedes encontrar un ejemplo de ello en esta guía de integración.pypi_downloads_per_day_v2, insertar en ella y adjuntar sus particiones resultantes a pypi_downloads_per_day.
A menudo, la consulta de una vista materializada puede ser más compleja (lo cual no es raro, ya que de lo contrario los usuarios no usarían una vista) y consumir recursos. En casos menos frecuentes, los recursos que requiere la consulta superan los del servidor. Esto pone de relieve una de las ventajas de las vistas materializadas de ClickHouse: son incrementales y no procesan todo el conjunto de datos de una sola vez.
En este caso, los usuarios tienen varias opciones:
- Modifique su consulta para rellenar con datos históricos los rangos; por ejemplo,
WHERE timestamp BETWEEN 2024-12-17 08:00:00 AND 2024-12-17 09:00:00,WHERE timestamp BETWEEN 2024-12-17 07:00:00 AND 2024-12-17 08:00:00, etc. - Use un motor de tabla Null para rellenar la vista materializada. Esto reproduce la población incremental típica de una vista materializada, ejecutando su consulta sobre bloques de datos (de tamaño configurable).
Uso de un motor de tabla Null para rellenar vistas materializadas
/dev/null del mundo de los motores de tabla). Aunque pueda parecer contradictorio, las vistas materializadas siguen ejecutándose sobre los datos insertados en este motor de tabla. Esto permite construir vistas materializadas sin persistir los datos originales, evitando la E/S y el almacenamiento asociado.
Es importante destacar que cualquier vista materializada adjunta al motor de tabla sigue ejecutándose sobre bloques de datos a medida que se insertan, enviando los resultados a una tabla de destino. Estos bloques tienen un tamaño configurable. Aunque los bloques más grandes pueden ser más eficientes (y procesarse más rápido), consumen más recursos (principalmente memoria). El uso de este motor de tabla significa que podemos crear nuestra vista materializada de forma incremental, es decir, un bloque cada vez, evitando así tener que mantener toda la agregación en memoria.
Considera el siguiente ejemplo:
pypi_v2, para recibir las filas que se usarán para construir nuestra vista materializada. Observe cómo limitamos el esquema únicamente a las columnas que necesitamos. Nuestra vista materializada realiza una agregación sobre las filas insertadas en esta tabla (un bloque cada vez), enviando los resultados a nuestra tabla de destino, pypi_downloads_per_day.
Aquí hemos usado
pypi_downloads_per_day como tabla de destino. Para una mayor resiliencia, los usuarios podrían crear una tabla duplicada, pypi_downloads_per_day_v2, y usarla como tabla de destino de la vista, como se muestra en ejemplos anteriores. Al completarse la inserción, las particiones de pypi_downloads_per_day_v2 podrían, a su vez, moverse a pypi_downloads_per_day. Esto permitiría recuperarse en caso de que la inserción falle por problemas de memoria o interrupciones del servidor; es decir, bastaría con truncar pypi_downloads_per_day_v2, ajustar la configuración y volver a intentarlo.pypi_v2 desde pypi los datos relevantes para rellenar con datos históricos.
639.47 MiB.
Ajuste de rendimiento y recursos
- Paralelismo de lectura - El número de hilos utilizados para leer. Se controla mediante
max_threads. En ClickHouse Cloud, esto lo determina el tamaño de la instancia y, de forma predeterminada, corresponde al número de vCPU. Aumentar este valor puede mejorar el rendimiento de lectura a costa de un mayor uso de memoria. - Paralelismo de inserción - El número de hilos de inserción utilizados para insertar datos. Se controla mediante
max_insert_threads. Nota: este valor está limitado pormax_threads, por lo que el paralelismo de inserción efectivo esmin(max_insert_threads, max_threads). En ClickHouse Cloud, esto viene determinado por el tamaño de la instancia (entre 2 y 4) y, en OSS, se establece en 1. Aumentar este valor puede mejorar el rendimiento a costa de un mayor uso de memoria. - Tamaño del bloque de inserción - los datos se procesan en un bucle en el que se extraen, se analizan y se agrupan en bloques de inserción en memoria según la clave de partición. Estos bloques se ordenan, optimizan, comprimen y se escriben en el almacenamiento como nuevas partes de datos. El tamaño del bloque de inserción, controlado por los ajustes
min_insert_block_size_rowsymin_insert_block_size_bytes(sin comprimir), afecta al uso de memoria y a la E/S de disco. Los bloques más grandes usan más memoria, pero crean menos partes, lo que reduce la E/S y las fusiones en segundo plano. Estos ajustes representan umbrales mínimos (el primero que se alcance desencadena un vaciado). - Tamaño de bloque de la vista materializada: además del mecanismo descrito anteriormente para la inserción principal, antes de insertarse en las vistas materializadas, los bloques también se compactan para que el procesamiento sea más eficiente. El tamaño de estos bloques lo determinan los ajustes
min_insert_block_size_bytes_for_materialized_viewsymin_insert_block_size_rows_for_materialized_views. Los bloques más grandes permiten un procesamiento más eficiente, a costa de un mayor uso de memoria. De forma predeterminada, estos ajustes toman, respectivamente, los valores de los ajustes de la tabla de origenmin_insert_block_size_rowsymin_insert_block_size_bytes.
Consejo para consultas
INSERT SELECT triviales: Para consultas sencillas como INSERT INTO t1 SELECT * FROM t2, sin transformaciones complejas, considere habilitar optimize_trivial_insert_select=1. Esta configuración (deshabilitada de forma predeterminada desde la versión 24.7) ajusta automáticamente el paralelismo de SELECT para que coincida con max_insert_threads, lo que reduce el uso de recursos y la cantidad de partes creadas. Esto resulta especialmente útil para migraciones masivas de datos entre tablas.min_insert_block_size_bytes_for_materialized_views ni min_insert_block_size_rows_for_materialized_views para mejorar el rendimiento. Si se modifican estos parámetros, aplique las mismas buenas prácticas descritas para min_insert_block_size_rows y min_insert_block_size_bytes.
Para minimizar el uso de memoria, puede experimentar con estas configuraciones. Esto reducirá inevitablemente el rendimiento. A continuación se muestran ejemplos usando la consulta anterior.
Reducir max_insert_threads a 1 disminuye la sobrecarga de memoria.
max_threads en 1.
min_insert_block_size_rows en 0 (lo desactiva como factor determinante del tamaño del bloque) y min_insert_block_size_bytes en 10485760 (10MiB).
Sin columna de marca de tiempo ni columna monotónicamente creciente
- Pause las inserciones en la tabla principal.
- Cree un duplicado de la tabla de destino principal con la sintaxis
CREATE AS. - Adjunte las particiones de la tabla de destino original al duplicado con
ALTER TABLE ATTACH. Nota: Esta operación de adjuntar es distinta del movimiento usado anteriormente. Aunque se basa en hard links, los datos de la tabla original se conservan. - Cree nuevas vistas materializadas.
- Reinicie las inserciones. Nota: Las inserciones solo actualizarán la tabla de destino, no el duplicado, que solo hará referencia a los datos originales.
- Haga el backfill de la vista materializada aplicando el mismo proceso usado anteriormente para datos con marcas de tiempo, usando la tabla duplicada como origen.
pypi_downloads_per_day (supondremos que no podemos usar la marca de tiempo):
pypi_downloads_per_day usando nuestro sencillo enfoque de INSERT INTO SELECT descrito antes. Esto también puede mejorarse usando el enfoque con la tabla Null documentado arriba, con el uso opcional de una tabla duplicada para aportar mayor resiliencia.
Aunque esta operación sí requiere pausar las inserciones, las operaciones intermedias normalmente pueden completarse con rapidez, lo que minimiza cualquier interrupción en los datos.