Zeichenfolgenoptimierung in ClickHouse. Yandex-Bericht

Das ClickHouse-Analysedatenbankmodul verarbeitet viele verschiedene Zeilen und verbraucht Ressourcen. Um das System zu beschleunigen, werden ständig neue Optimierungen hinzugefügt. Der ClickHouse-Entwickler Nikolay Kochetov spricht über den String-Datentyp, einschließlich des neuen Typs LowCardinality, und erklärt, wie die Arbeit mit Strings beschleunigt werden kann.


- Lassen Sie uns zunächst sehen, wie Sie Zeichenfolgen speichern können.



Wir haben String-Datentypen. String funktioniert standardmäßig gut, es sollte fast immer verwendet werden. Es hat einen kleinen Overhead - 9 Bytes pro Zeile. Wenn die Zeilengröße im Voraus festgelegt und bekannt sein soll, ist es besser, FixedString zu verwenden. Darin können Sie die Anzahl der benötigten Bytes festlegen. Dies ist praktisch für Daten wie IP-Adressen oder Hash-Funktionen.



Natürlich verlangsamt sich manchmal etwas. Angenommen, Sie stellen eine Abfrage für eine Tabelle. ClickHouse liest eine ziemlich große Datenmenge, beispielsweise mit einer Geschwindigkeit von 100 GB / s, wobei nur wenige Zeilen verarbeitet werden. Wir haben zwei Tabellen, in denen fast dieselben Daten gespeichert sind. ClickHouse liest Daten aus der zweiten Tabelle mit einer höheren Geschwindigkeit, liest jedoch dreimal weniger Zeilen pro Sekunde.



Wenn wir uns die Größe der komprimierten Daten ansehen, werden sie fast gleich sein. Tatsächlich werden dieselben Daten in die Tabellen geschrieben - die ersten Milliarden Zahlen - nur in der ersten Spalte werden sie in Form von UInt64 und in der zweiten Spalte in String geschrieben. Aus diesem Grund liest die zweite Abfrage Daten länger von der Festplatte und dekomprimiert sie.



Hier ist ein weiteres Beispiel. Angenommen, es gibt einen vorgegebenen Satz von Linien, der auf eine Konstante von 1000 oder 10.000 begrenzt ist und sich fast nie ändert. In diesem Fall ist der Datentyp Enum für uns geeignet. In ClickHouse gibt es zwei davon - Enum8 und Enum16. Aufgrund der Speicherung in Enum bearbeiten wir Anfragen schnell.

ClickHouse bietet Beschleunigungen für GROUP BY, IN, DISTINCT und Optimierungen für einige Funktionen, z. B. zum Vergleich mit einer konstanten Zeichenfolge. Natürlich werden die Zahlen in der Zeichenfolge nicht konvertiert, im Gegenteil, die konstante Zeichenfolge wird in den Wert Enum konvertiert. Danach wird alles schnell verglichen.

Es gibt aber auch Nachteile. Selbst wenn wir den genauen Satz von Zeilen kennen, muss er manchmal wieder aufgefüllt werden. Eine neue Linie ist angekommen - wir müssen ALTER machen.



ALTER für Enum in ClickHouse ist optimal implementiert. Wir überschreiben die Daten auf der Festplatte nicht, aber ALTER kann langsamer werden, da Enum-Strukturen im Schema der Tabelle selbst gespeichert sind. Daher müssen wir beispielsweise auf Leseanforderungen aus der Tabelle warten.

Die Frage ist, kann man es besser machen? Vielleicht ja. Sie können die Enum-Struktur nicht im Tabellenschema, sondern in ZooKeeper speichern. Es können jedoch Synchronisationsprobleme auftreten. Beispielsweise hat ein Replikat Daten empfangen, das andere nicht, und wenn es eine alte Aufzählung enthält, wird etwas kaputt gehen. (In ClickHouse haben wir die nicht blockierenden ALTER-Anforderungen fast abgeschlossen. Wenn wir sie vollständig abgeschlossen haben, müssen wir nicht auf



Leseanforderungen warten.) Um sich nicht mit ALTER Enum herumzuschlagen, können Sie externe ClickHouse-Wörterbücher verwenden. Ich möchte Sie daran erinnern, dass dies eine Schlüsselwert-Datenstruktur in ClickHouse ist, mit der Sie Daten aus externen Quellen abrufen können, beispielsweise aus MySQL-Tabellen.

