Ejecución de SQL simultáneamente en tres servidores DuckDB remotos con Quack

, la buena gente de DuckDB lanzó un protocolo de comunicación de base de datos llamado Quack. Su objetivo principal era permitir que las bases de datos DuckDB mantenidas en diferentes servidores se comunicaran entre sí a través de HTTP en una disposición cliente/servidor y permitirles leer y escribir datos entre sí.

En otras palabras, usando el protocolo Quack, DuckDB ubicado en el servidor A ahora podría consultar o escribir en una base de datos DuckDB en el servidor remoto B. Esto puede sonar un poco como procesamiento de datos distribuido, pero no lo es y el equipo de DuckDB se esforzó en enfatizar que Quack no facilita el procesamiento de consultas distribuidas.

Aun así, estaba intrigado y pude ver muchos usos para Quack. En particular, estaba interesado en saber si era posible ejecutar sentencias SQL paralelas en cada servidor y reunir los resultados de cada consulta y ponerlos a disposición para su uso o visualización en un servidor coordinador.

Tenga en cuenta que esta es una propuesta diferente a simplemente unir tablas en diferentes servidores en una única declaración SQL. DuckDB puede hacerlo adjuntando una base de datos del servidor B al servidor A, por ejemplo, y luego ejecutando SQL en el servidor A que puede leer datos en el servidor B como si fuera local.

Entonces, para investigar las lecturas y escrituras simultáneas que Quack debería habilitar, creé un repositorio de GitHub llamado cluster-duck, y no, no es un clúster DuckDB distribuido. Es solo una versión cómica de la expresión común y grosera que probablemente ya conoces. Pero le permitirá realizar lecturas y escrituras simultáneas en bases de datos remotas de DuckDB.

Nota: Antes de continuar, me gustaría declarar que no tengo afiliación ni asociación comercial con ninguno de los productos, sistemas o sus creadores que se mencionan en este artículo.

la configuración

Para probar Quack, configuré 3 servidores AWS EC2 a través de una pila de CloudFormation. Cada servidor contiene una base de datos DuckDB y uno de los servidores también actúa como nodo coordinador. Puedes encontrar la pila CF en mi repositorio de GitHub.

Desde AWS CloudShell (o localmente si tiene instalada la CLI de AWS), con el repositorio desprotegido, implemente la pila con:

implementación de aws cloudformation –region us-east-2 –stack-name cluster-duck-test-v2 –template-file python-reference/infra/aws/cluster-duck-3-node.yaml –capabilities CAPABILITY_NAMED_IAM

Para los tres servidores EC2, se instaló lo siguiente:

Amazon Linux 2023 ARM64. Un archivo de intercambio de 512 MB. Python 3.12 y pip. Un entorno virtual Python en: /opt/cluster-duck-venv duckdb==1.5.5 boto3 DuckDB 1.5.5 ARM64 CLI en: /usr/local/bin/duckdb La extensión oficial de DuckDB Quack, cargada por el proceso de trabajo. El programa del servidor Quack en: /opt/cluster-duck/quack_server.py La base de datos del trabajador en: /var/lib/cluster-duck/worker.duckdb Un servicio systemd llamado: cluster-duck-quack.service Un temporizador de apagado automático, cuatro horas de forma predeterminada.

Quack escucha en el puerto 9494 de forma predeterminada. Su token de autenticación se recupera de un parámetro cifrado del almacén de parámetros SSM.

La fuente de Python se copia en los tres servidores porque comparten la misma plantilla de lanzamiento de CloudFormation. Sin embargo, el coordinador sólo se hace ejecutable como comando en un servidor, generalmente el trabajador 1.

Estos archivos se instalan en cada servidor:

/opt/cluster-duck/quack_server.py /opt/cluster-duck/seed_ related_data.py /opt/cluster-duck/ related_cluster_sql.py /opt/cluster-duck/cluster_duck/sql_api.py

El servidor de coordinación (Trabajador 1) obtiene además estos lanzadores de comandos:

/usr/local/bin/cluster-duck /usr/local/bin/cluster-duck-sql

Cuando ejecuta cluster-duck-sql en el coordinador, esto finalmente comienza:

/opt/cluster-duck-venv/bin/python /opt/cluster-duck/ related_cluster_sql.py

La fuente de Python está comprimida e incrustada directamente dentro de la plantilla de CloudFormation como un archivo codificado en Base64.

Durante el arranque de EC2, el script de datos de usuario:

