In 4 Sekunden um die Welt im Columnstore (Teil 1)

In diesem Artikel werde ich erwĂ€gen, die Geschwindigkeit von Berichten zu erhöhen. Mit einem Bericht meine ich jede Abfrage an eine Datenbank, die Aggregatfunktionen verwendet. Außerdem werde ich auf Fragen eingehen, die sich auf die Ressourcen beziehen, die fĂŒr die Erstellung und UnterstĂŒtzung von Berichten sowohl von Menschen als auch von Maschinen aufgewendet werden.

In den Beispielen werde ich einen Datensatz verwenden, der 52.608.000 DatensÀtze enthÀlt.

Am Beispiel nicht schwieriger analytischer Reserven werde ich zeigen, dass selbst ein schwacher Computer ohne großen Aufwand zu einem guten Werkzeug fĂŒr die Analyse einer „anstĂ€ndigen“ Datenmenge werden kann.

Nachdem wir keine komplizierten Experimente durchgefĂŒhrt haben, werden wir feststellen, dass eine regulĂ€re Tabelle keine geeignete Quelle fĂŒr analytische Abfragen ist.

Wenn der Leser die AbkĂŒrzungen OLTP und OLAP leicht entschlĂŒsseln kann, kann es sinnvoll sein, direkt zum Abschnitt Columnstore zu wechseln

Zwei AnsÀtze zum Arbeiten mit Daten


Hier werde ich mich kurz fassen, weil Zu diesem Thema gibt es im Internet mehr als genug Informationen.

Auf höchster Ebene gibt es also nur zwei AnsĂ€tze fĂŒr die Arbeit mit Daten: OLTP und OLAP.

OLTP - kann als sofortige Transaktionsverarbeitung ĂŒbersetzt werden. In der Tat sprechen wir ĂŒber die Online-Verarbeitung von kurzen Transaktionen, die mit einer kleinen Datenmenge arbeiten. Zum Beispiel das Aufzeichnen, Aktualisieren oder Löschen einer Bestellung. In den allermeisten FĂ€llen handelt es sich bei einer Bestellung um eine Ă€ußerst kleine Datenmenge, bei deren Verarbeitung Sie keine Angst vor den langen Sperren haben mĂŒssen, die das moderne RDBMS auferlegt.

OLAP - kann als analytische Verarbeitung einer großen Anzahl von Transaktionen gleichzeitig ĂŒbersetzt werden. Jeder Bericht verwendet diesen speziellen Ansatz, da der Bericht in den allermeisten FĂ€llen zusammenfassende, aggregierte Zahlen fĂŒr bestimmte Abschnitte erstellt.

Jeder Ansatz hat seine eigene Technologie. FĂŒr OLTP ist es beispielsweise PostgreSQL und fĂŒr OLAP Microsoft SQL Server Analysis Services. WĂ€hrend PostgresSQL ein bekanntes Format zum Speichern von Daten in Tabellen verwendet, wurden fĂŒr OLAP verschiedene Formate erfunden. Dies sind mehrdimensionale Tabellen, ein mit SchlĂŒssel-Wert-Paaren gefĂŒllter Eimer und mein bevorzugter Spaltenspeicher. Über letzteres im Detail weiter unten.

Warum sind zwei AnsÀtze erforderlich?


Es wurde festgestellt, dass jedes Data Warehouse frĂŒher oder spĂ€ter zwei Arten von Belastungen ausgesetzt ist: hĂ€ufiges Lesen (natĂŒrlich auch Schreiben und Aktualisieren) extrem kleiner Datenmengen und seltenes Lesen, aber sehr große Datenmengen. In der Tat ist dies eine AktivitĂ€t, zum Beispiel der Abendkasse und des Leiters. Die Kasse, die den ganzen Tag arbeitet, fĂŒllt den Speicher mit kleinen Datenblöcken, wĂ€hrend am Ende des Tages das angesammelte Volumen, wenn das GeschĂ€ft gut lĂ€uft, eine beeindruckende GrĂ¶ĂŸe erreicht. Im Gegenzug möchte der Manager am Ende des Tages wissen, wie viel Geld die Abendkasse pro Tag verdient hat.

In OLTP gibt es also Tabellen und Indizes. Diese beiden Tools eignen sich hervorragend zum Aufzeichnen von KassenaktivitĂ€ten mit allen Details. Indizes bieten eine schnelle Suche nach einer zuvor aufgezeichneten Bestellung, sodass das Ändern einer Bestellung einfach ist. Um jedoch die BedĂŒrfnisse des Leiters zu befriedigen, mĂŒssen wir die gesamte pro Tag gesammelte Datenmenge berĂŒcksichtigen. DarĂŒber hinaus benötigt der Manager in der Regel nicht alle Details aller Bestellungen. Was er wirklich wissen muss, ist, wie viel Geld die Abendkasse im Allgemeinen verdient hat. Es spielt keine Rolle, wo sich das TicketbĂŒro befand, wann es eine Mittagspause gab, wer dafĂŒr arbeitete usw. OLAP existiert dann, so dass das System in kurzer Zeit die Frage beantworten kann, wie viel das Unternehmen insgesamt verdient hat, ohne jede Bestellung und alle Details nacheinander zu lesen. Kann OLAP dieselben Tabellen und Indizes wie OLTP verwenden? Die Antwort ist nein, zumindest sollte es nicht. Erstens, weil OLAP nicht alle in den Tabellen aufgezeichneten Details benötigt. Dieses Problem wird gelöst, indem Daten in anderen Formaten als zweidimensionalen Tabellen gespeichert werden. Zweitens werden die analysierten Informationen hĂ€ufig ĂŒber verschiedene Tabellen verteilt, was zu mehreren Assoziationen fĂŒhrt, einschließlich Assoziationen vom Typ Self-Join. Um dieses Problem zu lösen, entwickeln sie in der Regel ein spezielles Datenbankschema. Dieses Schema ist sowohl fĂŒr das OLAP-Laden als auch fĂŒr das normale normalisierte Schema fĂŒr das OLTP-Laden optimiert.

