Snowflake frente a ClickHouse
Diferencias entre ambos motores en almacenamiento, cómputo y dialecto SQL, y modismos de Snowflake sin equivalente directo en ClickHouse.
Este documento sirve de referencia para los socios que migran de Snowflake a ClickHouse. Explica las diferencias arquitectónicas que condicionan las decisiones de diseño y las seis brechas entre dialectos SQL que encontrarás en la carga NYC Taxi.
1. Comparación arquitectónica
Almacenamiento
Snowflake toma por ti todas las decisiones de almacenamiento físico. Los datos se guardan como microparticiones columnares comprimidas en almacenamiento de objetos en la nube. Tú eliges el tamaño del warehouse y la estructura de las tablas; Snowflake se ocupa de todo lo demás: agrupación, compactación y gestión de archivos son automáticas.
ClickHouse exige tomar explícitamente las decisiones de almacenamiento físico. Al crear una tabla, especificas:
- El motor (determina cómo se almacenan, fusionan y deduplican los datos)
- El ORDER BY (se convierte en el orden físico y el índice primario)
- Opcionalmente: PARTITION BY, TTL y SETTINGS (códec de compresión, comportamiento de las fusiones)
Son decisiones de corrección, no simples ajustes de rendimiento. Un motor equivocado puede producir resultados incorrectos sin avisar. Un ORDER BY inadecuado puede hacer que una consulta que debería ser rápida recorra la tabla completa.
Ejecución de consultas
Snowflake usa MPP sin recursos compartidos mediante warehouses virtuales. Un warehouse es un clúster de nodos de cómputo que procesa consultas. Pagas mientras está activo, aunque esté inactivo consume créditos. La suspensión automática ayuda, pero el arranque en frío añade latencia.
ClickHouse emplea ejecución vectorizada. ClickHouse Cloud escala automáticamente cada servicio de cómputo por separado y lo reduce a cero cuando está inactivo. Varios servicios de cómputo pueden compartir el mismo almacenamiento (mediante SharedMergeTree): es el modelo de separación cómputo-cómputo de ClickHouse Cloud, donde cada servicio es una capa de cómputo independiente sobre una capa de datos común.
Modelo de concurrencia
Snowflake aísla las cargas creando warehouses separados. ETL usa TRANSFORM_WH y
los análisis, ANALYTICS_WH. Cada warehouse tiene cómputo dedicado; un trabajo ETL
lento no puede privar de recursos a una consulta analítica.
ClickHouse Cloud admite el mismo patrón mediante la separación cómputo-cómputo:
puedes aprovisionar varios servicios de cómputo que comparten almacenamiento. Cada uno
es una capa autoscalable independiente; ETL se ejecuta en un servicio y los análisis
interactivos en otro, sin competencia entre ellos. Dentro de un solo servicio, las
cargas se aíslan con cuotas flexibles (max_threads, priority y max_memory_usage
por usuario o consulta) y perfiles de usuario con límites de recursos. Para la mayoría
de las cargas analíticas, cuyas consultas terminan en milisegundos, basta un servicio y
las cuotas por consulta son la opción más ligera.
Modelo de costes
| Snowflake | ClickHouse Cloud | |
|---|---|---|
| Cómputo | Créditos (segundos de warehouse) | Unidades de cómputo (separadas del almacenamiento) |
| Almacenamiento | 23 USD/TB/mes | ~0,023 USD/GB/mes (más barato) |
| Reducción a cero | Solo suspensión automática | Reducción completa a cero |
| Transferencia de datos | Entrada gratuita; salida de pago | Tarifas estándar de salida de la nube |
La diferencia más importante: en Snowflake pagas por tiempo de warehouse haya o no consultas en ejecución. En ClickHouse Cloud, el cómputo baja a cero entre consultas. Para cargas analíticas intermitentes, ClickHouse Cloud suele costar entre 3 y 8 veces menos que una configuración equivalente de Snowflake.
2. Brechas entre dialectos SQL
La carga NYC Taxi contiene seis construcciones que deben traducirse. Todas aparecen en
Q1–Q7, dentro de 01-setup-snowflake/queries/.
Brecha 1: QUALIFY
QUALIFY es una extensión de Snowflake que filtra filas por el resultado de una función
de ventana, de forma similar a como HAVING filtra por el resultado de una agregación.
En esta migración lo tratamos como una brecha de dialecto y lo reescribimos mediante
una subconsulta: el patrón universal y portable que funciona en todos los motores SQL.
-- Snowflake
SELECT
trip_id,
pickup_at,
fare_amount,
ROW_NUMBER() OVER (PARTITION BY pickup_location_id ORDER BY fare_amount DESC) AS fare_rank
FROM fact_trips
WHERE pickup_at >= CURRENT_DATE - 7
QUALIFY fare_rank <= 10;
-- ClickHouse: wrap in a subquery
SELECT trip_id, pickup_at, fare_amount, fare_rank
FROM (
SELECT
trip_id,
pickup_at,
fare_amount,
ROW_NUMBER() OVER (PARTITION BY pickup_location_id ORDER BY fare_amount DESC) AS fare_rank
FROM analytics.fact_trips
WHERE pickup_at >= today() - 7
)
WHERE fare_rank <= 10;Por qué importa:
QUALIFYaparece en Q3. Reescribirlo con una subconsulta es el patrón seguro y portable: funciona sea cual sea el motor SQL de destino y hace explícito el resultado de la función de ventana. El peligro de cualquier sintaxis específica de Snowflake es asumir que se transfiere sin más; prueba siempre cada consulta antes de dar por terminada la migración.
Brecha 2: sintaxis de ruta con dos puntos de VARIANT
El tipo VARIANT de Snowflake usa una notación de ruta con dos puntos para acceder a
campos anidados: column:field.subfield::TYPE. ClickHouse almacena los datos
semiestructurados como String y los extrae en tiempo de consulta mediante funciones
JSONExtract*.
-- Snowflake
SELECT
trip_metadata:driver.rating::FLOAT AS driver_rating,
trip_metadata:app.version::STRING AS app_version,
trip_metadata:surge_multiplier::FLOAT AS surge
FROM trips_raw;
-- ClickHouse
SELECT
JSONExtractFloat(trip_metadata, 'driver', 'rating') AS driver_rating,
JSONExtractString(trip_metadata, 'app', 'version') AS app_version,
JSONExtractFloat(trip_metadata, 'surge_multiplier') AS surge
FROM default.trips_raw;La familia completa JSONExtract* incluye JSONExtractFloat, JSONExtractInt,
JSONExtractString, JSONExtractBool, JSONExtractKeys, JSONExtractArrayRaw y
JSONExtractRaw. Usa JSONExtractRaw cuando necesites obtener un objeto o array
anidado como cadena para seguir procesándolo.
¿Por qué no usar el tipo
JSONde ClickHouse? El tipoJSON(antes experimental) está disponible en versiones recientes, pero tiene una semántica distinta y todavía no está consolidado en producción para todos los casos. Para un laboratorio de migración,String+JSONExtract*es la opción segura y bien conocida.
Brecha 3: LATERAL FLATTEN
LATERAL FLATTEN de Snowflake desanida en filas un array contenido en una columna
VARIANT. ClickHouse no tiene un equivalente directo.
-- Snowflake: explode a VARIANT array into rows
SELECT t.trip_id, f.value:stop_name::STRING AS stop_name
FROM trips_raw t,
LATERAL FLATTEN(input => t.trip_metadata:route_stops) f;
-- ClickHouse Option 1: JSONExtract into Array, then arrayJoin
SELECT
trip_id,
arrayJoin(JSONExtract(trip_metadata, 'route_stops', 'Array(String)')) AS stop_name
FROM default.trips_raw;
-- ClickHouse Option 2: Pre-flatten the column during dbt staging
-- In stg_trips.sql, extract all array elements to separate columns
-- or use the dbt model to reshape the data at load timeEl aplanamiento previo (Opción 2) es preferible cuando el array tiene un esquema
acotado y conocido. arrayJoin (Opción 1) es mejor para consultas ad hoc o cuando la
longitud del array es variable.
Brecha 4: MERGE INTO
MERGE INTO de Snowflake es el mecanismo principal de upsert. ClickHouse no dispone de
una sentencia MERGE. El equivalente correcto depende del motor de la tabla.
-- Snowflake
MERGE INTO fact_trips t
USING staging_trips s ON t.trip_id = s.trip_id
WHEN MATCHED THEN UPDATE SET t.fare_amount = s.fare_amount, t.updated_at = s.updated_at
WHEN NOT MATCHED THEN INSERT VALUES (s.trip_id, s.pickup_at, ...);
-- ClickHouse with ReplacingMergeTree: just INSERT
-- RMT deduplicates by the ORDER BY key during background merges.
-- Use FINAL at query time to get the latest version:
INSERT INTO analytics.fact_trips SELECT * FROM staging_trips;
SELECT * FROM analytics.fact_trips FINAL WHERE trip_id = '...';
-- ClickHouse with dbt delete_insert incremental:
-- dbt handles the upsert by: DELETE WHERE key IN (new batch), then INSERT
-- This is the recommended approach for the analytics layerLa estrategia incremental delete_insert de dbt-clickhouse es el equivalente semántico
más cercano a MERGE INTO para modelos analíticos. Elimina las filas existentes que
coinciden con alguna clave del lote entrante y después inserta todas las filas del lote,
de forma atómica por partición.
Trampa clave de ReplacingMergeTree: la deduplicación en segundo plano es asíncrona. Entre fusiones, las versiones antigua y nueva de una fila coexisten en la tabla. Usa siempre
FINALen las consultas que deban devolver exactamente una fila por clave. Consulta Motores MergeTree para conocer toda la semántica de deduplicación.
Brecha 5: Streams de Snowflake (CDC)
Los Streams de Snowflake registran cambios por fila (INSERT, UPDATE y DELETE) en una
tabla. Exponen las columnas de sistema METADATA$ACTION, METADATA$ISUPDATE y
METADATA$ROW_ID. ClickHouse no tiene un mecanismo interno equivalente.
Equivalente en ClickHouse: cambio directo del productor
ClickHouse no posee un mecanismo CDC interno equivalente a los Streams de Snowflake. En esta migración, el patrón es más sencillo que usar un conector CDC:
- Primero, la carga masiva:
scripts/02_migrate_trips.pylee por lotes todas las filas históricas de Snowflake y las inserta en ClickHouse - Después, el cambio del productor:
scripts/03_cutover.shdetiene el productor de Snowflake e inicia uno de ClickHouse que escribe directamente en ClickHouse Cloud - No se necesita una ventana CDC: el script de migración se ocupa de la carga histórica
y el productor asume las escrituras en vivo;
ReplacingMergeTree(_synced_at)entrips_rawhace idempotentes los reintentos de la migración o del productor
Tras el cambio, la estrategia dbt delete_insert gestiona los upserts de la capa
analítica. Los Streams y Tasks de Snowflake se retiran por completo.
Brecha 6: funciones de fecha y hora
Snowflake y ClickHouse usan nombres distintos para las funciones de fecha. La mayoría son sustituciones mecánicas.
| Snowflake | ClickHouse | Notas |
|---|---|---|
DATE_TRUNC('hour', ts) | toStartOfHour(ts) | También: toStartOfDay, toStartOfMonth, toStartOfWeek |
DATE_TRUNC('day', ts) | toDate(ts) | |
DATEADD('day', n, ts) | ts + INTERVAL n DAY | O addDays(ts, n) |
DATEDIFF('minute', t1, t2) | dateDiff('minute', t1, t2) | Nombre de función en minúsculas |
CURRENT_DATE | today() | |
CURRENT_TIMESTAMP() | now() | |
TO_TIMESTAMP(epoch, 9) | fromUnixTimestamp64Nano(epoch) | Unidades explícitas en CH |
YEAR(ts) | toYear(ts) | |
MONTH(ts) | toMonth(ts) | |
EXTRACT(epoch FROM ts) | toUnixTimestamp(ts) |
DateTime frente a DateTime64:
DateTimede ClickHouse tiene precisión de segundos. UsaDateTime64(3, 'UTC')para precisión de milisegundos (equivalente alTIMESTAMP_NTZde Snowflake). El3es la escala de subsegundos y'UTC', la zona horaria.
3. Opciones de movimiento de datos
| Método | Cuándo usarlo | Notas |
|---|---|---|
Script de migración de Python (scripts/02_migrate_trips.py) | Carga masiva de Snowflake → ClickHouse | Conexión directa mediante snowflake-connector-python + clickhouse-connect; reanudable; no requiere servicios adicionales; se usa en este laboratorio |
| ClickPipes | Kafka, S3, Kinesis, CDC de PostgreSQL, CDC de MySQL | Conector administrado; no admite Snowflake como origen |
remoteSecure() | Extracción ad hoc desde otro servicio ClickHouse | No se aplica a un origen Snowflake |
| Transferencia mediante almacenamiento de objetos | Cargas grandes de una sola vez | Exportación Snowflake → S3 → función de tabla S3 de ClickHouse; requiere una cuenta AWS y configurar IAM |
| JDBC/ODBC | Canalizaciones ETL personalizadas | Flexible, pero exige orquestación personalizada |
Para este laboratorio, el script de migración de Python es la opción correcta: no
requiere servicios adicionales en la nube (ni S3 ni Kafka), se puede depurar por
completo y usa paquetes (snowflake-connector-python, clickhouse-connect) que los
socios ya tienen instalados para otros pasos.
4. Comparación de arquitecturas CDC
| Streams + Tasks de Snowflake | ClickHouse (este laboratorio) | |
|---|---|---|
| Seguimiento de cambios | Objeto stream interno en la tabla (TRIPS_CDC_STREAM) | Sin equivalente: el productor escribe directamente en ClickHouse tras el cambio |
| Eventos de cambio | METADATA$ACTION: INSERT/UPDATE/DELETE | INSERT directo del productor de ClickHouse |
| Latencia | Programación configurable de tareas (mín. 1 min) | Intervalo de lote configurable (10 s de forma predeterminada) |
| Consumo | Una tarea SQL lee el stream y emite al destino | Productor de Python (producer/producer.py) |
| Cambios de esquema | Coordinación manual | El código del productor controla el esquema |
Tras la migración, el productor escribe directamente en ClickHouse: no se necesitan
Streams ni Tasks. La estrategia dbt delete_insert gestiona los upserts de la capa
analítica. La agregación periódica (Tasks de Snowflake) tiene un sustituto nativo de
ClickHouse en las vistas materializadas actualizables. El proyecto dbt del laboratorio
incluye una, analytics.mv_live_trip_feed, aunque el laboratorio no activa su intervalo
de actualización (consulta el módulo 05).
5. Análisis detallado del modelo de costes
Snowflake: basado en créditos
Un crédito de Snowflake cuesta unos 3 USD (Enterprise). Coste = tamaño_del_warehouse × tiempo_activo. Un warehouse SMALL consume un crédito por hora; uno MEDIUM consume 2. Con una suspensión automática mínima de 60 segundos, incluso una sola consulta cuesta al menos 1/60 de hora.
Para el laboratorio NYC Taxi (warehouse X-Small, 1 crédito/h):
- Configuración de la Parte 1: ~2–4 créditos (~6–12 USD)
- Por cada sesión continua de 8 h: ~4–8 créditos/día (~12–24 USD)
- El monitor de recursos de ANALYTICS_WH limita el consumo a 50 créditos/mes (~150 USD)
ClickHouse Cloud: cómputo y almacenamiento separados
ClickHouse Cloud factura por separado cómputo y almacenamiento:
- Cómputo: la capa Development cuesta unos 0,10 USD/h cuando está activa y baja a cero en reposo
- Almacenamiento: ~0,023 USD/GB/mes (considerablemente menos que los 23 USD/TB de Snowflake)
- ClickPipes: incluido en la suscripción Cloud para los orígenes compatibles (Kafka, S3, Kinesis, CDC de PostgreSQL y CDC de MySQL; no Snowflake)
Para el laboratorio NYC Taxi:
- 50 millones de filas × ~300 bytes/fila sin comprimir = ~15 GB → ~8 GB comprimidos en ClickHouse
- Coste de almacenamiento: ~0,18 USD/mes
- Cómputo durante la Parte 3 activa (~2 h): ~0,20–0,40 USD
Coste total de la Parte 3: ~2–4 USD, frente a los ~6–12 USD de Snowflake para la misma sesión.
La diferencia de costes explica por qué muchas organizaciones empiezan con Snowflake (operación más sencilla) y migran a ClickHouse (menor coste y mayor rendimiento) a medida que crece su carga analítica.