Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Die analytische Datenbank ClickHouse verarbeitet eine Vielzahl von verschiedenen Zeichenfolgen, wobei Ressourcen verbraucht werden. Um die Systemleistung zu steigern, werden kontinuierlich neue Optimierungen hinzugefügt. Der Entwickler von ClickHouse, Nikolai Kochetov, spricht über den Datentyp String, einschließlich des neuen Typs LowCardinality, und erklärt, wie man die Verarbeitung von Zeichenfolgen beschleunigen kann.

Video abspielen

Lassen Sie uns zunächst klären, wie man Zeichenfolgen speichern kann.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Wir verfügen über Zeichenfolgendatentypen. Der Typ String eignet sich standardmäßig gut und sollte fast immer verwendet werden. Er hat einen geringen Overhead von 9 Byte pro Zeichenfolge. Wenn wir möchten, dass die Größe der Zeichenfolgen fest und im Voraus bekannt ist, sollten wir besser FixedString verwenden. Dabei kann die gewünschte Anzahl von Bytes festgelegt werden, was ihn ideal für Daten wie IP-Adressen oder Hash-Funktionen macht.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Natürlich gibt es manchmal Engpässe. Angenommen, Sie führen eine Abfrage auf einer Tabelle aus. ClickHouse liest eine beträchtliche Menge an Daten, sagen wir, mit einer Geschwindigkeit von 100 GB/s, während nur wenige Zeichenfolgen verarbeitet werden. Wir haben zwei Tabellen, die nahezu identische Daten speichern. Aus der zweiten Tabelle liest ClickHouse die Daten schneller, verarbeitet jedoch drei Mal weniger Zeichenfolgen pro Sekunde.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Wenn wir die Größe der komprimierten Daten betrachten, wird sie nahezu identisch sein. Tatsächlich sind in den Tabellen die gleichen Daten aufgezeichnet – die ersten eine Milliarde Zahlen – nur im ersten Spaltenformat als UInt64 und im zweiten als String. Dadurch benötigt die zweite Abfrage länger, um die Daten von der Festplatte zu lesen und sie zu dekomprimieren.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Hier ist ein weiteres Beispiel. Angenommen, es gibt eine bekannte Menge von Zeichenfolgen, die konstant auf 1000 oder 10.000 beschränkt ist und sich praktisch nie ändert. Für diesen Fall eignet sich der Datentyp Enum - in ClickHouse gibt es zwei Varianten: Enum8 und Enum16. Durch die Speicherung in Enum können wir Abfragen schnell verarbeiten.

In ClickHouse gibt es Optimierungen für GROUP BY, IN, DISTINCT und spezifische Optimierungen für einige Funktionen, wie z. B. den Vergleich mit einer konstanten Zeichenfolge. Natürlich werden die Zahlen in der Zeichenfolge nicht konvertiert, sondern die konstante Zeichenfolge wird in den Enum-Wert umgewandelt. Danach erfolgt der Vergleich schnell.

Es gibt jedoch auch Nachteile. Selbst wenn wir die exakte Menge von Zeichenfolgen kennen, muss sie manchmal erweitert werden. Wenn eine neue Zeichenfolge hinzukommt, müssen wir einen ALTER-Befehl ausführen.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Der ALTER-Befehl für Enum in ClickHouse ist optimal implementiert. Wir schreiben keine Daten auf die Festplatte um, aber ALTER kann verzögert werden, weil die Enum-Strukturen im Schema der Tabelle gespeichert sind. Daher müssen wir auf Leseanfragen an die Tabelle warten, zum Beispiel.

Es stellt sich die Frage, ob es besser gemacht werden kann. Vermutlich ja. Man könnte die Enum-Struktur nicht im Tabellenschema, sondern in ZooKeeper speichern. Allerdings könnten hierbei Synchronisationsprobleme auftreten. Zum Beispiel könnte eine Replik Daten erhalten haben, während eine andere nicht, und wenn diese eine alte Enum hat, könnte etwas kaputtgehen. (In ClickHouse haben wir fast die nicht-blockierenden ALTER-Anfragen fertiggestellt. Sobald wir sie vollständig implementiert haben, muss man nicht mehr auf Leseanfragen warten.)

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Um sich mit ALTER Enum nicht herumzuschlagen, können externe Dictionnaires in ClickHouse verwendet werden. Ich erinnere daran, dass dies eine Key-Value-Datenstruktur innerhalb von ClickHouse ist, mit der Daten aus externen Quellen abgerufen werden können, zum Beispiel aus MySQL-Tabellen.

