Blockchains mit SQL abfragen · Lektion 6 von 6
Leistung, Kosten und Reproduzierbarkeit
9 Min. Lesezeit
Blockchain-Tabellen mit vollständiger Historie umfassen Milliarden von Zeilen. Dieselbe Abfrage kann je nach Schreibweise wenige Cent kosten und Sekunden dauern oder echtes Geld kosten und eine Stunde laufen. Die Gewohnheiten, die Abfragen schnell und günstig halten (enge Bereiche, festgelegte Zeiträume, explizite Versionen), sorgen auch dafür, dass Ergebnisse reproduzierbar sind – und genau das macht aus einem Abfrageergebnis einen Beleg. Diese Lektion behandelt beides und endet damit, wie sich ein Ergebnis anhand eines Nodes überprüfen lässt.
Wie Analyse-Engines Daten lesen
Data Warehouses und Lake-Engines speichern Daten spaltenweise statt zeilenweise. Eine Abfrage liest nur die Spalten, die sie benennt. SELECT * auf einer Transaktionstabelle liest daher auch die umfangreiche input-Calldata-Spalte, selbst wenn Sie nur drei schmale Spalten benötigen. Bei Diensten mit nutzungsabhängiger Abrechnung ist die Rechnung proportional zu den gelesenen Bytes. Spalten explizit anzugeben ist daher die erste Maßnahme zur Kostenkontrolle.
Die zweite Maßnahme ist die Partitionierung. Große Tabellen werden in Partitionen aufgeteilt, üblicherweise nach dem Tag des Blockzeitstempels oder nach Blockbereich, und als separate Dateien oder Segmente gespeichert. Wenn ein Filter auf der Partitionsspalte eine Partition ausschließt, überspringt die Engine sie vollständig: Das nennt man Partition Pruning. Innerhalb von Partitionen können Daten nach einer Spalte wie Blocknummer oder Adresse geclustert (sortiert) sein. Engines speichern Mindest- und Höchstwerte für jeden Datenblock, damit sie Blöcke überspringen können, die keine Treffer enthalten können.
Einige verbreitete Annahmen müssen korrigiert werden:
LIMIT 10macht einen Scan bei den meisten Data Warehouses nicht günstiger; Filterung und Scan erfolgen vor dem Limit.- Ein Filter auf einer Nicht-Partitionsspalte (wie
to_address) führt nicht zum Überspringen von Partitionen, auch wenn Clustering nach dieser Spalte helfen kann. - Die meisten Dienste bieten einen Probelauf oder
EXPLAIN, der die Bytes oder Partitionen angibt, die eine Abfrage lesen wird. Nutzen Sie diese Funktion, bevor Sie etwas über die vollständige Historie ausführen.
Die Größenordnung hilft bei der Kostenschätzung. Ethereum erzeugt einen Block pro 12-Sekunden-Slot, also etwa 7.200 Blöcke pro Tag; Bitcoin etwa 144. Ein Zeitraum von einer Woche auf Ethereum umfasst ungefähr 50.000 Blöcke – einen kleinen Bruchteil der mehr als 20 Millionen Blöcke in der bisherigen Chain-Historie.
Partition- und Blockbereichsfilter
Analysten denken in Blockhöhen; Tabellen sind nach Zeit partitioniert. Das zuverlässige Vorgehen besteht darin, den Blockbereich zunächst in ein Zeitfenster umzuwandeln und dann nach beiden Kriterien zu filtern.
-- Schritt 1: Das Zeitfenster ermitteln, das den Blockbereich abdeckt
SELECT MIN(block_timestamp) AS t0, MAX(block_timestamp) AS t1
FROM blocks
WHERE number BETWEEN 21100000 AND 21107199;
-- Schritt 2: Mit Literalen nach der Partitionsspalte filtern, danach nach Blöcken
SELECT transaction_hash, from_address, to_address, amount_raw
FROM token_transfers
WHERE block_timestamp >= TIMESTAMP '2024-11-01 00:00:00' -- Schritt 1, erweitert
AND block_timestamp < TIMESTAMP '2024-11-05 00:00:00' -- auf ganze Tage
AND block_number BETWEEN 21100000 AND 21107199 -- genauer Bereich
AND token_address = '0x7a11…0001';
Schritt 1 liest die kleine Tabelle blocks. Schritt 2 verwendet den Zeitfilter, damit die Engine nur einige Tagespartitionen einliest, sowie den Blockfilter, um genau die beabsichtigten Zeilen zurückzugeben. Die Erweiterung auf ganze Tage kostet wenig und schützt vor Problemen an Zeitstempelgrenzen.
Zwei Hinweise: Viele Engines führen Partition Pruning nur dann durch, wenn der Partitionsfilter die Spalte mit Konstanten vergleicht. Ein Join oder eine Unterabfrage, die das Zeitfenster bereitstellt, kann daher einen Scan der gesamten Tabelle auslösen. Übergeben Sie die Werte als Literale oder Abfrageparameter. Auch das Umhüllen der Partitionsspalte mit einer Funktion, etwa CAST(block_timestamp AS DATE) = …, kann bei manchen Engines Partition Pruning verhindern. Vergleichen Sie stattdessen die Rohspalte mit einem Wertebereich.
Weitere Einsparungen ergeben sich durch einmaliges Extrahieren und lokale Iteration. Eine Untersuchung umfasst typischerweise Dutzende Varianten derselben Frage zu denselben Adressen und Blöcken. Anstatt jedes Mal Tabellen mit vollständiger Historie zu durchsuchen, führen Sie eine gut gefilterte Abfrage aus, die alle Transfers abruft, an denen die Adressen des Falls innerhalb des Blockbereichs des Falls beteiligt sind. Speichern Sie die Daten in einer kleinen Tabelle oder einer Parquet-Datei und führen Sie die Erkundung dort durch, zum Beispiel in einer eingebetteten Engine auf einem Laptop. Der teure Scan erfolgt einmal, die Iterationen sind nahezu kostenlos, und der gespeicherte Extrakt dient zugleich als fester Snapshot des Belegmaterials. Das verbessert die Reproduzierbarkeit ebenso wie die Kosteneffizienz.
Stichproben
Stichproben eignen sich dazu, eine unbekannte Tabelle zu erkunden, Abfragelogik zu testen und Verteilungen wie typische Gebührenhöhen einzuschätzen. Für Summen, Nachverfolgungen und seltene Ereignisse sind sie ungeeignet. Der On-Chain-Wert ist stark konzentriert: Eine Handvoll Transfers kann den Großteil des Tagesvolumens ausmachen, und eine Stichprobe von 1 % wird sie normalerweise nicht erfassen.
Es gibt zwei Arten von Stichproben mit unterschiedlichen Auswirkungen:
- Speicherstichproben.
TABLESAMPLE SYSTEM (1 PERCENT)(in irgendeiner Form von BigQuery, PostgreSQL, Trino und DuckDB unterstützt) liest eine zufällige Teilmenge der Speicherblöcke. Dadurch sinkt die Zahl der gelesenen Bytes. Die Zeilen sind jedoch geclustert und nicht unabhängig, und bei manchen Engines liefert eine erneute Ausführung eine andere Stichprobe. - Deterministische Stichproben. Ein Filter auf einem aus einem Hash abgeleiteten Wert liefert jedes Mal dieselben Zeilen.
-- Wiederholbare Stichprobe: Transaktions-IDs sind gleichmäßig verteilte Hashes, daher behält '%00' etwa 1 von 256
SELECT COUNT(*) AS sampled
FROM transactions
WHERE block_timestamp >= TIMESTAMP '2024-11-01 00:00:00'
AND block_timestamp < TIMESTAMP '2024-12-01 00:00:00'
AND hash LIKE '%00';
Da Transaktions-Hashes effektiv zufällig sind, wählt die Beibehaltung der Hashes mit der Endung 00 etwa 1 von 256 (0,39 %) aus – bei jeder Engine und jedem Durchlauf dieselben. Der Filter liest weiterhin die Spalte hash für den gesamten Zeitraum. Er macht die Ergebnisse also kleiner und wiederholbar, aber den Scan nicht günstiger. Kombinieren Sie ihn mit einem engen Partitionsfilter.
Inkrementelle Modelle
Überwachungsjobs (eine Beobachtungsliste von Adressen, tägliche Flüsse zu Börsen) sollten nicht bei jeder Ausführung die gesamte Historie neu berechnen. Ein inkrementelles Modell verarbeitet nur neue Blöcke und fügt sie einer abgeleiteten Tabelle hinzu. Die Schwierigkeit ist der aktuelle Chain-Kopf: Neuere Blöcke können reorganisiert werden. Ein Job, der nur Datensätze anhängt, behält daher Zeilen aus Blöcken, die nicht mehr existieren (siehe die Lektion Blöcke und Header als Daten).
-- Inkrementelle Aktualisierung mit Reorg-Lookback; sicher erneut ausführbar
-- :from_block = höchster bereits geladener Block minus 64
-- :to_block = letzter finalisierter Block
DELETE FROM watched_transfers
WHERE block_number >= :from_block;
INSERT INTO watched_transfers
SELECT tt.*
FROM token_transfers AS tt
WHERE tt.block_number BETWEEN :from_block AND :to_block
AND EXISTS (SELECT 1 FROM watchlist AS w
WHERE w.address IN (tt.from_address, tt.to_address));
Bei jeder Ausführung wird der Lookback-Bereich gelöscht und neu geladen. Daher ergibt eine zweimalige Ausführung des Jobs dieselbe Tabelle: Der Job ist idempotent. Wenn nur bis zum finalisierten Block geladen wird (auf Ethereum tritt Finalität normalerweise nach etwa zwei Epochen, also ungefähr 13 Minuten, ein), lassen sich die meisten Reorg-Probleme vermeiden. Bei Bitcoin sind mindestens sechs Blöcke Lookback eine gängige Konvention. EXISTS wird statt eines Joins verwendet, damit ein Transfer zwischen zwei überwachten Adressen nur einmal und nicht zweimal eingefügt wird. Führen Sie beide Anweisungen in einer Datenbanktransaktion aus, damit Leser nie einen teilweise geladenen Bereich sehen. Die Parametersyntax ist unterschiedlich: :name bei vielen Clients, @name in BigQuery, $1 in PostgreSQL, ? in JDBC. Open-Source-Tools wie dbt implementieren dieses Muster als „inkrementelle Modelle“. Ein regelmäßiger vollständiger Neuaufbau erkennt Abweichungen, die bei den inkrementellen Aktualisierungen übersehen wurden.
Versionierte, reproduzierbare Abfragen
Ein Ergebnis ist reproduzierbar, wenn eine andere Person oder Sie selbst ein Jahr später dieselbe Abfrage auf denselben Daten ausführen und dieselben Zeilen erhalten können. Dafür ist Folgendes erforderlich:
- Festgelegter Bereich. Verwenden Sie Blockbereiche, niemals
CURRENT_DATEoder „letzte 30 Tage“. - Versionskontrolle. Speichern Sie SQL in einem Repository und geben Sie den Commit im Bericht an; vermeiden Sie manuell bearbeitete Kopien.
- Deterministische Ausgabe. Sortieren Sie nach einem vollständigen Schlüssel (Blocknummer, danach Log-Index). Ohne
ORDER BYkann sich die Zeilenreihenfolge zwischen Durchläufen ändern. Vermeiden Sie Näherungsfunktionen in berichteten Zahlen. - Datenversionen. Halten Sie den Datensatz, seinen Snapshot oder das Extraktionsdatum sowie die Version veränderlicher Eingaben wie Label- und Preistabellen fest.
- Ein Ausführungsdatensatz, der neben dem Ergebnis gespeichert wird:
{"query": "flows_to_exchanges.sql", "commit": "9c41e2a",
"dataset": "eth_transfers_parquet", "snapshot": "2026-10-01",
"blocks": [21100000, 21107199], "labels_version": "v14",
"engine": "duckdb 1.x", "run_utc": "2026-10-28T14:05:00Z",
"rows": 1832, "result_sha256": "5be1…a7d0"}
Der Hash des exportierten Ergebnisses ermöglicht es einer prüfenden Person, zu bestätigen, dass exakt dieselben Zeilen reproduziert wurden. Die Lektion Den Analysebericht verfassen behandelt, wie diese Datensätze in den Anhang aufgenommen werden.
Ergebnisse anhand eines Nodes validieren
Datensätze werden von Software erstellt, die Fehler aufweisen kann. Bevor eine Zahl in einen Bericht aufgenommen wird, sollten Sie eine Stichprobe davon anhand der Quelle überprüfen (siehe die Lektion Datenqualität, Normalisierung und Provenienz):
- Kontostände. Vergleichen Sie einen in SQL berechneten Kontostand mit
eth_getBalancefür die Adresse im selben Block oder mitbalanceOf(Selector0x70a08231), das übereth_callin diesem Block aufgerufen wird. - Blöcke. Vergleichen Sie gespeicherte Block-Hashes mit den Hashes des Nodes (
getblockhashbei Bitcoin,eth_getBlockByNumberauf EVM-Chains), um Zeilen aus verwaisten Blöcken zu erkennen. - Anzahlen. Vergleichen Sie
transaction_countjedes Blocks mit der Anzahl der tatsächlich intransactionsgeladenen Zeilen. - Kontinuität. Prüfen Sie, dass keine Blocknummern fehlen und der Parent-Hash jedes Blocks mit dem Hash des vorherigen Blocks übereinstimmt.
-- Kontinuitätsprüfung: Lücken bei Blocknummern und fehlerhafte Parent-Links
SELECT number, prev_number, parent_hash, prev_hash
FROM (SELECT number, parent_hash,
LAG(number) OVER (ORDER BY number) AS prev_number,
LAG(hash) OVER (ORDER BY number) AS prev_hash
FROM blocks
WHERE number BETWEEN 21100000 AND 21107199) AS s
WHERE number <> prev_number + 1 -- ein fehlender Block
OR parent_hash <> prev_hash; -- ein Block aus einem anderen Fork
Jede zurückgegebene Zeile weist auf ein Problem hin: einen fehlenden Block oder einen gespeicherten Block, der nicht mit seinem Vorgänger verknüpft ist. Ein leeres Ergebnis für den Blockbereich des Falls sowie übereinstimmende Stichprobenprüfungen ermöglichen die Aussage, dass die Daten validiert wurden.
Prüfungsschwerpunkt: warum spaltenorientierte Engines die gelesenen Spalten in Rechnung stellen und warumSELECT *undLIMITirreführend sind; Partition Pruning und warum der Partitionsfilter literale Wertebereiche auf der Rohspalte verwenden sollte; Blockbereiche in Zeitfenster umwandeln; wann Stichproben akzeptabel sind und worin sich Speicherstichproben und deterministische Hash-Stichproben unterscheiden; inkrementelle Modelle mit Reorg-Lookback, finalisierten Köpfen und idempotentem Löschen und Einfügen; die Bestandteile einer reproduzierbaren Abfrage (festgelegter Blockbereich, Versionskontrolle, deterministische Sortierung, Datenversionen, Ausführungsdatensatz); Validierung anhand eines Nodes mit Prüfungen von Kontoständen, Block-Hashes, Anzahlen und Kontinuität.