Einleitung
Letzte Woche durfte ich die Coalesce-Konferenz von dbt in London besuchen und bin dort in einen wirklich starken Vortrag von Felipe Leite und Stephen Pastan von Miro hineingeraten. Die beiden berichteten, wie sie einen deutlichen Geschwindigkeitsgewinn erzielt haben, indem sie Datenbank-Klone durch mehrere Tabellen-Klone ersetzt haben. Das musste ich selbst ausprobieren.
Experimente
Die Ergebnisse wurden mit folgender Query erfasst. Sie misst die Dauer jedes Vorgangs, indem die früheste Query-Startzeit und die letzte Query-Endzeit an die DATEDIFF-Funktion übergeben werden:
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;
Snowflake-Nutzung optimieren
SELECT optimiert Ihre Snowflake-Nutzung automatisch und macht das Management spürbar einfacher.
Setup
Legen Sie eine Datenbank mit 10 Schemas an, in jedem 100 Tabellen:
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);")
Referenzwert – Datenbank-Klon
1create database test_1 clone test;
Dieser Vorgang dauerte 22 Min. 34 Sek.
Ergebnisse:
| Query Count | 1 |
|---|---|
| Dauer | 22 Min. 34 Sek. |
| Cloud Services Credits | 0,179 |
Experiment 1 – Klone auf Schema-Ebene
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',
Code ausklappen
Ergebnisse:
| Query Count | 12 |
|---|---|
| Dauer | 1 Min. 47 Sek. |
| Cloud Services Credits | 0,148 |
Mit execute_async wird jede SQL-Anweisung abgesetzt, ohne auf den Abschluss der vorherigen zu warten – so werden alle 10 Schemas parallel geklont. Das Ergebnis: stolze 10x schneller von Anfang bis Ende als der klassische Datenbank-Klon.
Experiment 2 – Klone auf Tabellen-Ebene
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:
Code ausklappen
Das dauerte 1 Minute 48 Sekunden. Der begrenzende Faktor war die Rate, mit der der Client die Queries absetzen konnte (vermutlich wegen Netzwerk-Latenz). Um das abzufedern, habe ich die Befehle auf 10 Threads verteilt:
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):
Code ausklappen
Ergebnisse:
| Query Count | 1012 |
|---|---|
| Dauer | 22 Sek. |
| Cloud Services Credits | 0,165 |
Mit 10 Threads vergingen zwischen dem Start des create database-Befehls und dem Abschluss des letzten create table ... clone-Befehls gerade einmal 22 Sekunden – also 60x schneller als der Befehl create database ... clone. Der Engpass bleibt weiterhin die Rate, mit der sich Queries absetzen lassen.
Fazit
Die vollständigen Ergebnisse:
| Klon-Strategie | Referenzwert – Datenbank-Klon | Experiment 1 – Klone auf Schema-Ebene | Experiment 2 – Klone auf Tabellen-Ebene |
|---|---|---|---|
| Query Count | 1 | 12 | 1012 |
| Dauer | 22 Min. 34 Sek. | 1 Min. 47 Sek. | 22 Sek. |
| Cloud Services Credits | 0,179 | 0,148 | 0,165 |
Alle ausgeführten Queries liefen ausschließlich über Cloud Services – ein laufendes Warehouse oder das Aufwecken eines pausierten Warehouses war nicht nötig.
Ich hoffe, dass Snowflake die Schema- und Datenbank-Klon-Funktionalität weiter verbessert. Bis dahin ist das Klonen einzelner Tabellen klar der bessere Weg.
Nochmals vielen Dank an Felipe Leite und Stephen Pastan von Miro, dass sie diese Erkenntnisse geteilt haben!
Niall Woodward·Co-Founder & CTO von SELECT
Niall ist Co-Founder & CTO von SELECT, einer SaaS-Plattform für Kostenmanagement und Optimierung in Snowflake. Vor SELECT war Niall als Data Engineer bei Brooklyn Data Company und in mehreren Startups tätig. Als Open-Source-Enthusiast ist er außerdem Maintainer von SQLFluff sowie Schöpfer von drei dbt-Packages: dbt_artifacts, dbt_snowflake_monitoring und dbt_query_tags.