Im ClickHouse-Wörterbuch speichern wir zahlreiche verschiedene Strings, während in der Tabelle deren Identifikatoren in Form von Zahlen enthalten sind. Wenn wir einen String benötigen, rufen wir die Funktion dictGet auf und arbeiten mit ihm. Danach sollten wir kein ALTER durchführen. Um etwas in Enum hinzuzufügen, fügen wir es in dieselbe MySQL-Tabelle ein.

Hier treten jedoch weitere Probleme auf. Erstens, die unhandliche Syntax. Wenn wir einen String abrufen möchten, müssen wir dictGet aufrufen. Zweitens, das Fehlen einiger Optimierungen. Ein Vergleich mit einer konstanten Zeichenkette für Wörterbücher ist genauso schnell nicht zu bewerkstelligen.

Es kann auch Probleme mit Aktualisierungen geben. Angenommen, wir haben einen String im Cache-Wörterbuch angefordert, und er ist nicht im Cache gelandet. Dann müssen wir warten, bis die Daten aus einer externen Quelle geladen werden.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Ein allgemeiner Nachteil beider Methoden ist, dass wir alle Schlüssel an einem Ort speichern und sie synchronisieren. Warum also nicht die Wörterbücher lokal speichern? Keine Synchronisierung – keine Probleme. Man kann das Wörterbuch lokal in einem Segment auf der Festplatte speichern. Das heißt, wir haben Insert gemacht und das Wörterbuch aufgezeichnet. Wenn wir mit Daten im Speicher arbeiten, können wir das Wörterbuch entweder in einem Datenblock, in einem Abschnitt der Spalte oder in einem Cache speichern, um die Berechnungen zu beschleunigen.

Wörterbuchkodierung von Zeichenfolgen

So sind wir zur Schaffung eines neuen Datentyps in ClickHouse gekommen – LowCardinality. Dies ist ein Datenformat: wie sie auf die Festplatte geschrieben werden und wie sie gelesen werden, wie sie im Speicher dargestellt werden und wie sie verarbeitet werden.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Auf der Folie gibt es zwei Spalten. Rechts werden die Zeichenfolgen standardmäßig im Typ String gespeichert. Man sieht, dass es sich um verschiedene Modelle von Mobiltelefonen handelt. Links gibt es eine genau gleiche Spalte, aber im Typ LowCardinality. Diese besteht aus einem Wörterbuch mit vielen verschiedenen Zeichenfolgen (Zeichenfolgen aus der rechten Spalte) und einer Liste von Positionen (Zeilennummern).

Mit diesen beiden Strukturen kann die ursprüngliche Spalte wiederhergestellt werden. Es gibt auch einen umgekehrten Index – eine Hash-Tabelle, die hilft, die Position im Wörterbuch anhand der Zeichenfolge zu finden. Diese ist notwendig, um einige Anfragen zu beschleunigen. Zum Beispiel, wenn wir eine Zeichenfolge in unserer Spalte vergleichen, suchen oder zusammenführen möchten.

LowCardinality ist ein parametrisierter Datentyp. Er kann entweder eine Zahl sein, etwas, das als Zahl gespeichert ist, oder eine Zeichenfolge, oder auch Nullable von diesen.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Ein Merkmal von LowCardinality ist, dass es für bestimmte Funktionen gespeichert werden kann. Auf der Folie ist ein Beispielabfrage zu sehen. In der ersten Zeile habe ich eine Spalte vom Typ LowCardinality vom Typ String erstellt und sie S genannt. Anschließend habe ich nach ihrem Namen gefragt – ClickHouse sagte, dass es sich um LowCardinality von String handelt. Das ist korrekt.

Die dritte Zeile ist fast identisch, nur dass wir die Funktion length aufgerufen haben. In ClickHouse gibt die Funktion length den Datentyp UInt64 zurück. Aber jetzt haben wir LowCardinality von UInt64. Was ist der Sinn?

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Im Wörterbuch wurden die Namen von Mobiltelefonen gespeichert, und wir haben die Funktion length angewendet. Jetzt haben wir ein ähnliches Wörterbuch, das nur aus Zahlen besteht – das sind die Längen der Strings. Die Spalte mit den Positionen hat sich nicht verändert. Letztendlich haben wir weniger Daten verarbeitet und somit Zeit bei der Abfrage gespart.