Decodifica el archivo incrustado. Crea /opt/cluster-duck. Extrae los archivos de Python en ese directorio. Crea los lanzadores de comandos en el trabajador 1. Inicia el servicio Quack en cada trabajador.

Todo lo necesario está contenido en el archivo de CloudFormation.

Cómo funciona la coordinación

Quack lleva cada declaración SQL al servidor DuckDB elegido y devuelve su resultado. La parte que coordina las tres llamadas es el código Python que se ejecuta en el Trabajador 1.

En primer lugar, cada argumento de consulta o archivo de consulta se valida como una única instrucción SQL y se convierte en un QueryFragment. Los fragmentos están etiquetados en el orden en que fueron suministrados:

fragmentos.append( QueryFragment(worker_id, f"query-{index}", sql) )

Luego, el coordinador crea un hilo por fragmento y una barrera con el mismo número de participantes:

barrera = threading.Barrier(len(fragmentos)) época = time.perf_counter() def run_fragment(fragmento): barrera.wait() iniciado = time.perf_counter() raw_result = self.executor(fragment.worker_id, fragment.sql) terminado = time.perf_counter() return { "start_offset_ms": (iniciado – época) * 1000, "duration_ms": (terminado – iniciado) * 1000, "resultado": raw_result, } con ThreadPoolExecutor(max_workers=len(fragments)) como grupo: futuros = { pool.submit(run_fragment, fragment): fragmento por fragmento en fragmentos }

La barrera retiene los hilos hasta que cada fragmento está listo y luego los suelta. No se iniciarán exactamente en el mismo ciclo de CPU porque todavía se aplica la programación normal del sistema operativo, razón por la cual el resultado incluye una extensión de inicio medida.

Cada fragmento obtiene su propia conexión de cliente DuckDB local en el coordinador. Esa conexión carga Quack, adjunta un trabajador remoto y envía la declaración a través de remote.query():

con duckdb.connect() como conexión: conexión.execute("INSTALAR curandero") conexión.execute("CARGAR curandero") conexión.execute( f"ATTACH {endpoint} AS remoto " f"(TYPE curandero, TOKEN {token}, DISABLE_SSL true)" ) cursor = conexión.execute( f"SELECT * FROM remoto.query({sql_string(sql)})" ) columnas = tupla(descripción[0]para una descripción en cursor.description) devuelve FragmentResult(columnas, cursor.fetchall())

La llamada a fetchall() materializa cada resultado en el Trabajador 1. El coordinador espera todos los futuros, registra cuándo comenzó cada uno y cuánto tiempo tomó, y luego presenta los resultados separados en una salida. Quack se encarga de la ejecución y el transporte remotos; Python realiza la construcción de fragmentos, la liberación simultánea, el tiempo y la recopilación de resultados.

Creando nuestros datos de prueba

Cada uno de los tres servidores tiene una base de datos DuckDB diferente, como se muestra a continuación.

Trabajador Archivo de base de datos Tabla generada ————————————————————— Trabajador 1 /var/lib/cluster-duck/worker.duckdb ventas Trabajador 2 /var/lib/cluster-duck/worker.duckdb clientes Trabajador 3 /var/lib/cluster-duck/worker.duckdb productos

Cada tabla generada contiene un conjunto de datos sintéticos apropiado de diez millones de registros. Aquí están los primeros 5 registros de cada uno para darle una mejor idea de lo que contienen.

