Introduzione
La scorsa settimana ho avuto il piacere di partecipare alla conferenza Coalesce di dbt a Londra, dove ho seguito un intervento davvero interessante di Felipe Leite e Stephen Pastan di Miro. Hanno spiegato come fossero riusciti a ottenere un netto miglioramento delle prestazioni sostituendo il clone dell'intero database con cloni multipli a livello di tabella. Non potevo non provarlo di persona.
Gli esperimenti
I risultati sono stati raccolti con la seguente query, che misura la durata di ogni processo passando alla funzione DATEDIFF il primo start time e l'ultimo end time delle query:
select
count(*) as query_count,
datediff(seconds, min(start_time), max(end_time)) as duration,
sum(credits_used_cloud_services) as credits_used_cloud_services
from snowflake.account_usage.query_history where query_tag = X;
Ottimizza l'utilizzo di Snowflake
SELECT ottimizza in automatico e ti aiuta a gestire l'utilizzo di Snowflake in tutta semplicità.
Setup
Creiamo un database con 10 schemi, ognuno con 100 tabelle:
import snowflake.connector
con = snowflake.connector.connect(
...
)
for i in range(1, 11):
con.cursor().execute(f"create schema test.schema_{i};")
for j in range(1, 101):
con.cursor().execute(f"create table test.schema_{i}.table_{j} (i number) as (select 1);")
Riferimento - Clone del database
1create database test_1 clone test;
L'operazione è stata completata in 22m 34s.
Risultati:
| Query Count | 1 |
|---|---|
| Durata | 22m 34s |
| Crediti cloud services | 0.179 |
Esperimento 1 - Cloni a livello di schema
import snowflake.connector
from snowflake.connector import DictCursor
def clone_database_by_schema(con, source_database, target_database):
con.cursor().execute(f"create database {target_database};")
cursor = con.cursor(DictCursor)
cursor.execute(f"show schemas in database {source_database};")
for i in cursor.fetchall():
if i["name"] not in ("INFORMATION_SCHEMA", "PUBLIC"):
con.cursor().execute_async(f"create schema {target_database}.{i['name']} clone {source_database}.{i['name']};")
con = snowflake.connector.connect(
...
session_parameters={
'QUERY_TAG': 'test 2',
Espandi codice
Risultati:
| Query Count | 12 |
|---|---|
| Durata | 1m 47s |
| Crediti cloud services | 0.148 |
Con execute_async ogni istruzione SQL viene eseguita senza attendere il completamento della precedente: il risultato è che tutti e 10 gli schemi vengono clonati in parallelo. Stiamo parlando di un'esecuzione 10 volte più rapida, dall'inizio alla fine, rispetto al clone classico del database.
Esperimento 2 - Cloni a livello di tabella
import snowflake.connector
from snowflake.connector import DictCursor
def clone_database_by_table(con, source_database, target_database):
con.cursor().execute(f"create database {target_database};")
cursor = con.cursor(DictCursor)
cursor.execute(f"show tables in database {source_database};")
results = cursor.fetchall()
schemas_to_create = {r['schema_name'] for r in results}
tables_to_clone = [f"{r['schema_name']}.{r['name']}" for r in results]
for schema in schemas_to_create:
con.cursor().execute(f"create schema {target_database}.{schema};")
for table in tables_to_clone:
Espandi codice
L'esecuzione ha richiesto 1 minuto e 48 secondi: il vero collo di bottiglia è la velocità con cui il client riesce a inviare le query (probabilmente per via dei tempi di attesa sulla rete). Per attenuare il problema, ho distribuito i comandi su 10 thread:
1import snowflake.connector
2from snowflake.connector import DictCursor
3import threading
4
5class ThreadedRunCommands():
6 """Helper class for running queries across a configurable number of threads"""
7 def __init__(self, con, threads):
8 self.threads = threads
9 self.register_command_thread = 0
10 self.thread_commands = [\
\
11 [] for _ in range(self.threads)\
\
12 ]
13 self.con = con
14
15 def register_command(self, command):
Espandi codice
Risultati:
| Query Count | 1012 |
|---|---|
| Durata | 22s |
| Crediti cloud services | 0.165 |
Con 10 thread, l'intervallo fra l'avvio del comando create database e il completamento dell'ultimo create table ... clone è stato di appena 22 secondi: 60 volte più veloce del comando create database ... clone. Il collo di bottiglia rimane la velocità con cui le query vengono inviate.
In sintesi
Il quadro completo dei risultati:
| Strategia di clonazione | Riferimento - Clone del database | Esperimento 1 - Cloni a livello di schema | Esperimento 2 - Cloni a livello di tabella |
|---|---|---|---|
| Query count | 1 | 12 | 1012 |
| Durata | 22m 34s | 1m 47s | 22s |
| Crediti cloud services | 0.179 | 0.148 | 0.165 |
Tutte le query eseguite hanno utilizzato esclusivamente cloud services, senza bisogno di un warehouse attivo né di riattivarne uno sospeso.
Mi auguro che Snowflake migliori le funzionalità di clonazione di schemi e database; nel frattempo, clonare le tabelle resta la strada migliore.
Ancora grazie a Felipe Leite e Stephen Pastan di Miro per aver condiviso questo approccio!
Niall Woodward·Co-founder & CTO di SELECT
Niall è Co-Founder e CTO di SELECT, la piattaforma SaaS per la gestione e l'ottimizzazione dei costi di Snowflake. Prima di fondare SELECT, ha lavorato come data engineer presso Brooklyn Data Company e in diverse startup. Appassionato di open-source, è anche maintainer di SQLFluff e autore di tre pacchetti dbt: dbt_artifacts, dbt_snowflake_monitoring e dbt_query_tags.