Im ClickHouse-Wörterbuch speichern wir viele verschiedene Zeilen und in der Tabelle deren Bezeichner in Form von Zahlen. Wenn wir einen String benötigen, rufen wir die Funktion dictGet auf und arbeiten damit. Danach sollten wir ALTER nicht mehr machen. Um Enum etwas hinzuzufügen, fügen wir dies in dieselbe MySQL-Tabelle ein.

Es gibt aber noch andere Probleme. Erstens die umständliche Syntax. Wenn wir einen String erhalten wollen, müssen wir dictGet aufrufen. Zweitens das Fehlen einiger Optimierungen. Ein Vergleich mit der konstanten Zeichenfolge für Wörterbücher ist ebenfalls nicht schnell möglich.

Möglicherweise treten weiterhin Probleme mit dem Update auf. Angenommen, wir haben eine Zeile im Cache-Wörterbuch angefordert, die jedoch nicht in den Cache gelangt ist. Dann müssen wir warten, bis die Daten von einer externen Quelle geladen sind.



Ein gemeinsamer Nachteil beider Methoden ist, dass wir alle Schlüssel an einem Ort speichern und synchronisieren. Warum also nicht Wörterbücher lokal speichern? Keine Synchronisation - kein Problem. Sie können das Wörterbuch lokal in einem Stück auf der Festplatte speichern. Das heißt, wir haben Einfügen, ein Wörterbuch aufgenommen. Wenn wir mit Daten im Speicher arbeiten, können wir ein Wörterbuch entweder in einen Datenblock oder in einen Teil einer Spalte oder in einen Cache schreiben, um die Berechnungen zu beschleunigen.

Vokabel-String-Codierung


So kamen wir zur Erstellung eines neuen Datentyps in ClickHouse - LowCardinality. Dies ist ein Format zum Speichern von Daten: wie sie auf die Festplatte geschrieben und gelesen werden, wie sie im Speicher dargestellt werden und wie sie verarbeitet werden.



Auf der Folie befinden sich zwei Spalten. Auf der rechten Seite werden Zeichenfolgen standardmäßig im Zeichenfolgentyp gespeichert. Es ist zu sehen, dass dies eine Art Handy-Modell ist. Links befindet sich genau dieselbe Spalte, nur im Typ LowCardinality. Es besteht aus einem Wörterbuch mit vielen verschiedenen Zeilen (Zeilen aus der rechten Spalte) und einer Liste von Positionen (Zeilennummern).

Mit diesen beiden Strukturen können Sie die ursprüngliche Spalte wiederherstellen. Es gibt auch einen umgekehrten inversen Index - eine Hash-Tabelle, mit der Sie die Position im Wörterbuch zeilenweise finden können. Es wird benötigt, um einige Abfragen zu beschleunigen. Wenn wir beispielsweise vergleichen möchten, suchen Sie in unserer Spalte nach einer Zeile oder führen Sie sie zusammen.

LowCardinality ist ein parametrischer Datentyp. Es kann entweder eine Zahl oder etwas sein, das als Zahl oder als Zeichenfolge gespeichert ist, oder Nullable von ihnen.



Die Besonderheit von LowCardinality ist, dass es für einige Funktionen gespeichert werden kann. Eine Beispielanforderung wird auf der Folie angezeigt. In der ersten Zeile habe ich aus String eine Spalte vom Typ LowCardinality mit dem Namen S erstellt. Dann habe ich sie nach ihrem Namen gefragt - ClickHouse sagte, es sei LowCardinality aus String. Alles ist richtig.

Die dritte Zeile ist fast dieselbe, nur haben wir die Längenfunktion genannt. In ClickHouse gibt die Längenfunktion den UInt64-Datentyp zurück. Aber wir haben LowCardinality von UInt64. Was ist der Punkt?