Was passiert, wenn OLAP ein OLTP-Schema verwendet?


TatsĂ€chlich habe ich diesen Abschnitt eingefĂŒhrt, damit dieser Artikel eindeutig meine eigenen Anforderungen an das Format eines solchen Materials erfĂŒllt, d. H. Problem, Lösung, Schlussfolgerung.

Wir listen eine Reihe von Nachteilen bei der Verwendung von OLTP-Schemata fĂŒr die Datenanalyse auf.

  • Zu viele Indizes.

    Oft mĂŒssen Sie spezielle Indizes erstellen, um Berichte zu unterstĂŒtzen. Diese Indizes implementieren ein OLAP-Datenspeicherschema. Sie werden vom OLTP-Teil der Anwendung nicht verwendet, wĂ€hrend sie belastet werden, stĂ€ndige UnterstĂŒtzung erfordern und Speicherplatz beanspruchen.
  • Die Menge der gelesenen Daten ĂŒberschreitet die erforderliche Menge.
  • Fehlen eines klaren Datenschemas.

    Tatsache ist, dass die von Berichten in einer einzigen Form ĂŒbermittelten Informationen hĂ€ufig in verschiedenen Tabellen verteilt sind. Solche Informationen erfordern eine stĂ€ndige Transformation im laufenden Betrieb. Das einfachste Beispiel ist die Höhe der Einnahmen, die aus Bargeld und Sachgeld besteht. Ein weiteres bemerkenswertes Beispiel sind Datenhierarchien. Weil Die Anwendungsentwicklung ist fortschrittlich und es ist nicht immer bekannt, was in Zukunft benötigt wird. Dieselbe Bedeutungshierarchie kann in verschiedenen Tabellen gespeichert werden. WĂ€hrend die On-the-Fly-Erfassung in OLAP aktiv genutzt wird, sind dies etwas andere Dinge.
  • ÜbermĂ€ĂŸige KomplexitĂ€t der Abfragen.

    Weil Ein OLTP-Schema unterscheidet sich von einem OLAP. Es wird eine stark verwandte Softwareschicht benötigt, die das OLTP-Datenschema in die richtige Form bringt.
  • KomplexitĂ€t von Support, Debugging und Entwicklung.

    Im Allgemeinen können wir sagen, dass es umso schwieriger ist, die Codebasis in einem gesunden Zustand zu halten, je komplexer sie ist. Dies ist ein Axiom.
  • Die KomplexitĂ€t der Testabdeckung.

    Viele Kopien sind aufgrund von Diskussionen darĂŒber, wie eine Datenbank mit allen Testskripten gefĂŒllt werden kann, fehlerhaft. Es ist jedoch besser zu sagen, dass die Aufgabe, Tests durchzufĂŒhren, durch ein einfacheres Datenschema um ein Vielfaches vereinfacht wird.
  • Endloses Performance-Debugging.

    Es besteht eine hohe Wahrscheinlichkeit, dass der Benutzer einen Bericht bestellt, der fĂŒr den Datenbankserver „schwer“ ist. Diese Wahrscheinlichkeit nimmt mit der Zeit zu. Es sollte beachtet werden, dass OLAP ebenfalls fĂŒr dieses Problem anfĂ€llig ist, aber im Gegensatz zu OLTP ist die OLAP-Ressource in dieser Angelegenheit viel höher.

Columnstore

Dieser Artikel konzentriert sich auf das Columnstore-Speicherformat, jedoch ohne Details auf niedriger Ebene. Andere oben erwĂ€hnte Formate verdienen ebenfalls Aufmerksamkeit, aber dies ist ein Thema fĂŒr einen anderen Artikel.

TatsĂ€chlich ist das Columnstore-Format seit 30 Jahren bekannt. Es wurde jedoch bis vor kurzem nicht im RDBMS implementiert. Das Wesentliche beim Spaltenspeicher ist, dass Daten nicht in Zeilen, sondern in Spalten gespeichert werden. Das heißt, Auf einer Seite (alle bekannt 8 KB) zeichnet der Server nur Daten eines Feldes auf. Und so mit jedem Feld in der Tabelle der Reihe nach. Dies ist notwendig, damit Sie keine zusĂ€tzlichen Informationen lesen mĂŒssen. Stellen wir uns eine Tabelle mit 10 Feldern und einer Abfrage vor, in der nur ein Feld in der SELECT-Anweisung angegeben ist. Wenn es sich um eine regulĂ€re Tabelle handelt, die in einem zeilenbasierten Format gespeichert ist, muss der Server alle 10 Felder lesen, gibt jedoch gleichzeitig nur eines zurĂŒck. Es stellte sich heraus, dass der Server neunmal mehr Informationen las als nötig. Columnstore löst dieses Problem vollstĂ€ndig, weil Im Speicherformat können Sie nur ein geordnetes Feld lesen. All dies geschieht, weil die Speichereinheit in einem RDBMS eine Seite ist. Das heißt, Der Server schreibt und liest immer mindestens eine Seite. Die Frage ist nur, wie viele Felder darauf vorhanden sind.

Wie Columnstore wirklich helfen kann


Um dies zu beantworten, muss man genaue Zahlen haben. Lass sie uns holen. Aber welche Zahlen können ein genaues Bild ergeben?

  1. Die Menge an Speicherplatz.
  2. Abfrageleistung.
  3. Fehlertoleranz.
  4. Einfache Implementierung.
  5. Welche neuen FĂ€higkeiten sollte ein Entwickler haben, um mit neuen Strukturen zu arbeiten?

Speicherplatz


Lassen Sie uns eine einfache Tabelle erstellen, sie mit Daten fĂŒllen und ĂŒberprĂŒfen, wie viel Speicherplatz dafĂŒr benötigt wird.

create foreign table cstore_table ( trd date, org int, op int, it int, wh int, m1 numeric(32, 2), m2 numeric(32, 2), m3 numeric(32, 2), m4 numeric(32, 2), m5 numeric(32, 2) ) server cstore_server options(compression 'pglz'); 

Wie Sie bemerkt haben, habe ich eine externe Tabelle erstellt. Tatsache ist, dass PostgreSQL keine integrierte Columnstore-UnterstĂŒtzung bietet. PostgreSQL verfĂŒgt jedoch ĂŒber ein leistungsstarkes System fĂŒr Erweiterungen. Eine davon ermöglicht das Erstellen von Spaltenspeichertabellen. Links am Ende des Artikels.

  • pglz - teilt der Erweiterung mit, dass die Daten mithilfe des in PostgreSQL integrierten Algorithmus komprimiert werden sollen;
  • trd - Transaktionszeit;
  • op, it, wh - analytische Schnitte oder Messungen;
  • m1, m2, m3, m4, m5 - numerische Indikatoren oder Maße;

FĂŒgen wir eine „anstĂ€ndige“ Datenmenge ein und sehen, wie viel Speicherplatz auf der Festplatte benötigt wird. Gleichzeitig ĂŒberprĂŒfen wir die Leistung des Einsatzes. Weil Ich habe meine Experimente auf einen Laptop zu Hause gestellt, ich bin ein bisschen organisch in der Datenmenge. Außerdem, was sogar gut ist, werde ich die Festplatte verwenden, auf der das Gastbetriebssystem Fedora 30 ausgefĂŒhrt wird. Betriebssystemhost - Windows 10 Home Edition. Prozessor Intel Core 7. Das Gastbetriebssystem erhielt 4 GB RAM. PostgreSQL-Version - PostgreSQL 10.10 auf x86_64-pc-linux-gnu, kompiliert von gcc (GCC) 9.1.1 20190503 (Red Hat 9.1.1-1), 64-Bit. Ich werde mit einem Datensatz mit der Anzahl der DatensĂ€tze 52 608 000 experimentieren.

 explain (analyze) insert into cstore_table select '2010-01-01'::date + make_interval(days => d) as trd , op , org , wh , it , 100 as m1 , 100 as m2 , 100 as m3 , 100 as m4 , 100 as m5 from generate_series(0, 1) as op cross join generate_series(1, 2) as org cross join generate_series(1, 3) as wh cross join generate_series(1, 4000) as it cross join generate_series(0, 1095) as d; 

Der Umsetzungsplan sieht wie folgt aus
In cstore_table einfĂŒgen (Kosten = 0,01..24902714242540.01 Zeilen = 1000000000000000 Breite = 150) (tatsĂ€chliche Zeit = 119560.456..119560.456 Zeilen = 0 Schleifen = 1)
----> Verschachtelte Schleife (Kosten = 0,01..24902714242540.01 Zeilen = 1000000000000000 Breite = 150) (tatsÀchliche Zeit = 1.823..22339.976 Zeilen = 52608000 Schleifen = 1)
----------> Funktionsscan fĂŒr generate_series d (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsĂ€chliche Zeit = 0,151..2,198 Zeilen = 1096 Schleifen = 1)
----------> Materialisieren (Kosten = 0,01..27284555030.01 Zeilen = 1000000000000 Breite = 16) (tatsÀchliche Zeit = 0,002..3.196 Zeilen = 48000 Schleifen = 1096)
----------------> Verschachtelte Schleife (Kosten = 0,01..17401742530.01 Zeilen = 1000000000000 Breite = 16) (tatsÀchliche Zeit = 1.461..15.072 Zeilen = 48000 Schleifen = 1)
----------------------> Funktionsscan auf generate_series it (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 1,159..2,007 Zeilen = 4000 Schleifen = 1)
----------------------> Materialisieren (Kosten = 0,01..26312333.01 Zeilen = 1.000.000.000 Breite = 12) (tatsÀchliche Zeit = 0.000..0.001 Zeilen = 12 Schleifen = 4000)
----------------------------> Verschachtelte Schleife (Kosten = 0,01..16429520,01 Zeilen = 1.000.000.000 Breite = 12) (tatsÀchliche Zeit = 0,257 ..0.485 Zeilen = 12 Schleifen = 1)
----------------------------------> Funktionsscan auf generate_series wh (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,046..0,049 Zeilen = 3 Schleifen = 1)
----------------------------------> Materialisieren (Kosten = 0,01..28917,01 Zeilen = 1.000.000 Breite = 8) (tatsÀchliche Zeit = 0,070..0,139 Zeilen = 4 Schleifen = 3)
---------------------------------------> Verschachtelte Schleife (Kosten = 0.01..20010.01 Zeilen = 1000000 Breite = 8) (tatsÀchliche Zeit = 0,173..0,366 Zeilen = 4 Schleifen = 1)
---------------------------------------> Funktionsscan auf generate_series op ( Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,076..0,079 Zeilen = 2 Schleifen = 1)
-----------------------------------------> Funktionsscan auf generate_series org (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,043..0,047 Zeilen = 2 Schleifen = 2)
Planungszeit: 0,439 ms
AusfĂŒhrungszeit: 119692.051 ms
Gesamtvorlaufzeit - 1.994867517 Minuten

Erstellungszeit des Datensatzes - 22.339976 Sekunden

EinfĂŒgezeit - 1.620341333 Minuten

Es ist mir nicht gelungen, den belegten Speicherplatz mithilfe von PostgreSQL-Funktionen zu bewerten. Ich weiß nicht warum, aber ich zeige 0. Vielleicht ist dies das Standardverhalten fĂŒr externe Tabellen. Wird fĂŒr diesen Dateimanager verwendet. Das belegte Speicherplatzvolumen betrĂ€gt also 226,2 MB. Um viel oder wenig zu bewerten, vergleichen wir es mit einer normalen Tabelle.

 explain (analyze) create table rbstore_table as select '2010-01-01'::date + make_interval(days => d) as trd , op , org , wh , it , 100 as m1 , 100 as m2 , 100 as m3 , 100 as m4 , 100 as m5 from generate_series(0, 1) as op cross join generate_series(1, 2) as org cross join generate_series(1, 3) as wh cross join generate_series(1, 4000) as it cross join generate_series(0, 1095) as d; 

Der Umsetzungsplan sieht wie folgt aus
Verschachtelte Schleife (Kosten = 0,01..22402714242540.01 Zeilen = 1000000000000000 Breite = 44) (tatsÀchliche Zeit = 0,585..23781.942 Zeilen = 52608000 Schleifen = 1)
---> Funktionsscan auf generate_series d (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,091..2,130 Zeilen = 1096 Schleifen = 1)
---> Materialisieren (Kosten = 0,01..27284555030.01 Zeilen = 1000000000000 Breite = 16) (tatsÀchliche Zeit = 0,001..3.574 Zeilen = 48000 Schleifen = 1096)
----------> Verschachtelte Schleife (Kosten = 0,01..17401742530.01 Zeilen = 1000000000000 Breite = 16) (tatsÀchliche Zeit = 0,489..14.044 Zeilen = 48000 Schleifen = 1)
----------------> Funktionsscan auf generate_series it (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,477..1,352 Zeilen = 4000 Schleifen = 1 )
----------------> Materialisieren (Kosten = 0,01..26312333.01 Zeilen = 1000000000 Breite = 12) (tatsÀchliche Zeit = 0.000..0.001 Zeilen = 12 Schleifen = 4000)
----------------------> Verschachtelte Schleife (Kosten = 0,01..16429520.01 Zeilen = 1.000.000.000 Breite = 12) (tatsÀchliche Zeit = 0.010..0.019 Zeilen = 12 Schleifen = 1)
----------------------------> Funktionsscan auf generate_series wh (Kosten = 0,00..10,00 Zeilen = 1000 Breite = 4) (tatsÀchlich Zeit = 0,003..0,003 Zeilen = 3 Schleifen = 1)
----------------------------> Materialisieren (Kosten = 0,01..28917,01 Zeilen = 1.000.000 Breite = 8) (tatsÀchliche Zeit = 0,002. 0,004 Zeilen = 4 Schleifen = 3)
----------------------------------> Verschachtelte Schleife (Kosten = 0,01..20010,01 Zeilen = 1.000.000 Breite = 8 ) (tatsÀchliche Zeit = 0,006..0,009 Zeilen = 4 Schleifen = 1)
----------------------------------------> Funktionsscan bei generate_series op (Kosten = 0,00 ..10.00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0.002..0.002 Zeilen = 2 Schleifen = 1)
----------------------------------------> Funktionsscan auf generate_series org (Kosten = 0,00 ..10.00 Zeilen = 1000 Breite = 4) (tatsÀchliche Zeit = 0,001..0.001 Zeilen = 2 Schleifen = 2)
Planungszeit: 0,569 ms
AusfĂŒhrungszeit: 378883.989 ms
Die Zeit, die fĂŒr die Umsetzung dieses Plans aufgewendet wird, interessiert uns nicht, weil im wirklichen Leben werden solche EinsĂ€tze nicht angenommen. Wir sind daran interessiert, wie viel Speicherplatz diese Tabelle belegt. Nachdem ich die Anforderung fĂŒr Systemfunktionen erfĂŒllt hatte, erhielt ich 3,75 GB.

Also, cstore_table - 226 MB, rbstore_table - 3,75 GB. Der Unterschied von 16,99 ist auffĂ€llig, aber es ist unwahrscheinlich, dass der gleiche Unterschied in der Produktion erzielt werden kann, hauptsĂ€chlich aufgrund der Verteilung von Daten. In der Regel ist dieser Unterschied geringer und betrĂ€gt etwa das FĂŒnffache.

Aber warten Sie, niemand verwendet Rohdaten in einem zeilenbasierten Format fĂŒr Analysezwecke. Beispielsweise versuchen sie, indizierte Daten fĂŒr die Berichterstellung zu verwenden. Und weil "Rohdaten" werden immer sein, Sie mĂŒssen die GrĂ¶ĂŸen mit den GrĂ¶ĂŸen der Indizes vergleichen. Lassen Sie uns mindestens einen Index erstellen. Sei es ein Index fĂŒr das Datumsfeld und die Art der Operation - trd + op.

Also habe ich nur zwei Felder indiziert, und der Index hat 1583 MB benötigt, was viel mehr ist als die cstore_table. In der Regel ist jedoch mehr als ein Index fĂŒr das Laden von OLAP erforderlich. Hierbei ist zu beachten, dass fĂŒr die cstore_table keine zusĂ€tzliche Indizierung erforderlich ist. Diese Tabelle dient als Index fĂŒr alle Abfragen.

Aus all dem kann eine einfache Schlussfolgerung gezogen werden: Mithilfe von Columnstore-Tabellen können Sie den verwendeten Speicherplatz reduzieren.

Abfrageleistung


FĂŒhren Sie zur Bewertung der Leistung eine Abfrage aus, die Zusammenfassungsdaten fĂŒr einen bestimmten Monat fĂŒr einen bestimmten Operationstyp zurĂŒckgibt.

 explain (analyze, costs, buffers) select sum(m1) from cstore_table where trd = '2011-01-01' and op = 1; 

Der Umsetzungsplan sieht wie folgt aus
Aggregat (Kosten = 793602.69..793602.70 Zeilen = 1 Breite = 32) (tatsÀchliche Zeit = 79.708..79.708 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 44226
---> Fremdscan auf cstore_table (Kosten = 0,00..793544.70 Zeilen = 23197 Breite = 5) (tatsÀchliche Zeit = 23.209..76.628 Zeilen = 24000 Schleifen = 1)
-------- Filter: ((trd = '2011-01-01' :: Datum) UND (op = 1))
-------- Vom Filter entfernte Zeilen: 26000
-------- CStore-Datei: / var / lib / pgsql / 10 / data / cstore_fdw / 14028/16417
-------- CStore-DateigrĂ¶ĂŸe: 120818897
-------- Puffer: geteilter Treffer = 44226
Planungszeit: 0,165 ms
AusfĂŒhrungszeit: 79,887 ms
Und

 explain (analyze, costs, buffers) select sum(m1) from rbstore_table where trd = '2011-01-01' and op = 1; 

Der Umsetzungsplan sieht wie folgt aus
Aggregat (Kosten = 40053,80..40053,81 Zeilen = 1 Breite = 8) (tatsÀchliche Zeit = 389,183..389,183 Zeilen = 1 Schleifen = 1)
- Puffer: Shared Read = 545
---> Index-Scan mit trd_op_ix auf rbstore_table (Kosten = 0,56..39996.70 Zeilen = 22841 Breite = 4) (tatsÀchliche Zeit = 55.955..385.283 Zeilen = 24000 Schleifen = 1)
-------- Index Cond: ((trd = '2011-01-01 00:00:00' :: Zeitstempel ohne Zeitzone) AND (op = 1))
-------- Puffer: Shared Read = 545
Planungszeit: 112,175 ms
AusfĂŒhrungszeit: 389,219 ms
389,219 ms vs 79,887 ms. Hier sehen wir, dass selbst bei einer relativ kleinen Menge von Spaltenspeicherdaten eine Tabelle erheblich schneller ist als ein Index fĂŒr eine zeilenbasierte Tabelle.

Lassen Sie uns die Anfrage Ă€ndern und versuchen, das GerĂ€t fĂŒr das gesamte Jahr 2011 zu erhalten.

 explain (analyze, costs, buffers) select sum(m1) from cstore_table where trd between '2011-01-01' and '2011-12-31' and op = 1; 

Der Umsetzungsplan sieht wie folgt aus
Aggregat (Kosten = 946625,58..946625.59 Zeilen = 1 Breite = 32) (tatsÀchliche Zeit = 3123.604..3123.604 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 44226
---> Fremdscan in cstore_table (Kosten = 0,00..925064,70 Zeilen = 8624349 Breite = 5) (tatsÀchliche Zeit = 21,728..2100,665 Zeilen = 8760000 Schleifen = 1)
-------- Filter: ((trd> = '2011-01-01' :: date) AND (trd <= '2011-12-31' :: date) AND (op = 1))
-------- Vom Filter entfernte Zeilen: 8760000
-------- CStore-Datei: / var / lib / pgsql / 10 / data / cstore_fdw / 14028/16411
-------- CStore-DateigrĂ¶ĂŸe: 120818897
-------- Puffer: geteilter Treffer = 44226
Planungszeit: 0,212 ms
AusfĂŒhrungszeit: 3123,960 ms
Und

 explain (analyze, costs, buffers) select sum(m1) from rbstore_table where trd between '2011-01-01' and '2011-12-31' and op = 1; 

Der Umsetzungsplan sieht wie folgt aus
Aggregat finalisieren (Kosten = 885214.33..885214.34 Zeilen = 1 Breite = 8) (tatsÀchliche Zeit = 98512.560..98512.560 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 2565 read = 489099
---> Sammeln (Kosten = 885214.12..885214.33 Zeilen = 2 Breite = 8) (tatsÀchliche Zeit = 98427.034..98523.194 Zeilen = 3 Schleifen = 1)
-------- Geplante Arbeitnehmer: 2
-------- Gestartete Arbeiter: 2
-------- Puffer: geteilter Treffer = 2565 gelesen = 489099
---------> Teilaggregat (Kosten = 884214.12..884214.13 Zeilen = 1 Breite = 8) (tatsÀchliche Zeit = 97907.608..97907.608 Zeilen = 1 Schleifen = 3)
-------------- Puffer: geteilter Treffer = 2565 read = 489099
---------------> Paralleler Seq-Scan in rbstore_table (Kosten = 0,00..875264.00 Zeilen = 3580047 Breite = 4) (tatsÀchliche Zeit = 40820.004..97405.250 Zeilen = 2920000 Schleifen = 3)
--------------------- Filter: ((trd> = '2011-01-01 00:00:00' :: Zeitstempel ohne Zeitzone) UND (trd <= '2011-12-31 00:00:00' :: Zeitstempel ohne Zeitzone) UND (op = 1))
-------------------- Vom Filter entfernte Zeilen: 14616000
-------------------- Puffer: geteilter Treffer = 2565 read = 489099
Planungszeit: 7.899 ms
AusfĂŒhrungszeit: 98523.278 ms
98523,278 ms vs 3123,960 ms. Vielleicht wĂŒrde uns ein Teilindex helfen, aber es ist besser, ihn nicht zu riskieren und eine geeignete row_based-Struktur zu erstellen, in der vorgefertigte Werte gespeichert werden.

Manuelle Aggregate


Eine geeignete Struktur fĂŒr manuelle Aggregate könnte eine regulĂ€re row_based-Tabelle sein, die vorberechnete Werte enthĂ€lt. Beispielsweise kann es einen Datensatz fĂŒr 2011 mit dem Operationstyp 1 enthalten, wĂ€hrend in den Feldern m1, m2, m3, m4 und m5 der aggregierte Wert genau fĂŒr diese Analyseabschnitte gespeichert wird. Mit einem ausreichenden Satz von Aggregaten und Indizes erzielen analytische Abfragen eine beispiellose Leistung. Interessanterweise verfĂŒgt Microsoft SQL Server Analysis Services ĂŒber einen speziellen Assistenten, mit dem Sie die Anzahl und Tiefe vorberechneter Werte konfigurieren können.

Diese Lösung hat folgende Vorteile:

  • Echtzeitanalyse.

    Bitte verwechseln Sie nicht den Begriff "Echtzeitanalyse". Hier geht es um die Tatsache, dass das Inkrementieren der Einheit in den allermeisten FĂ€llen ĂŒber einen akzeptablen Zeitraum erfolgt.

    In der Tat ist dieses Plus umstritten, aber lassen Sie uns nicht darĂŒber sprechen. Die Tatsache bleibt bestehen. Die Architektur der Lösung ist so, dass die Einheiten fast immer „frisch“ bleiben.
  • VollstĂ€ndige UnabhĂ€ngigkeit vom Datenvolumen.

    Dies ist ein sehr ernstes Plus. UnabhĂ€ngig davon, wie viele Daten verarbeitet werden, werden sie frĂŒher oder spĂ€ter verarbeitet und Aggregate empfangen.
  • Relative KomplexitĂ€t.

    Um Echtzeitanalysen und UnabhÀngigkeit vom Datenvolumen zu erhalten, muss die Lösung fortschrittliche Technologien wie Multithreading und manuelle Sperrverwaltung auf DBMS-Ebene verwenden.
  • SchwierigkeitsprĂŒfung.

    Hier geht es sowohl um Unit-Tests als auch um manuelle Tests. Ich denke, der Leser sollte nicht erklÀren, dass das Erkennen von Multithreading-Fehlern keine leichte Aufgabe ist.
  • Erhöhter Speicherplatzbedarf.


Die tatsÀchliche Verwendung von Columnstore


Hier mĂŒssen wir noch einmal in die Theorie eintauchen und die Frage, was analytische Daten sind, genauer analysieren.

Nehmen Sie den durchschnittlichen Leiter des Unternehmens. In der Regel ist er / sie besorgt ĂŒber zwei globale Fragen: „Wie lĂ€uft es im Moment?“ und "Was hat sich in letzter Zeit geĂ€ndert?".

Um die Frage „Wie lĂ€uft es im Moment?“ Zu beantworten, benötigen wir absolut keine historischen Daten. Das heißt, egal wie es vor einem Monat lief.

Um mit dem Puls Schritt zu halten, wird oft die Frage gestellt. Diese Art der Datenanalyse wird als betriebsbereit bezeichnet.

Um die Frage „Was hat sich in letzter Zeit geĂ€ndert?“ Zu beantworten, benötigen wir genau historische Daten. DarĂŒber hinaus wird die Analyse in der Regel in den gleichen Zeitintervallen durchgefĂŒhrt. Zum Beispiel wird ein Monat mit einem Monat, Jahr zu Jahr usw. verglichen. NatĂŒrlich sollte das System den Benutzer nicht daran hindern, beliebige ZeitrĂ€ume zu vergleichen, aber ein solcher Fall muss als selten erkannt werden, weil Ein Vergleich eines geschlossenen Jahres mit einer nicht geschlossenen HĂ€lfte macht wenig Sinn. Ein charakteristisches Merkmal der vergleichenden Analyse ist, dass sie nicht so oft wie betrieblich erforderlich ist. Wir werden diese Art der Analyse als historisch bezeichnen.

Offensichtlich sollte die Betriebsanalyse schnell erfolgen. Dementsprechend stellt es hohe Anforderungen an die Leistung. WĂ€hrend fĂŒr die historische Analyse können solche Anforderungen nicht gestellt werden. Obwohl die Leistung der historischen Analyse auf einem sehr hohen Niveau bleiben sollte. Zumindest damit das Analysesystem selbst wettbewerbsfĂ€hig bleibt.

Entsprechend zwei Arten von Analysen können wir zwei Arten von Analysedaten unterscheiden: Betriebsdaten und historische Daten. Von Seiten des Benutzers sollte es nicht erkennbar sein, mit welchen bestimmten Daten er gerade arbeitet.

Aufgrund dieser Überlegungen hat sich auf Datenbankservern die Möglichkeit ergeben, Tabellen in separate Abschnitte aufzuteilen.

In Bezug auf den Spaltenspeicher ist es möglich, Abschnitte in zeilenbasierten und Spaltenspeicherformaten zu mischen. Es ist bekannt, dass Betriebsanalysedaten hĂ€ufigen Änderungen unterliegen, wodurch ihre Speicherung im Spaltenspeicherformat verhindert wird. Und da Betriebsdaten nicht zu hĂ€ufig vorkommen, können sie im zeilenbasierten Format gespeichert werden.

Historische Daten Àndern sich nicht. Es gibt viele dieser Daten, und daher passt das Columnstore-Format besser zu ihnen. Denken Sie daran, dass die Leistung von fett gedruckten Abfragen in einer Columnstore-Quelle höher ist als in einer zeilenbasierten Quelle.

Schauen wir uns ein Beispiel fĂŒr all das an.

Im Folgenden erstelle ich die Hauptlagertabelle und fĂŒge die Abschnitte der betrieblichen und historischen Analyse hinzu.

 create table warehouse ( trd date, org int, op int, it int, wh int, m1 numeric(32, 2), m2 numeric(32, 2), m3 numeric(32, 2), m4 numeric(32, 2), m5 numeric(32, 2) ) partition by range(trd); create foreign table historycal_data ( trd date, org int, op int, it int, wh int, m1 numeric(32, 2), m2 numeric(32, 2), m3 numeric(32, 2), m4 numeric(32, 2), m5 numeric(32, 2) ) server cstore_server options(compression 'pglz'); insert into historycal_data select '2010-01-01'::date + make_interval(days => d) as trd , op , org , wh , it , 100 as m1 , 100 as m2 , 100 as m3 , 100 as m4 , 100 as m5 from generate_series(0, 1) as op cross join generate_series(1, 2) as org cross join generate_series(1, 3) as wh cross join generate_series(1, 4000) as it cross join generate_series(0, (1095 - 31)) as d; analyze historycal_data; create table operational_data as select ('2012-12-01'::date + make_interval(days => d))::date as trd , op , org , wh , it , 100::numeric(32, 2) as m1 , 100::numeric(32, 2) as m2 , 100::numeric(32, 2) as m3 , 100::numeric(32, 2) as m4 , 100::numeric(32, 2) as m5 from generate_series(0, 1) as op cross join generate_series(1, 2) as org cross join generate_series(1, 3) as wh cross join generate_series(1, 4000) as it cross join generate_series(0, 30) as d; create index trd_op_ix on operational_data (trd, op); analyze operational_data; alter table warehouse attach partition operational_data for values from ('2012-12-01') to ('2112-01-01'); alter table warehouse attach partition historycal_data for values from ('2010-01-01') to ('2012-12-01'); 

Alles ist fertig. Versuchen wir, ein paar Berichte zu bestellen. Beginnen wir mit der Bestellung von Daten fĂŒr einen Tag des aktuellen Monats.

 explain (analyze, costs, buffers) select sum(m1) from warehouse where trd = '2012-12-01' and op = 1; 

Aggregat (Kosten = 15203.37..15203.38 Zeilen = 1 Breite = 32) (tatsÀchliche Zeit = 17.320..17.320 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 3 gelesen = 515
---> AnhÀngen (Kosten = 532,59..15140,89 Zeilen = 24991 Breite = 5) (tatsÀchliche Zeit = 1,924..13,838 Zeilen = 24000 Schleifen = 1)
------- Puffer: geteilter Treffer = 3 gelesen = 515
---------> Bitmap-Heap-Scan fĂŒr Betriebsdaten (Kosten = 532,59..15140,89 Zeilen = 24991 Breite = 5) (tatsĂ€chliche Zeit = 1,924..11,992 Zeilen = 24000 Schleifen = 1)
--------------- ÜberprĂŒfen Sie erneut Cond: ((trd = '2012-12-01' :: date) AND (op = 1))
--------------- Heap-Blöcke: genau = 449
--------------- Puffer: geteilter Treffer = 3 gelesen = 515
----------------> Bitmap-Index-Scan auf trd_op_ix (Kosten = 0,00..526,34 Zeilen = 24991 Breite = 0) (tatsÀchliche Zeit = 1,877..1,877 Zeilen = 24000 Schleifen = 1 )
--------------------- Index Cond: ((trd = '2012-12-01' :: date) AND (op = 1))
--------------------- Puffer: geteilter Treffer = 2 gelesen = 67
Planungszeit: 0,388 ms
AusfĂŒhrungszeit: 100.941 ms
Jetzt werden wir Daten fĂŒr das gesamte Jahr 2012 bestellen, bei denen die Anzahl der Transaktionen 8.784.000 betrĂ€gt.

 explain (analyze, costs, buffers) select sum(m1) from warehouse where trd between '2012-01-01' and '2012-12-31' and op = 1; 
Aggregat (Kosten = 960685.82..960685.83 Zeilen = 1 Breite = 32) (tatsÀchliche Zeit = 4124.681..4124.681 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 45591 read = 11282
---> AnhÀngen (Kosten = 0,00..938846.60 Zeilen = 8735687 Breite = 5) (tatsÀchliche Zeit = 66.581..3036.394 Zeilen = 8784000 Schleifen = 1)
--------- Puffer: geteilter Treffer = 45591 read = 11282
----------> Fremdscan fĂŒr historycal_data (Kosten = 0,00..898899.60 Zeilen = 7994117 Breite = 5) (tatsĂ€chliche Zeit = 66.579..2193.801 Zeilen = 8040000 Schleifen = 1)
--------------- Filter: ((trd> = '2012-01-01' :: date) AND (trd <= '2012-12-31' :: date) AND (op = 1))
--------------- Vom Filter entfernte Zeilen: 8040000
--------------- CStore-Datei: / var / lib / pgsql / 10 / data / cstore_fdw / 14028/16448
--------------- CStore-DateigrĂ¶ĂŸe: 117401470
--------------- Puffer: geteilter Treffer = 42966
----------> Seq Scan on operational_data (Kosten = 0,00..39947,00 Zeilen = 741570 Breite = 5) (tatsÀchliche Zeit = 0,019..284,824 Zeilen = 744000 Schleifen = 1)
--------------- Filter: ((trd> = '2012-01-01' :: date) AND (trd <= '2012-12-31' :: date) AND (op = 1))
--------------- Vom Filter entfernte Zeilen: 744000
--------------- Puffer: geteilter Treffer = 2625 gelesen = 11282
Planungszeit: 0,256 ms
AusfĂŒhrungszeit: 4125.239 ms
Lassen Sie uns am Ende sehen, was passiert, wenn der Benutzer beispielsweise ohne böswillige Absicht einen Bericht ĂŒber alle Transaktionen im System bestellen möchte, von denen es 52 608 000 gibt.

 explain (analyze, costs, buffers) select sum(m1) from warehouse 

Aggregat (Kosten = 672940.20..672940.21 Zeilen = 1 Breite = 32) (tatsÀchliche Zeit = 15907.886..15907.886 Zeilen = 1 Schleifen = 1)
- Puffer: geteilter Treffer = 17075 read = 11154
---> AnhÀngen (Kosten = 0,00..541420,20 Zeilen = 52608000 Breite = 5) (tatsÀchliche Zeit = 0,192..9115.144 Zeilen = 52608000 Schleifen = 1)
--------- Puffer: geteilter Treffer = 17075 gelesen = 11154
----------> Fremdscan fĂŒr historycal_data (Kosten = 0,00..512633.20 Zeilen = 51120000 Breite = 5) (tatsĂ€chliche Zeit = 0.191..5376.449 Zeilen = 51120000 Schleifen = 1)
--------------- CStore-Datei: / var / lib / pgsql / 10 / data / cstore_fdw / 14028/16448
--------------- CStore-DateigrĂ¶ĂŸe: 117401470
--------------- Puffer: geteilter Treffer = 14322
----------> Seq Scan on operational_data (Kosten = 0,00..28787,00 Zeilen = 1488000 Breite = 5) (tatsÀchliche Zeit = 0,032..246,978 Zeilen = 1488000 Schleifen = 1)
--------------- Puffer: geteilter Treffer = 2753 read = 11154
Planungszeit: 0,157 ms
AusfĂŒhrungszeit: 15908.096 ms
Bitte beachten Sie, dass ich meinen Artikel immer noch schreibe, als wÀre nichts passiert. Ich musste meinen nicht so leistungsstarken Laptop mit Festplatte und 4 GB RAM nicht einmal neu starten. Obwohl das Problem des Ressourcenverbrauchs eine genauere Untersuchung erfordert.

Fehlertoleranz


Zum Teil wurde die Fehlertoleranz zum Zeitpunkt dieses Schreibens getestet. Mein Laptop lebt, und im Allgemeinen habe ich außer den ĂŒblichen keine Verlangsamung in seiner Arbeit bemerkt.

Lassen Sie den Leser mir verzeihen, dass ich keine Zeit hatte, das Problem der Fehlertoleranz im Detail zu erarbeiten, aber ich kann sagen, dass die betreffende Erweiterung Fehlertoleranz aufweist - eine Sicherung ist möglich.

Einfache Implementierung


Wie sich herausstellte, gibt es beim Erstellen einer Tabelle, in der Daten in einem Spaltenspeicherformat gespeichert werden, keine anderen Optionen als einen Komprimierungsalgorithmus. Die Komprimierung selbst ist unbedingt erforderlich.

Das Format selbst hat eine bestimmte Struktur. Durch Einstellen der entsprechenden Parameter können Sie eine bestimmte Beschleunigung der analytischen Abfragen erreichen oder den Grad der Komprimierung von Informationen anpassen.

Wie oben gezeigt, ist das Erstellen einer Columnstore-Tabelle ĂŒberhaupt kein Problem. Die Erweiterung kann mit 40 PostgreSQL-Datentypen arbeiten. In Webinaren wurde ĂŒber alle von PostgreSQL unterstĂŒtzten Typen gesprochen.

Welche neuen FĂ€higkeiten sollte ein Entwickler haben, um mit neuen Strukturen zu arbeiten?


Der SQL-Entwickler benötigt keine besonderen Kenntnisse zum Schreiben von Abfragen in Spaltenspeichertabellen. Eine solche Tabelle ist in allen Abfragen sichtbar, wie eine regulĂ€re zeilenbasierte Tabelle. Dies schließt jedoch die Notwendigkeit einer Abfrageoptimierung nicht aus.

Fazit


In diesem Artikel habe ich gezeigt, wie nĂŒtzlich eine Tabelle mit einem Spaltenspeicher-Speicherformat sein kann. Dies spart Speicherplatz und leistungsstarke analytische Abfragen. Die einfache Arbeit mit der Tabelle reduziert automatisch die Kosten fĂŒr die Erstellung eines vollwertigen analytischen Data Warehouse Seine Verwendung erfordert nicht die Entwicklung komplexer, schwer zu debuggender Algorithmen. Das Testen wird vereinfacht.

Trotz der Tatsache, dass die oben gestellten Experimente Optimismus hervorrufen, wurden viele Probleme nicht gelöst. Beispiel: Welcher Abfrageplan wird generiert, wenn die Columnstore-Tabelle mit anderen Tabellen verknĂŒpft wird. Ich hoffe, diese Arbeit im nĂ€chsten Teil fortzusetzen. Wie viele Teile davon abhĂ€ngen, wie sich cstore_fdw bei mehr oder weniger realen Daten verhĂ€lt.

Links zu zusÀtzlichen Materialien


Kurzer RĂŒckblick cstore_fdw

cstore_fdw auf github

Roadmap cstore_fdw

Source: https://habr.com/ru/post/de472396/


All Articles