Snowflake MigrationClickHouse Workshops

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

SnowflakeClickHouse Cloud
CómputoCréditos (segundos de warehouse)Unidades de cómputo (separadas del almacenamiento)
Almacenamiento23 USD/TB/mes~0,023 USD/GB/mes (más barato)
Reducción a ceroSolo suspensión automáticaReducción completa a cero
Transferencia de datosEntrada gratuita; salida de pagoTarifas 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: QUALIFY aparece 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 JSON de ClickHouse? El tipo JSON (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 time

El 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 layer

La 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 FINAL en 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.py lee por lotes todas las filas históricas de Snowflake y las inserta en ClickHouse
  • Después, el cambio del productor: scripts/03_cutover.sh detiene 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) en trips_raw hace 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.

SnowflakeClickHouseNotas
DATE_TRUNC('hour', ts)toStartOfHour(ts)También: toStartOfDay, toStartOfMonth, toStartOfWeek
DATE_TRUNC('day', ts)toDate(ts)
DATEADD('day', n, ts)ts + INTERVAL n DAYO addDays(ts, n)
DATEDIFF('minute', t1, t2)dateDiff('minute', t1, t2)Nombre de función en minúsculas
CURRENT_DATEtoday()
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: DateTime de ClickHouse tiene precisión de segundos. Usa DateTime64(3, 'UTC') para precisión de milisegundos (equivalente al TIMESTAMP_NTZ de Snowflake). El 3 es la escala de subsegundos y 'UTC', la zona horaria.


3. Opciones de movimiento de datos

MétodoCuándo usarloNotas
Script de migración de Python (scripts/02_migrate_trips.py)Carga masiva de Snowflake → ClickHouseConexión directa mediante snowflake-connector-python + clickhouse-connect; reanudable; no requiere servicios adicionales; se usa en este laboratorio
ClickPipesKafka, S3, Kinesis, CDC de PostgreSQL, CDC de MySQLConector administrado; no admite Snowflake como origen
remoteSecure()Extracción ad hoc desde otro servicio ClickHouseNo se aplica a un origen Snowflake
Transferencia mediante almacenamiento de objetosCargas grandes de una sola vezExportación Snowflake → S3 → función de tabla S3 de ClickHouse; requiere una cuenta AWS y configurar IAM
JDBC/ODBCCanalizaciones ETL personalizadasFlexible, 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 SnowflakeClickHouse (este laboratorio)
Seguimiento de cambiosObjeto stream interno en la tabla (TRIPS_CDC_STREAM)Sin equivalente: el productor escribe directamente en ClickHouse tras el cambio
Eventos de cambioMETADATA$ACTION: INSERT/UPDATE/DELETEINSERT directo del productor de ClickHouse
LatenciaProgramación configurable de tareas (mín. 1 min)Intervalo de lote configurable (10 s de forma predeterminada)
ConsumoUna tarea SQL lee el stream y emite al destinoProductor de Python (producer/producer.py)
Cambios de esquemaCoordinación manualEl 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.

En esta página

ES