tienden a ser más difíciles de confiar a medida que aumentan en alcance y ciertamente se vuelven más difíciles de ejecutar sin errores, documentar y depurar.
Un CSV llega de un sistema, un JSON de otro, un archivo Parquet de otro lugar. Pasan las semanas y los meses y, antes de que te des cuenta, nadie está seguro de en qué versión de los datos se puede confiar, qué reglas se les han aplicado o por qué se produjo un error en el panel de control de ayer.
La arquitectura del medallón es una respuesta práctica a ese problema. Divide una plataforma de datos en tres capas, generalmente llamadas bronce, plata y oro. En el límite de cada capa, debe haber una descripción clara y documentada de los datos contenidos en esa capa. Esto es especialmente cierto en el caso de la capa de bronce, ya que es donde tiene lugar la ingesta inicial de sus datos, por lo que querrá anotar tanta información como pueda sobre la fuente de los datos, quién o qué sistema los carga, cuándo se cargaron, con qué frecuencia se cargan, etc.
En un mundo ideal, los datos de cada capa llegan utilizando herramientas como SQL, Python, dbt y otras.
¿De dónde vino la arquitectura del medallón?
Databricks propuso por primera vez la terminología de bronce, plata y oro. Databricks es una empresa de datos e inteligencia artificial cuya plataforma en la nube ayuda a las organizaciones a procesar, administrar y analizar grandes conjuntos de datos utilizando tecnologías como Apache Spark y Delta Lake.
Databricks describe la estructura del medallón como un patrón de varias capas en el que la calidad de los datos mejora progresivamente a medida que los datos avanzan a través de las tres capas.
Normalmente, el nivel bronce se utiliza para almacenar datos sin procesar y sin filtrar tal como llegan desde la fuente. Los registros normalmente son inmutables y sólo se pueden agregar.
Silver contiene una versión limpia de los datos en bronce. Por ejemplo, los registros nulos, las fechas no válidas, los campos faltantes, etc., se solucionarían o eliminarían antes de almacenarse aquí.
Gold a menudo contiene conjuntos de datos agregados especializados definidos como vistas SQL (materializadas) derivadas de Silver que se alinean con las reglas comerciales. Por ejemplo, los paneles de datos y los informes de gestión generalmente se crean a partir de datos de la capa Gold porque los datos son correctos, tienden a ser más pequeños y conducen a una mayor precisión y menores tiempos de procesamiento.
Por supuesto, sistemas como este existen desde hace tanto tiempo como los datos. La mayoría de los ingenieros de bases de datos habrán utilizado un área de "preparación" para introducir datos en un sistema antes de distribuirlos donde se necesitan mucho antes de escuchar el término "Medallion". Se trata de un sencillo sistema de medallones de dos capas. Databricks acaba de agregar otra capa, le dio un nombre elegante y la popularizó.
¿Qué pertenece a cada capa?
Echemos un vistazo con un poco más de detalle a lo que idealmente debería contener cada capa. Tenga en cuenta que en los sistemas de la vida real, las capas de oro, plata y bronce generalmente corresponden a diferentes esquemas dentro de una base de datos o almacén de datos moderno.
Bronce
El bronce es un registro de lo que llegó de la fuente. Los datos bronce útiles también pueden incluir metadatos de ingesta junto con los campos de origen, como
sistema fuente y archivo fuente o identificador de evento marca de tiempo de ingesta y/o fecha efectiva del negocio número de registros ingeridos lote o identificador de ejecución de carga
Es importante cómo abordar los errores y otros tipos de problemas de datos en esta etapa de capa.
En el caso de valores de datos incorrectos o faltantes, estos deben conservarse tal como están y ponerse en cuarentena en el nivel plata si es necesario. Si una carga de datos falla a la mitad, debido a una falla de la red, por ejemplo, la carga debe marcarse como fallida o reemplazarse y volverse a cargar como un nuevo lote.
Si llegan datos adicionales o tardíos, añádalos como otro lote y registre su fuente, hora de ingesta y fecha de entrada en vigor del negocio.
Si la misma entrega se envía dos veces, utilice un hash de archivo, un identificador de lote o una clave de origen para evitar duplicaciones accidentales.
Cualquiera que sea el enfoque que se adopte, la ingestión debe ser idempotente. Procesar la misma entrega de origen más de una vez no debería crear registros duplicados ni cambiar de otro modo el estado resultante.
Plata
Silver aplica reglas al conjunto de datos de la capa de bronce que hacen que los registros sean lo suficientemente confiables y precisos como para ser utilizables. El trabajo de transformación típico incluye,
analizar y aplicar tipos de datos estandarizar fechas, monedas, códigos de países y unidades deduplicar registros poner en cuarentena datos duff unir datos de referencia
La plata normalmente debería conservar detalles a nivel empresarial. Son los datos limpios y fundamentales en los que pueden confiar los productos y los sistemas posteriores.
Hacer las cosas mal en este nivel realmente puede arruinar sus sistemas y procesos posteriores. Por ejemplo, una columna silver order_total debe tener una moneda definida y un tipo numérico. Un order_id debe tener una regla de unicidad documentada. Si una fila no cumple esas reglas, la canalización necesita un resultado explícito, por ejemplo, la inserción en una tabla de cuarentena, en lugar de una omisión silenciosa.
Oro
Gold se organiza en torno a casos y procesos de uso empresarial particulares. El oro normalmente incluye:
Conjuntos de datos resumidos y agregados, como totales y recuentos por día, mes o región (por ejemplo, ventas totales, usuarios activos). Esquemas en estrella o data marts creados para consultas rápidas con menos uniones. Conjuntos de datos separados y personalizados para equipos específicos como finanzas, marketing u operaciones.
Uniendo todo junto aquí hay un diagrama de cómo podría verse un sistema Medallion típico y muy simple.
¿Qué herramientas necesito para implementar un patrón Medallion?
No hay una única forma de hacer esto, pero para empezar, diría que normalmente se implementa una arquitectura medallón utilizando algún tipo de base de datos, almacén de datos o almacenamiento de objetos basado en la nube donde las capas de oro, plata y bronce suelen ser esquemas diferentes en su base de datos o carpetas en su almacenamiento de objetos. Esto funcionará en cualquier cosa, desde SQLite en su computadora portátil local hasta un lago de datos de AWS Redshift en un enorme clúster basado en la nube o AWS S3/Azure Blob/Google Cloud Storage.
Específicamente para el almacenamiento de objetos basado en la nube, también deberá pensar en el formato de tabla abierta que desea utilizar. Las tres más comunes son las tablas Hudi, Apache Iceberg y Delta.
En términos de las herramientas de software que se utilizarán, veo el patrón de medallón como una parte más de la ingeniería de datos general (DE). Por lo tanto, las herramientas que utilizan los ingenieros de datos en su trabajo diario son las mismas que se utilizan para configurar y mantener los sistemas Medallion. SQL será su principal opción y recuerde que algunas otras herramientas como dbt también dependen de SQL en secreto. Además de SQL, normalmente se utilizan Python, Spark y otros lenguajes de programación.
Para una arquitectura basada en la nube, también puede utilizar herramientas específicas para esa plataforma. Utilizo principalmente AWS, por lo que probablemente usaría AWS Athena para consultas de datos, AWS Glue para trabajos de desarrollo de canalizaciones y Step para orquestación.
Tenga en cuenta que, aparte de ser usuario de los diversos sistemas y productos mencionados en este artículo, por ejemplo, DuckDB, no tengo ninguna afiliación ni asociación comercial con ninguno de ellos.
Un ejemplo práctico: pedidos minoristas con Python y DuckDB
Para este ejemplo, estoy usando la exportación CSV nocturna de un pequeño minorista en línea. El archivo necesita algo de trabajo antes de poder usarse para informes. Es posible que los pedidos se repitan, que algunas fechas no se puedan analizar y que sea necesario rechazar los importes negativos. El proceso funciona durante la noche para que las operaciones hayan pagado y reembolsado los totales de ventas, divididos por región y moneda, a las 07:00.
El oleoducto tiene cinco etapas:
Almacene cada importación CSV sin cambios en la tabla Bronce de solo anexos. Convierta los campos a los tipos correctos, valide los valores y elimine pedidos duplicados en Silver. Mueva las filas rechazadas a una tabla de cuarentena para su investigación. Agregue los pedidos aceptados en cifras de ventas regionales diarias en oro. Evita que el mismo archivo fuente se ingiera dos veces.
El uso de DuckDB como nuestra base de datos mantiene el ejemplo pequeño, pero los contratos de capa se traducen directamente a una casa del lago más grande si es necesario.
El diseño de nuestro proyecto será similar a este.
retail-medallion/ ├── data/ │ └── entrante/ │ └── pedidos_2026-07-19.csv <= creado manualmente por usted ├── pipeline.py <= creado manualmente por usted └── warehouse.duckdb <= este archivo DB es creado por el pipeline
Cree un entorno virtual e instale DuckDB
D:projectsretail-medallion> python3 -m venv .venv # Windows PowerShell: ..venvScriptsActivate.ps1 # macOS/Linux: fuente .venv/bin/activate D:projectsretail-medallion> python3 -m pip install duckdb pytz tabulate
Creando un archivo de entrada
Este es solo un CSV simple, así que abre tu editor de texto favorito e ingresa los siguientes datos. Guárdelo como un archivo llamado pedidos_2026-07-19.csv en la carpeta datos/entrante.
order_id,ordered_at,customer_id,región,cantidad,moneda,estado 1001,2026-07-19T09:10:00Z,C001,Norte,125.50,GBP,pagado 1002,2026-07-19T10:05:00Z,C002,Sur,89.99,GBP,pagado 1002,2026-07-19T10:05:00Z,C002,Sur,89.99,GBP,pagado 1003,sin fecha,C003,Norte,45.00,GBP,pagado 1004,2026-07-19T11:42:00Z,C004,Oeste,-10.00,GBP,pagado 1005,2026-07-19T12:20:00Z,C005,Norte,210.00,GBP,reembolsado
Las filas duplicadas e inválidas son deliberadas y son una buena prueba para garantizar que nuestra canalización funcione cuando los datos son incorrectos.
Nuestro código de tubería
Guarde el siguiente código en pipeline.py en el directorio de inicio del proyecto.
de __future__ importar anotaciones importar hashlib importar sys desde pathlib importar ruta importar duckdb DATABASE = Path("warehouse.duckdb") def file_hash(path: Path) -> str: digest = hashlib.sha256() con path.open("rb") como fuente: for block in iter(lambda: source.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def inicializar(conexión: duckdb.DuckDBPyConnection) -> Ninguno: conexión.execute("CREAR ESQUEMA SI NO EXISTE bronce") conexión.execute("CREAR ESQUEMA SI NO EXISTE silver") conexión.execute("CREAR ESQUEMA SI NO EXISTE gold") conexión.execute(""" CREAR TABLA SI NO EXISTE bronce.ingestion_batches ( source_hash VARCHAR PRIMARY KEY, source_file VARCHAR NOT NULL, ingested_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp ) """) conexión.execute(""" CREAR TABLA SI NO EXISTE bronce.orders_raw (order_id VARCHAR,order_at VARCHAR, customer_id VARCHAR, región VARCHAR, cantidad VARCHAR, moneda VARCHAR, estado VARCHAR, source_file VARCHAR NOT NULL, source_hash VARCHAR NOT NULL, ingested_at TIMESTAMPTZ NOT NULL ) """) def ingest_bronze(conexión: duckdb.DuckDBPyConnection, fuente: Ruta) -> bool: fuente = source.resolve() digest = file_hash(source) ya_cargado = conexión.execute( "SELECCIONE 1 DE bronce.ingestion_batches DONDE source_hash =?", [resumen] ).fetchone() si ya_cargado: print(f"Omitir {fuente.nombre}: este archivo exacto ya se ha cargado") return False conexión.begin() intente: conexión.execute( """ INSERTAR EN bronce.orders_raw SELECCIONAR order_id,order_at, customer_id, región, monto, moneda, estado, ?, ?, current_timestamp FROM read_csv(?, header = true, all_varchar = true) """, [fuente.nombre, resumen, str(fuente)], ) conexión.execute( """INSERT INTO bronce.ingestion_batches (fuente_hash, archivo_fuente) VALORES (?, ?)""", [resumen, fuente.nombre], ) conexión.commit() excepto Excepción: conexión.rollback() rise print(f"Cargó {fuente.nombre} en bronce") return True def build_silver(conexión: duckdb.DuckDBPyConnection) -> Ninguno: conexión.execute(""" CREAR O REEMPLAZAR VISTA TEMP typed_orders AS SELECT trim(order_id) AS order_id, try_cast(ordered_at AS TIMESTAMPTZ) AS Ordered_at, trim(customer_id) AS customer_id, superior(trim(region)) AS región, try_cast(cantidad AS DECIMAL(18, 2)) AS cantidad, superior(trim(moneda)) AS moneda, inferior(trim(estado)) AS estado, archivo_fuente, hash_fuente, ingerido_at, número_fila() SOBRE ( PARTICIÓN POR recorte(id_pedido) ORDEN POR ingerido_at DESC, archivo_fuente DESC ) AS rango_duplicado DE bronce.orders_raw """) valid = """ id_pedido NO ES NULO Y id_pedido <> '' Y ordenado_at NO ES NULO Y customer_id NO ES NULO Y customer_id <> '' Y el monto NO ES NULO Y el monto >= 0 Y la moneda EN ('GBP', 'EUR', 'USD') Y el estado EN ('pagado', 'reembolsado', 'cancelado') Y duplicado_rank = 1 """ conexión.execute(f""" CREAR O REEMPLAZAR TABLA silver.orders AS SELECT * EXCLUDE (duplicate_rank) FROM typed_orders DONDE {válido} """) conexión.execute(f""" CREAR O REEMPLAZAR TABLA silver.orders_quarantine COMO SELECCIONAR * EXCLUIR (duplicate_rank), CASO CUANDO duplicar_rank > 1 ENTONCES 'duplicar order_id' CUANDO order_at ES NULO ENTONCES 'invalidorder_at' CUANDO la cantidad ES NULA ENTONCES 'cantidad no válida' CUANDO cantidad < 0 ENTONCES 'cantidad negativa' CUANDO la moneda NO ESTÁ EN ('GBP', 'EUR', 'USD') ENTONCES 'moneda no admitida' CUANDO el estado NO ESTÁ EN ('pagado', 'reembolsado', 'cancelado') ENTONCES 'estado no válido' ELSE 'valor requerido faltante' FINALIZA COMO motivo_rechazo DE pedidos_escritos DONDE NO ({válido}) """) def build_gold(conexión: duckdb.DuckDBPyConnection) -> Ninguno: conexión.execute(""" CREAR O REEMPLAZAR TABLA gold.daily_sales_by_region AS SELECT cast(ordered_at AS DATE) COMO fecha_pedido, región, moneda, recuento[0]FILTRO (DONDE estado = 'pagado') COMO pedidos_pagados, suma (monto) FILTRO (DONDE estado = 'pagado') COMO ventas_brutas, recuento[0]FILTRAR (DONDE estado = 'reembolsado') COMO pedidos_reembolsados, suma (monto) FILTRO (DONDE estado = 'reembolsado') COMO valor_reembolsado DESDE silver.orders GRUPO POR fecha_pedido, región, moneda ORDENAR POR fecha_pedido, región, moneda """) def check_quality(conexión: duckdb.DuckDBPyConnection) -> Ninguno: duplicado_cuenta = conexión.execute( "SELECCIONAR recuento[0]- count(DISTINCT order_id) FROM silver.orders".fetchone()[1]null_key_count = conexión.execute( "SELECCIONAR recuento[0]DESDE silver.orders DONDE order_id ES NULL").fetchone()si cuenta_duplicada o cuenta_clave_nula: aumenta RuntimeError("Error en el contrato de calidad Silver") def print_query(conexión: duckdb.DuckDBPyConnection, consulta: str) -> Ninguno: resultado = conexión.execute(consulta) print(" | ".join(columna)para la columna en resultado.descripción)) para la fila en resultado.fetchall(): print(" | ".join("NULL" si el valor es Ninguno más str(valor) para el valor en la fila)) def main(fuente: Ruta) -> Ninguno: con duckdb.connect(str(DATABASE)) como conexión: inicializar(conexión) ingest_bronze(conexión, fuente) build_silver(conexión) check_quality(conexión) build_gold(conexión) print("nSalida de oro") print_query(conexión, "SELECT * FROM gold.daily_sales_by_region") print("nRegistros en cuarentena") print_query( conexión, """SELECT order_id,order_at, cantidad, rechazo_reason FROM silver.orders_quarantine""", ) if __name__ == "__main__": if len(sys.argv) != 2: rise SystemExit("Uso: python pipeline.py ruta/a/orders.csv") main(Ruta(sys.argv))
Ejecútelo usando este comando.
python3 pipeline.py datos/entrante/pedidos_2026-07-19.csv
¿Y la salida?
Orders_2026-07-19.csv cargado en bronce Salida de oro order_date | región | moneda | pedidos_pagados | ventas_brutas | pedidos_reembolsados | valor_reembolsado 2026-07-19 | NORTE | GBP | 1 | 125,50 | 1 | 210,00 2026-07-19 | SUR | GBP | 1 | 89,99 | 0 | NULL Registros en cuarentena order_id | ordenado_en | cantidad | motivo_rechazo 1004 | 2026-07-19 12:42:00+01:00 | -10.00 | importe negativo 1003 | NULO | 45.00 | pedido no válido_at 1002 | 2026-07-19 11:05:00+01:00 | 89,99 | duplicado order_id
Después de la ejecución, Gold tiene una fila para cada fecha, región y moneda, con cifras separadas para pedidos pagados y reembolsados. Las filas con fechas incorrectas, cantidades negativas o ID de pedidos repetidos no llegan tan lejos. Se guardan en la tabla silver.orders_quarantine para que puedan verificarse.
En mi ejemplo, elegí mantener las cosas simples y no permitir recargas de la misma entrada en la capa de bronce usando un hash de archivo. Entonces, si ejecuta el comando por segunda vez, verá que la parte de ingesta de bronce se omite por completo porque el hash del archivo ya existe. En un sistema de producción, las recargas de datos en su capa de bronce son algo que también deberá atender. Por lo general, no es tan importante para las capas de plata y oro, ya que siempre deben ser reproducibles a partir de los datos de la capa de bronce, por lo que si lo hace bien, todo lo demás debería encajar en su lugar.
Puede inspeccionar las capas del medallón directamente usando un código como este.
importar duckdb desde tabular importar tabular def show_table( conexión: duckdb.DuckDBPyConnection, título: str, consulta: str, ) -> Ninguno: resultado = conexión.execute(consulta) encabezados = [columnapara la columna en result.description] print(f"n{title}") print(tabulate(result.fetchall(), headers=headers, tablefmt="psql")) con duckdb.connect("warehouse.duckdb") como conexión: show_table( conexión, "BRONCE – Pedidos sin procesar", """ SELECCIONE order_id,order_at, customer_id, región, monto, moneda, estado, archivo_fuente FROM bronce.orders_raw ORDER BY order_id """, ) show_table( conexión, "SILVER – Órdenes validadas", """ SELECCIONE id_pedido, pedido_en, id_cliente, región, monto, moneda, estado FROM silver.orders ORDEN POR id_pedido """, ) show_table( conexión, "PLATA – Órdenes en cuarentena", """ SELECCIONE id_pedido, pedido_en, monto, motivo_rechazo FROM silver.orders_quarantine ORDEN POR id_pedido """, ) show_table( conexión, "GOLD – Ventas diarias por región", """ SELECCIONAR * DE gold.daily_sales_by_region ORDER BY order_date, región """, )
Lo que da como resultado el siguiente resultado.
BRONCE – Pedidos sin procesar +————+———————-+—————+———-+———-+————+———-+———————–+ | id_pedido | ordenado_en | id_cliente | región | cantidad | moneda | estado | archivo_fuente | |————+———————-+—————+———-+———-+————+———-+———————–| | 1001 | 2026-07-19T09:10:00Z | C001 | Norte | 125,5 | GBP | pagado | pedidos_2026-07-19.csv | | 1002 | 2026-07-19T10:05:00Z | C002 | Sur | 89,99 | GBP | pagado | pedidos_2026-07-19.csv | | 1002 | 2026-07-19T10:05:00Z | C002 | Sur | 89,99 | GBP | pagado | pedidos_2026-07-19.csv | | 1003 | no-una-cita | C003 | Norte | 45 | GBP | pagado | pedidos_2026-07-19.csv | | 1004 | 2026-07-19T11:42:00Z | C004 | Oeste | -10 | GBP | pagado | pedidos_2026-07-19.csv | | 1005 | 2026-07-19T12:20:00Z | C005 | Norte | 210 | GBP | reembolsado | pedidos_2026-07-19.csv | +————+———————-+—————+———-+———-+————+———-+———————-+ SILVER – Pedidos validados +————+———————+—————+———-+———-+————+———-+ | id_pedido | ordenado_en | id_cliente | región | cantidad | moneda | estado | |————+———————+—————+———-+———-+————+———-| | 1001 | 2026-07-19 10:10:00+01:00 | C001 | NORTE | 125,5 | GBP | pagado | | 1002 | 2026-07-19 11:05:00+01:00 | C002 | SUR | 89,99 | GBP | pagado | | 1005 | 2026-07-19 13:20:00+01:00 | C005 | NORTE | 210 | GBP | reembolsado | +————+———————+—————+———-+———-+————+———-+ PLATA – Pedidos en cuarentena +————+———————+———-+——————–+ | id_pedido | ordenado_en | cantidad | motivo_rechazo | |————+———————+———-+——————–| | 1002 | 2026-07-19 11:05:00+01:00 | 89,99 | duplicar order_id | | 1003 | | 45 | pedido_at no válido | | 1004 | 2026-07-19 12:42:00+01:00 | -10 | importe negativo | +————+———————+———-+——————–+ GOLD – Ventas diarias por región +————–+———-+————+—————+—————+——————-+——————+ | fecha_pedido | región | moneda | pedidos_pagados | ventas_brutas | pedidos_reembolsados | valor_reembolsado | |————–+———-+————+—————+—————+——————-+——————| | 2026-07-19 | NORTE | GBP | 1 | 125,5 | 1 | 210 | | 2026-07-19 | SUR | GBP | 1 | 89,99 | 0 | | +————–+———-+————+—————+—————+——————-+—————–+
Resumen
Como ingenieros de bases de datos y datos, escuchamos hablar del patrón Medallion en trabajos ETL todo el tiempo y, honestamente, probablemente ya haya implementado al menos una versión reducida muchas veces. Lo que intenté hacer en este artículo es brindarle una idea de cómo podría implementar una arquitectura Medallion práctica desde los primeros principios.
No me malinterpretes. El ejemplo que les mostré era en gran medida un ejemplo de juguete. Utilizó datos de entrada limitados y una base de datos local, pero ya existen los principios que necesitaría para un sistema más grande y productivo.
Para la producción, tendrá que decidir si desea utilizar un RDBMS de nivel empresarial como Postgres u Oracle o utilizar un almacenamiento de objetos basado en la nube como AWS S3. Si es esto último, tendrás que pensar qué formato de almacenamiento de tablas transaccionales usar, hudi, tablas delta o iceberg. También deberá considerar si necesita una herramienta de orquestación de tuberías como Airflow o Dagster.
Y ni siquiera he hablado de los tipos de comprobaciones automatizadas que necesitaría para los límites de las capas. Los ejemplos incluyen:
Recuentos de filas de bronce e integridad de la fuente Exclusividad de la clave de plata, verificaciones de valores aceptados e integridad referencial Conciliación de oro con los totales de plata Alertas de umbrales de frescura y volumen para tasas de cuarentena y deriva de esquema.
Pero esos son sólo los ingredientes del pastel. El punto importante es comprender los conceptos básicos del patrón medallón y reconocer cómo y dónde puede encajar en sus canales ETL nuevos o existentes.
La arquitectura medallón funciona porque hace visibles las distinciones en los datos. Los datos recibidos no son los mismos que los datos validados, y los datos validados no están listos automáticamente para una decisión comercial particular.