Die Namen der Handys wurden im Wörterbuch gespeichert, wir haben die Längenfunktion angewendet. Jetzt haben wir ein ähnliches Wörterbuch, das nur aus Zahlen besteht. Dies sind die Längen der Zeichenfolgen. Die Spalte mit den Positionen hat sich nicht geändert. Infolgedessen haben wir weniger Daten verarbeitet, die auf Anfrage gespeichert wurden.

Möglicherweise gibt es andere Optimierungen, z. B. das Hinzufügen eines einfachen Caches. Wenn Sie den Wert einer Funktion berechnen, können Sie sich daran erinnern und ihn gleich machen. Berechnen Sie ihn nicht neu.

Die Optimierung kann auch GROUP BY erfolgen, da unsere Spalte mit dem Wörterbuch bereits teilweise aggregiert ist. Sie können den Wert der Hash-Funktionen schnell berechnen und grob den Bucket finden, in dem die nächste Zeile eingefügt werden soll. Sie können sich auch auf einige Aggregatfunktionen spezialisieren, z. B. uniq, da Sie nur ein Wörterbuch an dieses senden und die Positionen unberührt lassen können. Auf diese Weise funktioniert alles schneller. Die ersten beiden Optimierungen haben wir bereits zu ClickHouse hinzugefügt.



Was aber, wenn wir eine Spalte mit unserem Datentyp erstellen und viele fehlerhafte verschiedene Zeilen einfügen? Ist unser Gedächtnis voll? Nein, dafür gibt es in ClickHouse zwei spezielle Einstellungen. Der erste ist low_cardinality_max_dictionary_size. Dies ist die maximale Größe eines Wörterbuchs, das auf die Festplatte geschrieben werden kann. Das Einfügen erfolgt wie folgt: Wenn wir die Daten einfügen, kommt ein Strom von Zeilen zu uns, aus denen wir ein großes allgemeines Wörterbuch bilden. Wenn das Wörterbuch größer als der Einstellungswert wird, schreiben wir das aktuelle Wörterbuch auf die Festplatte und den Rest der Zeilen irgendwo auf der Seite neben den Indizes. Infolgedessen werden wir niemals ein großes Wörterbuch wiedergeben und keine Speicherprobleme bekommen.

Die zweite Einstellung heißt low_cardinality_use_single_dictionary_for_part. Stellen Sie sich vor, dass im vorherigen Schema beim Einfügen der Daten unser Wörterbuch voll war und wir es auf die Festplatte geschrieben haben. Es stellt sich die Frage, warum nicht jetzt ein anderes genau das gleiche Wörterbuch bilden?

Wenn es überläuft, schreiben wir es erneut auf die Festplatte und beginnen, eine dritte zu bilden. Diese Einstellung deaktiviert diese Funktion standardmäßig.

Tatsächlich können viele Wörterbücher nützlich sein, wenn wir eine Reihe von Zeilen einfügen möchten, aber versehentlich "Müll" eingefügt haben. Angenommen, wir haben zuerst die schlechten und dann die guten Zeilen eingefügt. Dann wird das Wörterbuch in viele kleine Wörterbücher unterteilt. Einige von ihnen werden mit Müll sein, aber letztere werden mit guten Linien sein. Und wenn wir zum Beispiel nur das letzte Pellet lesen, funktioniert auch alles schnell.



Bevor ich auf die Vorteile von LowCardinality eingehe, möchte ich gleich sagen, dass es unwahrscheinlich ist, dass Daten auf der Festplatte reduziert werden (obwohl dies passieren kann), da ClickHouse die Daten komprimiert. Es gibt eine Standardoption - LZ4. Sie können die Komprimierung auch mit ZSTD durchführen. Beide Algorithmen implementieren jedoch bereits die Wörterbuchkomprimierung, sodass unser externes ClickHouse-Wörterbuch nicht viel hilft.

Um nicht unbegründet zu sein, habe ich einige Daten aus der Metrik - String, LowCardinality (String) und Enum - genommen und in verschiedenen Datentypen gespeichert. Es stellte sich heraus, dass drei Spalten, in denen eine Milliarde Zeilen geschrieben sind. Die erste Spalte, CodePage, enthält insgesamt 62 Werte. Und Sie können sehen, dass sie in LowCardinality (String) besser zusammengedrückt wurden. Die Saite ist etwas schlechter, aber dies ist höchstwahrscheinlich darauf zurückzuführen, dass die Saiten kurz sind, wir ihre Längen speichern und sie viel Platz beanspruchen und nicht gut komprimieren.