Es könnten auch andere Optimierungen vorgenommen werden, wie zum Beispiel die Hinzufügung eines einfachen Caches. Bei der Berechnung des Funktionswerts könnte man ihn speichern und ein gleichwertiges Ergebnis liefern, ohne ihn erneut zu berechnen.

Eine Optimierung von GROUP BY kann ebenfalls vorgenommen werden, da unsere Spalte mit dem Wörterbuch bereits teilweise aggregiert ist – so können die Hash-Wertberechnungen schneller erfolgen und der Bucket, in den die nächste Zeile eingefügt werden soll, schneller gefunden werden. Außerdem können einige aggregierte Funktionen spezialisiert werden, wie z. B. uniq, da dort nur das Wörterbuch gesendet werden kann, während die Positionen unberührt bleiben – das sorgt für eine schnellere Verarbeitung. Die ersten beiden Optimierungen haben wir bereits in ClickHouse implementiert.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Was ist, wenn wir eine Spalte mit unserem Datentyp erstellen und viele schlechte, verschiedene Zeilen einfügen? Wird unser Speicher überlaufen? Nein, dafür gibt es in ClickHouse zwei spezielle Einstellungen. Die erste ist low_cardinality_max_dictionary_size. Das ist die maximale Größe des Wörterbuchs, die auf die Festplatte geschrieben werden kann. Die Einfügung erfolgt folgendermaßen: Wenn wir Daten einfügen, erhalten wir einen Datenstrom von Zeilen, aus denen wir ein großes gemeinsames Wörterbuch bilden. Wenn das Wörterbuch größer wird als der Wert der Konfiguration, schreiben wir das aktuelle Wörterbuch auf die Festplatte, während die anderen Zeilen „nebenbei“, in der Nähe der Indizes, gespeichert werden. So wird nie ein großes Wörterbuch neu gezählt und wir vermeiden Speicherprobleme.

Die zweite Einstellung nennt sich low_cardinality_use_single_dictionary_for_part. Stellen Sie sich vor, dass unser Wörterbuch in dem vorherigen Schema überlaufen ist, als wir Daten eingefügt haben, und wir es auf die Festplatte geschrieben haben. Die Frage ist, warum wir jetzt nicht ein weiteres solches Wörterbuch bilden sollen?

Wenn es überläuft, werden wir es erneut auf die Festplatte schreiben und ein drittes zu bilden beginnen. Diese Einstellung deaktiviert standardmäßig diese Möglichkeit.

Tatsächlich können mehrere Wörterbücher nützlich sein, wenn wir eine große Anzahl von Zeilen einfügen wollen, aber versehentlich 'Müll' eingefügt haben. Angenommen, wir haben zunächst schlechte Zeilen eingefügt und dann gute. Dann wird das Wörterbuch in viele kleine Wörterbücher aufgeteilt. Einige davon enthalten Müll, aber die letzten beinhalten gute Zeilen. Und wenn wir sagen wir nur das letzte Granulat lesen, wird auch alles schnell funktionieren.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Bevor wir über die Vorteile von LowCardinality sprechen, möchte ich gleich sagen, dass wir wahrscheinlich keine Reduzierung der Daten auf der Festplatte erreichen werden (obwohl das möglich sein könnte), da ClickHouse die Daten komprimiert. Es gibt eine Standardoption - LZ4. Man kann auch ZSTD zur Kompression nutzen. Beide Algorithmen nutzen jedoch bereits eine Wörterbuchkompression, weshalb unser externes Wörterbuch in ClickHouse nicht sehr hilfreich sein wird.

Um meine Aussagen zu untermauern, habe ich einige Daten aus der Metrik – String, LowCardinality(String) und Enum – genommen und sie in verschiedene Datentypen gespeichert. Das Ergebnis waren drei Spalten mit insgesamt einer Milliarde Zeilen. In der ersten Spalte, CodePage, gibt es nur 62 Werte. Dabei sieht man, dass LowCardinality(String) uns eine bessere Kompression liefert. String ist etwas schlechter, was wahrscheinlich daran liegt, dass die Strings kurz sind, wir deren Längen speichern, und sie viel Platz einnehmen und schlecht komprimiert werden.