consulta-1 (trabajador-1) – seleccione * del límite de ventas 5 id_venta id_cliente id_producto cantidad canal_ventas método_pago estado_venta precio_catálogo precio_unidad vendido descuento_pct fecha_venta ——- ———– ———- ——– ————- ————– ———– ————— ————— ———— ———- 1 7,920 104,730 2 tienda transferencia bancaria enviada 1.052,3 999,69 0,05 2024-01-02 2 15.839 209.459 3 procesamiento de billetera en el mercado 99,59 89,63 0,1 2024-01-03 3 23.758 314.188 4 factura telefónica devuelta 1.146,88 974,85 0,15 2024-01-04 4 31.677 418.917 5 tarjeta online cancelada 194,17 194,17 0 2024-01-05 5 39.596 523.646 1 tienda transferencia bancaria completada 1.241,46 1.179,39 0.05 2024-01-06 consulta-2 (trabajador-2) – seleccione * del límite de clientes 5 id_cliente código_cliente país segmento nivel_membresía is_active límite_crédito fecha_último_visto ———– ————— ——- ————– ————— ——— ———— ———– ——————- 1 CUST-0000000001 Plata para pequeñas empresas de EE. UU. Verdadero 250.10 2015-01-02 2025-01-01 00:00:01 2 CUST-0000000002 DE oro empresarial Verdadero 250.20 2015-01-03 2025-01-01 00:00:02 3 CUST-0000000003 FR estándar del sector público Verdadero 250,30 2015-01-04 2025-01-01 00:00:03 4 CUST-0000000004 CA plata de consumo Verdadero 250,40 2015-01-05 2025-01-01 00:00:04 5 CUST-0000000005 AU oro para pequeñas empresas Verdadero 250,50 2015-01-06 2025-01-01 00:00:05 consulta-3 (trabajador-3) – seleccione * del límite de productos 5 id_producto sku categoría marca región_proveedor precio_catálogo cantidad_stock fecha_introducida descontinuada ———- ————– ——– ——- ————— ————— ————– ———— ————— 1 SKU-0000000001 hogar Bramble EU 5.01 13 False 2020-01-02 2 SKU-0000000002 jardín Cobalt US 5.02 26 False 2020-01-03 3 SKU-0000000003 deportes Dove APAC 5.03 39 False 2020-01-04 4 SKU-0000000004 ropa Elm UK 5.04 52 False 2020-01-05 5 SKU-0000000005 comida Aster EU 5.05 65 False 2020-01-06

Los datos se generan directamente dentro de cada base de datos DuckDB durante el primer arranque de EC2. No se carga desde su computadora ni se copia entre servidores. Cada servidor sigue esta secuencia.

1. Determinar qué trabajador es

CloudFormation le da a cada instancia EC2 una etiqueta WorkerIndex:

Trabajador 1 → WorkerIndex=1 Trabajador 2 → WorkerIndex=2 Trabajador 3 → WorkerIndex=3 El script de arranque lee esa etiqueta a través del Servicio de metadatos de la instancia EC2: WORKER_INDEX=$(curl -fsS -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" http://169.254.169.254/latest/meta-data/tags/instance/WorkerIndex)

2. Ejecute el programa de generación de datos.

CloudFormation instala este programa en cada servidor:

/opt/cluster-duck/seed_ related_data.py

Luego ejecuta:

/opt/cluster-duck-venv/bin/python /opt/cluster-duck/seed_ related_data.py –worker "$WORKER_INDEX" –rows 10000000

El recuento de filas proviene del parámetro RowCount de CloudFormation, que por defecto es 10 millones.

3. Abra el archivo DuckDB del trabajador.

El programa se abre:

/var/lib/cluster-duck/worker.duckdb

que contiene este código.

con duckdb.connect(str(args.database)) como conexión: conexión.execute( CREATE_SQL[args.worker], {"row_count": args.rows},)

Cada servidor usa el mismo nombre de archivo de base de datos, pero es un archivo diferente en una instancia EC2 diferente.

Accediendo a la ventana de terminal de su coordinador

Queremos ejecutar algunas demostraciones y, para ello, debe poder acceder al terminal CLI de su servidor EC2 coordinador. Para hacerlo, abra la consola de AWS y vaya a la consola EC2. Verás una pantalla como esta.

Haga clic en el ID de instancia que corresponde a su instancia de coordinador. En la siguiente pantalla habrá un botón Conectar en la esquina superior derecha. Haz clic en eso. Verás esta pantalla

Asegúrese de haber seleccionado el botón de opción Administrador de sesión SSM, luego haga clic en el botón Conectar en la parte inferior derecha de la pantalla. Eso debería darle acceso a la ventana del terminal CLI como esta,

Ejemplos

En los siguientes ejemplos, para dejar las cosas lo más claras posible, uso el texto SQL sin formato en los fragmentos de código; sin embargo, también es posible almacenar el SQL en archivos separados y usarlos como entrada. Por ejemplo,

[root@ip-10-42-0-10 ~]# cluster-duck-sql –query-file "worker-1=/root/cluster-duck-sql/sales.sql" –query-file "worker-2=/root/cluster-duck-sql/customers.sql" –query-file "worker-3=/root/cluster-duck-sql/products.sql"

1. Ejecutar algunas declaraciones SQL simples

En la CLI del terminal, escriba el siguiente código.

sh-5.2$ sudo -i [root@ip-10-42-0-10 ~]# cluster-duck-sql –query "worker-1=SELECT sale_status, COUNTDESDE el GRUPO de ventas POR estado_venta ORDENAR POR estado_venta" –query "trabajador-2=SELECCIONAR país, CONTARDE clientes GRUPO POR país ORDENAR POR país" –query "trabajador-3=SELECCIONAR categoría, CONTARDE productos GRUPO POR categoría ORDENAR POR categoría" # salida # Consultas remotas simultáneas tabla de trabajadores start_offset_ms duración_segundos ——– ——- ————— —————- trabajador-1 consulta-1 0,996 0,572 trabajador-2 consulta-2 5,514 0,572 trabajador-3 consulta-3 0,738 0,54 Iniciar propagación: 4,776 ms consulta-1 (trabajador-1) sale_status count_star() ———– ———— cancelado 2.000.000 completado 2.000.000 procesando 2.000.000 devuelto 2.000.000 enviado 2.000.000 consulta-2 (trabajador-2) país count_star() ——- ———— AU 1.666.666 CA 1.666.667 DE 1.666.667 FR 1.666.667 UK 1.666.666 US 1.666.667 consulta-3 (trabajador-3) categoría count_star() ———– ———— ropa 1.666.667 electrónica 1.666.666 alimentos 1.666.666 jardín 1.666.667 hogar 1.666.667 deportes 1.666.667

2. Algo de SQL complejo (he recortado parte del resultado para ahorrar espacio)

[root@ip-10-42-0-10 ~]# hora cluster-duck-sql –query "worker-1=CON AS diario (SELECCIONE fecha_venta, canal_ventas, método_pago, estado_venta, CONTARAS recuento_transacción, SUM(cantidad) AS unidades, SUM(cantidad * precio_unidad_vendida) AS ingresos, AVG(descuento_pct) AS descuento_promedio, QUANTILE_CONT(precio_unidad_venta, 0,50) AS precio_mediano, QUANTILE_CONT(precio_unidad_venta, 0,95) AS p95_price FROM sales GROUP BY ALL), analizado COMO (SELECCIONAR *, SUMA(ingresos) SOBRE ( PARTICIÓN POR canal_ventas ORDENAR POR fecha_venta FILAS ENTRE 29 FILA ANTERIOR Y ACTUAL ) COMO ingresos_fila_30_rolling, RANGO() SOBRE ( PARTICIÓN POR fecha_venta ORDENAR POR ingresos DESC ) COMO rango_ingresos_diarios FROM diario ) SELECCIONAR * DESDE analizado DONDE rango_ingresos_diarios <= 3 ORDENAR POR fecha_venta DESC, rango_ingresos_diarios LÍMITE 100" –query "worker-2=CON grupos_clientes COMO ( SELECCIONE país, segmento, nivel_membresía, está_activo, AÑO(fecha_de_unión) COMO año_de_unión, CASO CUANDO límite_crédito < 2500 ENTONCES 'bajo_2500' CUANDO límite_crédito < 5000 ENTONCES '2500_to_4999' CUANDO límite_crédito < 7500 ENTONCES '5000_to_7499' MÁS '7500_plus' FINALIZA COMO banda_crédito, CONTARAS número_cliente, AVG(límite_crédito) AS límite_crédito_promedio, STDDEV_POP(límite_crédito) AS límite_crédito_stddev, QUANTILE_CONT(límite_crédito, 0,50) AS límite_crédito medio, QUANTILE_CONT(límite_crédito, 0,95) AS p95_límite_crédito, MIN(fecha_de_unión) AS primer_unión, MAX(last_seen_at) AS actividad_más_reciente FROM clientes GRUPO POR TODOS), clasificado COMO ( SELECT *, SUM(customer_count) OVER ( PARTITION BY country ) AS country_total, RANK() OVER ( PARTITION BY country ORDER BY customer_count DESC ) AS group_rank FROM customer_groups ) SELECT *, ROUND(100.0 * customer_count / country_total, 2) AS porcentaje_de_país DESDE clasificado DONDE rango_grupo <= 10 ORDENAR POR país, rango_grupo LÍMITE 100" –query "trabajador-3=CON grupos_inventario AS ( SELECCIONAR categoría, marca, región_proveedor, descontinuado, AÑO (fecha_introducción) COMO año_introducido, CONTARAS recuento_producto, SUM(cantidad_existencias) AS unidades_existencias, SUM(cantidad_existencias * precio_catálogo) AS valor_inventario, AVG(precio_catálogo) AS precio_promedio, STDDEV_POP(precio_catálogo) AS precio_desv.estándar, QUANTILE_CONT(precio_catálogo, 0,50) AS precio_mediano, QUANTILE_CONT(precio_catálogo, 0.95) AS p95_price FROM productos GRUPO POR TODOS), clasificado COMO (SELECCIONAR *, SUMA(valor_inventario) SOBRE (PARTICIÓN POR categoría) AS categoría_valor_inventario, RANGO() SOBRE (PARTICIÓN POR categoría ORDEN POR valor_inventario DESC) AS rango_inventario FROM grupos_inventario) SELECT *, ROUND( 100.0 * valor_inventario / categoría_valor_inventario, 2) AS porcentaje_de_categoría_valor DESDE clasificado DONDE inventario_clasificación <= 10 ORDER BY categoría, inventario_clasificación LÍMITE 100" # # Salida Consultas remotas simultáneas tabla de trabajadores start_offset_ms duración_segundos ——– ——- ————— —————- trabajador-1 consulta-1 1,673 15,491 trabajador-2 consulta-2 1,84 4,045 trabajador-3 consulta-3 1,42 4,045 Spread inicial: 0,420 ms consulta-1 (trabajador-1) fecha_venta canal_ventas método_pago estado_venta recuento_transacción unidades ingresos descuento_promedio precio_mediano precio_p95 ingresos_fila_30 rango_ingresos_diarios ———- ————- ————– ———– —————– —— ————- ———- ———— ——— ———————- —————— 2025-12-30 store bank_transfer cancelada 6.849 34.245 32.722.337,65 0,05 955,72 1.810,568 588.590.148,25 1 … … … 2025-11-11 billetera del mercado completada 6.849 6.849 6.199.619,04 0,1 905,5 1.714,096 557.458.798,88 2 consulta-2 (trabajador-2) segmento de país nivel_de_miembro is_active año_de_unión_banda_de_crédito_cuenta_de_clientes límite_de_crédito_promedio límite_de_crédito_stddev límite_de_crédito medio p95_límite_de_crédito first_joined most_recent_activity country_total group_rank porcentaje_del_país ——- ————– ————— ——— ———– ———– ————– ——————– ————- ——————- —————- ———— ——————– ————- ———- ——————— AU Small_Business Gold Verdadero 2.017 7500_plus 21.659 8.874,317 794,81 8878,10 10115,70 2017-01-01 2025-04-26 17:20:41 1.666.666 1 1,3 AU oro del sector público Verdadero 2.017 7500_plus 21.649 8.873,263 794.868 8876,70 10114,30 2017-01-01 2025-04-26 17:20:35 1.666.666 2 1,3 … … … Plata para pequeñas empresas de EE. UU. Verdadero 2.022 7500_plus 21.605 8.876,671 792.939 8873,30 10110,10 2022-01-01 2025-04-26 17:46:37 1.666.667 10 1.3 consulta-3 (trabajador-3) categoría marca región_proveedor año_de_introducción descontinuado recuento_producto unidades_stock valor_inventario precio_promedio precio_stddev precio_mediano p95_precio categoría_inventario_valor rango_inventario porcentaje_de_valor_categoría ———– ——- ————— ———— ————— ————- ———– ————— ————- ———— ———— ——— ———————— ————– —————————- ropa Aster US Falso 2,020 33,457 83,743,010 84191813103,00 1.004,833 577,373 1004,90 1904,90 4188462626332,28 1 2,01 ropa Aster UK Falso 2.020 33.458 83.228.180 83715767600,00 1.005,04 577,369 1.005,20 1.905,42 4188462626332,28 2 2 … … … inicio Dove APAC Falso 2.024 32.999 82.797.401 83251410292,23 1.004,931 577,217 1005,23 1904,83 4190160306717,15 8 1,99 inicio Dove APAC Falso 2.020 33.008 82.760.072 83227553226,96 1.005,078 577,503 1005,33 1905,43 4190160306717,15 9 1,99 … … deportivo Olmo UE Falso 2.020 33.006 82.731.982 83205651717,58 1.005,109 577,373 1005,19 1905,44 4190156973777,15 9 1,99 deportes Bramble UE Falso 2.020 33.005 82.700.285 83167881906,05 1.004,964 577,415 1005,21 1905,36 4190156973777.15 10 1.98 real 0m17.664s usuario 0m1.643s sys 0m0.389s [root@ip-10-42-0-10 ~]#

3. Escrituras/lecturas simultáneas

Para mostrar esto, escribiremos simultáneamente 20 registros nuevos en nuestra tabla de ventas y realizaremos tres lecturas. Cada lectura ve una instantánea consistente que contiene las transacciones de inserción independientes que se habían confirmado antes de que comenzara la lectura. Por lo tanto, es posible que no vea ninguna, algunas o todas las filas nuevas, pero no verá la mitad de una inserción individual o un resultado que cambie mientras se escanea SELECT.

Cada inserción en este ejemplo es una transacción de confirmación automática independiente. Si 19 inserciones tienen éxito y una falla, las 19 escrituras exitosas permanecen confirmadas. No hay ninguna transacción distribuida y Cluster-Duck no revierte las declaraciones exitosas.

Para que la demostración sea repetible, primero elimine las filas dejadas por ejecuciones anteriores:

[root@ip-10-42-0-10 ~]# cluster-duck-sql –allow-write –query "worker-1=BORRAR DE ventas DONDE sale_id ENTRE 30000001 Y 30000020"

Tenga en cuenta también que para realizar cambios en los datos de una base de datos debemos proporcionar el argumento —permitir escritura.

Ahora ejecute las 20 inserciones y tres lecturas juntas. Para ver qué SQL se ejecuta para cada una de las etiquetas de consulta en el resultado, podemos usar el argumento — show-sql.

[root@ip-10-42-0-10 ~]# cluster-duck-sql –show-sql –allow-write –query "worker-1=INSERT INTO sales VALUES (30000001,1,1,1,'online','card','completed',100.00,100.00,0.0,DATE '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000002,2,2,2,'store','bank_transfer','processing',110.00,104.50,0.05,DATE '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000003,3,3,3,'mercado','billetera','enviado',120.00,108.00,0.10,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000004,4,4,4,'teléfono','factura','completado',130.00,110.50,0.15,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000005,5,5,5,'en línea','tarjeta','procesamiento',140.00,112.00,0.20,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000006,6,6,1,'tienda','transferencia_bancaria','enviado',150.00,150.00,0.0,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000007,7,7,2,'mercado','billetera','completado',160.00,152.00,0.05,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000008,8,8,3,'teléfono','factura','procesamiento',170.00,153.00,0.10,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000009,9,9,4,'en línea','tarjeta','enviado',180.00,153.00,0.15,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000010,10,10,5,'tienda','transferencia_bancaria','completado',190.00,152.00,0.20,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000011,11,11,1,'mercado','billetera','procesamiento',200.00,200.00,0.0,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000012,12,12,2,'teléfono','factura','enviado',210.00,199.50,0.05,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000013,13,13,3,'en línea','tarjeta','completado',220.00,198.00,0.10,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000014,14,14,4,'tienda','transferencia_bancaria','procesamiento',230.00,195.50,0.15,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000015,15,15,5,'mercado','billetera','enviado',240.00,192.00,0.20,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000016,16,16,1,'teléfono','factura','completado',250.00,250.00,0.0,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000017,17,17,2,'en línea','tarjeta','procesamiento',260.00,247.00,0.05,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000018,18,18,3,'tienda','transferencia_bancaria','enviado',270.00,243.00,0.10,FECHA '2026-08-09')" –query "trabajador-1=INSERTAR EN VALORES de ventas (30000019,19,19,4,'mercado','billetera','completado',280.00,238.00,0.15,FECHA '2026-08-09')" –query "worker-1=INSERT INTO sales VALUES (30000020,20,20,5,'teléfono','factura','procesamiento',290.00,232.00,0.20,FECHA '2026-08-09')" –query "trabajador-1=SELECCIONAR CUENTACOMO filas_visibles DE ventas DONDE sale_id ENTRE 30000001 Y 30000020" –query "trabajador-1=SELECCIONAR CUENTACOMO filas_visibles DE ventas DONDE sale_id ENTRE 30000001 Y 30000020" –query "trabajador-1=SELECCIONAR CUENTAAS visible_rows FROM sales DONDE sale_id ENTRE 30000001 Y 30000020" # # Salida Consultas remotas simultáneas tabla de trabajadores start_offset_ms duración_segundos ——– ——– ————— —————- trabajador-1 consulta-19 77.923 3.648 trabajador-1 consulta-11 26.242 3.705 trabajador-1 consulta-23 4.64 3.727 trabajador-1 consulta-5 8.747 3.691 trabajador-1 consulta-1 4.832 3.744 trabajador-1 consulta-2 5.243 3.903 trabajador-1 consulta-15 113.315 3.823 trabajador-1 consulta-6 15.126 3.923 trabajador-1 consulta-22 84.907 3.853 trabajador-1 consulta-14 35.127 3.907 trabajador-1 consulta-18 71.325 3.871 trabajador-1 consulta-21 56.68 3.888 trabajador-1 consulta-12 28.05 3.936 trabajador-1 consulta-7 15.349 3.949 trabajador-1 consulta-20 89.082 3.875 trabajador-1 consulta-10 36.901 3.93 trabajador-1 consulta-16 77.02 3.895 trabajador-1 consulta-13 19.964 3.954 trabajador-1 consulta-3 5.632 3.968 trabajador-1 consulta-8 58.938 3.916 trabajador-1 consulta-9 15.774 3.959 trabajador-1 consulta-4 50.36 3.954 trabajador-1 consulta-17 84.575 3.936 Distribución inicial: 108.675 ms consulta-19 (trabajador-1) SQL: INSERTAR EN VALORES de ventas (30000019,19,19,4,'marketplace','wallet','completed',280.00,238.00,0.15,DATE '2026-08-09') Resultado: Recuento —– 1 consulta-11 (trabajador-1) SQL: INSERTAR EN VALORES de ventas (30000011,11,11,1,'marketplace','wallet','processing',200.00,200.00,0.0,DATE '2026-08-09') Resultado: Recuento —– 1 consulta-23 (trabajador-1) SQL: SELECCIONAR RECUENTOCOMO visible_rows DE ventas DONDE sale_id ENTRE 30000001 Y 30000020 Resultado: visible_rows ———— 4 … … consulta-22 (trabajador-1) SQL: SELECCIONAR CUENTACOMO filas_visibles DE ventas DONDE id_venta ENTRE 30000001 Y 30000020 Resultado: filas_visibles ———— 16 … … consulta-18 (trabajador-1) SQL: INSERTAR EN VALORES de ventas (30000018,18,18,3,'store','bank_transfer','shipped',270.00,243.00,0.10,DATE '2026-08-09') Resultado: Recuento —– 1 consulta-21 (trabajador-1) SQL: SELECCIONAR CUENTACOMO visible_rows DE ventas DONDE sale_id ENTRE 30000001 Y 30000020 Resultado: visible_rows ———— 13 … … consulta-17 (trabajador-1) SQL: INSERTAR EN VALORES de ventas (30000017,17,17,2,'online','card','processing',260.00,247.00,0.05,DATE '2026-08-09') Resultado: Conteo —– 1 [root@ip-10-42-0-10 ~]#

El resultado muestra que cuando se ejecutó la consulta 23, se habían insertado 4 registros. Cuando se ejecutó la consulta 22, se habían insertado 16 registros y para la consulta 21, se habían escrito 13 registros nuevos. Esto tiene sentido, ya que podemos ver en los tiempos de start_offset_ms que el orden de ejecución de las consultas fue consulta-23, luego consulta21 y finalmente consulta-22.

5. También puedes ejecutar DDL

Cree 3 tablas nuevas, una en cada base de datos, luego consúltelas.

[root@ip-10-42-0-10 ~]# cluster-duck-sql –allow-write –query "worker-1=CREAR O REEMPLAZAR TABLA sales_agg AS SELECT sale_status, COUNTAS sale_count DE ventas DONDE sale_status = 'cancelado' GRUPO POR sale_status" –query "worker-2=CREAR O REEMPLAZAR TABLA clientes_agg AS SELECCIONAR país, CONTARAS cuenta_clientes DE clientes DONDE país = 'Reino Unido' GRUPO POR país" –query "trabajador-3=CREAR O REEMPLAZAR TABLA productos_agg AS SELECCIONAR categoría, CONTARAS product_count FROM productos DONDE categoría = 'deportes' GRUPO POR categoría" cluster-duck-sql –query "worker-1=SELECT * FROM sales_agg ORDER BY sale_count DESC" –query "worker-2=SELECT * FROM clientes_agg ORDEN POR customer_count DESC" –query "worker-3=SELECT * FROM productos_agg ORDER BY product_count DESC" # # Salida concurrente consultas remotas tabla de trabajadores start_offset_ms duración_segundos ——– ——- ————— —————- trabajador-1 consulta-1 1.141 0.599 trabajador-2 consulta-2 1.033 0.603 trabajador-3 consulta-3 0.755 0.622 Distribución inicial: 0.386 ms consulta-1 (trabajador-1) Recuento —– 1 consulta-2 (trabajador-2) Recuento —– 1 consulta-3 (trabajador-3) Recuento —– 1 Consultas remotas simultáneas tabla de trabajadores start_offset_ms duración_segundos ——– ——- ————— —————- trabajador-1 consulta-1 0,925 0,464 trabajador-2 consulta-2 1,178 0,448 trabajador-3 consulta-3 0,675 0,467 Spread inicial: 0,503 ms consulta-1 (trabajador-1) estado_venta recuento_venta ———– ———- cancelado 2.000.000 consulta-2 (trabajador-2) país recuento_clientes ——- ————– Reino Unido 1.666.666 consulta-3 (trabajador-3) categoría recuento_producto ——– ————- deportes 1.666.667

El coste de todo esto.

El costo de esta configuración no debería ser una preocupación. Para empezar, DuckDB y Quack se pueden descargar y utilizar de forma gratuita. Los tres servidores EC2 que estoy levantando son pequeñas instancias de t4g.nano. Además de eso, tenemos tres volúmenes gp3 de 8 GB, direcciones IPv4 públicas, Administrador de sistemas, Almacén de parámetros y un recurso personalizado Lambda. La invocación de Lambda dura poco, pero permanece implementada hasta que se elimina la pila. Este Lambda se llama TokenManagerFunction en la plantilla de CloudFormation. Su única función es gestionar los tres tokens de autenticación de Quack. Funciona así

Creación de pila de CloudFormation ↓ Invocar TokenManager Lambda ↓ Generar tres tokens aleatorios de 64 caracteres ↓ Almacenarlos como parámetros SecureString en SSM ↓ Devolver éxito y detener

Aquí hay un costo estimado si ejecutamos toda esta configuración durante 4 horas.

Componente Costo aproximado Cuatro horas de EBS $0.011 Cuatro horas de EC2 $0.050 Cuatro horas de IPv4 público $0.060 Total $0.121

La cifra de $0,121 es una estimación para us-east-2, antes de créditos o asignaciones de nivel gratuito, impuestos y cargos por transferencia de datos. Los usuarios que califiquen de la capa gratuita de AWS pueden recibir algunas horas de IPv4 público sin cargo. AWS factura el almacenamiento gp3 en incrementos por segundo, con un mínimo de 60 segundos.

Sin embargo, para su tranquilidad, siempre recomendaría derribar cualquier infraestructura de AWS creada una vez que haya terminado. Esto se hace fácilmente si utiliza CloudFormation ejecutando el siguiente comando con la CLI de AWS.

aws cloudformation eliminar-pila –region us-east-2 –stack-name cluster-duck-test-v2

Resumen

Creé el repositorio “cluster-duck” para probar el nuevo protocolo de comunicaciones Quack de DuckDB. Quack permite que las bases de datos de DuckDB en diferentes servidores "hablen" entre sí a través de HTTP, y DuckDB lo está posicionando como un habilitador de las comunicaciones cliente/servidor entre las bases de datos de DuckDB.

Este desarrollo es potencialmente muy útil y estaba particularmente interesado en ver qué tan bien maneja Quack las lecturas y escrituras simultáneas hacia y desde una base de datos remota.

En mis pruebas y en el ejemplo que demostré, la respuesta parece ser que lo maneja bastante bien.

El equipo de DuckDB ha declarado que Quack es una característica experimental y en gran medida un trabajo en progreso. Hasta ese punto, puede esperar cambios potenciales en el protocolo, los nombres de las funciones, la configuración y los valores predeterminados, por lo que definitivamente no utilice Quack para ningún sistema de producción.

Con suerte, los conceptos que he descrito en este artículo serán útiles si necesita ejecutar consultas paralelas u otras declaraciones SQL en general contra bases de datos DuckDB que se ejecutan en diferentes servidores.

No puedo evitar preguntarme qué planes futuros tiene DuckDB para Quack. Si se convierte en una parte totalmente compatible del ecosistema DuckDB, definitivamente podría ver a Quack suministrando el transporte y el manejo de sesiones a un futuro motor distribuido de DuckDB, pero eso probablemente esté muy lejos. Pero incluso si hace lo que es capaz de hacer ahora, Quack será útil por derecho propio.

Eso es todo de mi parte por ahora. Puedes acceder a todo el código, plantilla de CloudFormation, etc. en mi repositorio de GitHub en:

https://github.com/taupirho/cluster-duck

Puede encontrar más información sobre DuckDB y Quack en la documentación en línea de DuckDB en este enlace.

https://duckdb.org/docs/current

PD: Estoy en el mercado para trabajar por contrato en este momento. Si usted o alguien que conoce está buscando un ingeniero de datos con experiencia, ya sea remoto o con sede en Edimburgo, Reino Unido, con habilidades en AWS, AI, Python, SQL, PySpark, DuckDB, etc., hágamelo saber a través de LinkedIn