Wenn Sie PhoneModel verwenden, gibt es mehr als 48.000 davon, und es gibt fast keine Unterschiede zwischen String und LowCardinality (String). Für die URL haben wir auch nur 2 GB gespeichert - ich denke, Sie sollten sich nicht darauf verlassen.

Schätzung der Arbeitsgeschwindigkeit



Link von der Folie

Lassen Sie uns nun die Arbeitsgeschwindigkeit bewerten. Um es auszuwerten, habe ich einen Datensatz verwendet, der Taxifahrten in New York beschreibt. Esistauf GitHubverfügbar. Es hat etwas mehr als eine Milliarde Reisen. Es zeigt den Ort, die Start- und Endzeiten der Reise, die Zahlungsmethode, die Anzahl der Passagiere und sogar die Art des Taxis - grün, gelb und Uber.



Ich habe die erste Anfrage ganz einfach gemacht - ich habe gefragt, wo Taxis am häufigsten bestellt werden. Dazu müssen Sie den Ort, an dem Sie bestellt haben, nehmen, GROUP BY darauf erstellen und die Zählfunktion berechnen. Hier gibt ClickHouse etwas heraus.



Um die Geschwindigkeit der Abfrageverarbeitung zu messen, habe ich drei Tabellen mit denselben Daten erstellt, aber drei verschiedene Datentypen für unseren Startort verwendet - String, LowCardinality und Enum. LowCardinality und Enum sind fünfmal schneller als String. Enum ist schneller, weil es mit Zahlen funktioniert. LowCardinality - weil die GROUP BY-Optimierung implementiert ist.



Lassen Sie uns die Anfrage komplizieren - fragen Sie, wo sich der beliebteste Park in New York befindet. Wir werden dies wieder daran messen, wo am häufigsten ein Taxi bestellt wird, aber gleichzeitig werden wir nur die Orte herausfiltern, an denen das Wort „Park“ verfügbar ist. Fügen Sie auch eine ähnliche Funktion hinzu.



Wir schauen auf die Zeit - wir sehen, dass Enum plötzlich langsamer wurde. Und es arbeitet noch langsamer als der Standard-String-Datentyp. Dies liegt daran, dass die Like-Funktion für Enum überhaupt nicht optimiert ist. Wir müssen unsere Zeilen von Enum in reguläre Zeilen umwandeln - wir machen mehr Arbeit. LowCardinality (String) ist ebenfalls nicht standardmäßig optimiert, funktioniert aber wie dort im Wörterbuch, sodass die Abfrage schneller als String ist.

Es gibt ein globaleres Problem mit Enum. Wenn wir es optimieren wollen, müssen wir es an jeder Stelle des Codes tun. Angenommen, wir haben eine neue Funktion geschrieben - Sie müssen sich auf jeden Fall Optimierungen für Enum einfallen lassen. Und in LowCardinality ist standardmäßig alles optimiert.



Schauen wir uns die letzte Anfrage an, künstlicher. Wir berechnen einfach die Hash-Funktion von unserem Standort aus. Die Hash-Funktion ist eine ziemlich langsame Anfrage, sie dauert lange, daher wird alles dreimal langsamer.



LowCardinality ist immer noch schneller, obwohl keine Filterung erfolgt. Dies liegt daran, dass unsere Funktionen nur im Wörterbuch funktionieren. Die Hash-Berechnungsfunktion hat ein Argument: Sie kann weniger Daten verarbeiten und auch LowCardinality zurückgeben.



Unser globaler Plan ist es, in jedem Fall eine Geschwindigkeit zu erreichen, die nicht niedriger als die von String ist, und Beschleunigung zu sparen. Und vielleicht werden wir eines Tages String durch LowCardinality ersetzen, Sie werden ClickHouse aktualisieren und alles wird für Sie etwas schneller funktionieren.

All Articles