Wenn man PhoneModel betrachtet, gibt es 48.000 – das ist bereits mehr, und die Unterschiede zwischen String und LowCardinality(String) sind kaum spürbar. Bei den URLs haben wir ebenfalls nur 2 GB gespart – ich denke, darauf sollte man sich nicht verlassen.

Bewertung der Arbeitsgeschwindigkeit

Optimierung von Abfragen in ClickHouse. Bericht von Yandex
Link zur Folie

Nun bewerten wir die Arbeitsgeschwindigkeit. Um dies zu tun, habe ich einen Datensatz mit Beschreibungen von Taxifahrten in New York verwendet. Er ist verfügbar auf GitHub. Dort sind etwas mehr als eine Milliarde Fahrten verzeichnet. Es sind Standort, Start- und Endzeit der Fahrt, Zahlungsmethode, Anzahl der Passagiere und sogar die Art des Taxis - grün, gelb und Uber - aufgeführt.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Ich habe die erste Abfrage ziemlich einfach gestaltet - ich habe gefragt, wo am häufigsten Taxis bestellt werden. Dazu muss der Standort, von dem aus das Taxi bestellt wurde, gruppiert und mit der Funktion count gezählt werden. ClickHouse liefert dafür einige Ergebnisse.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Um die Geschwindigkeit der Anfragenbearbeitung zu messen, habe ich drei Tabellen mit identischen Daten erstellt, jedoch drei verschiedene Datentypen für unseren Startstandort verwendet - String, LowCardinality und Enum. LowCardinality und Enum waren fünfmal schneller als String. Enum ist schneller, weil es mit Zahlen arbeitet. LowCardinality schneller, weil es eine Optimierung für GROUP BY implementiert.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Lassen Sie uns die Abfrage noch komplizierter gestalten - wir fragen, wo sich der beliebteste Park in New York befindet. Auch hier messen wir, wo am häufigsten Taxis bestellt werden, filtern jedoch nur die Standorte, die das Wort „Park“ enthalten. Zusätzlich fügen wir die Funktion like hinzu.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Wenn wir die Zeit betrachten, sehen wir, dass Enum plötzlich langsamer wird. Es arbeitet sogar langsamer als der Standard-Datentyp String. Das liegt daran, dass die Funktion 'like' für Enum überhaupt nicht optimiert ist. Wir müssen unsere Strings von Enum in normale Strings umwandeln – wir erledigen also mehr Arbeit. LowCardinality(String) ist ebenfalls standardmäßig nicht optimiert, aber dort arbeitet 'like' mit einem Wörterbuch, was die Abfrage im Vergleich zu String beschleunigt.

Bei der Arbeit mit Enum gibt es ein grundlegenderes Problem. Wenn wir es optimieren möchten, müssen wir dies an jedem Ort im Code tun. Angenommen, wir haben eine neue Funktion geschrieben – wir müssen unbedingt eine Optimierung für Enum finden. Bei LowCardinality ist alles standardmäßig optimiert.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Lassen Sie uns die letzte Abfrage betrachten, die künstlicher ist. Wir berechnen einfach die Hashfunktion für unseren Standort. Die Hashfunktion ist eine ziemlich langsame Abfrage, die lange dauert, daher wird alles um das Dreifache langsamer.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

LowCardinality funktioniert immer noch schneller, auch wenn hier keine Filterung möglich ist. Das liegt daran, dass unsere Funktionen nur im Wörterbuch arbeiten. Die Hash-Berechnungsfunktion hat ein Argument – sie kann weniger Daten verarbeiten und ebenfalls LowCardinality zurückgeben.

Optimierung von Abfragen in ClickHouse. Bericht von Yandex

Unser globales Ziel ist es, eine Geschwindigkeit zu erreichen, die in allen Fällen mindestens so hoch ist wie bei String, und die Beschleunigungen beizubehalten. Und vielleicht werden wir eines Tages String durch LowCardinality ersetzen, Sie aktualisieren ClickHouse, und alles läuft dann ein bisschen schneller.

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster