Dies ist die erste Lese-/Schreib-Implementierung für das Parquet-Dateiformat in PHP, basierend auf den von der Apache Foundation bereitgestellten Thrift-Quellen. Umfangreiche Teile des Codes und der Konzepte wurden von parquet-dotnet portiert (siehe https://github.com/elastacloud/parquet-dotnet und https://github.com/aloneguid/parquet-dotnet). Unser Dank geht daher an Ivan Gavryliuk (https://github.com/aloneguid).
Mit diesem Paket können Sie Parquet-Dateien/-Streams lesen und schreiben, ohne exotische externe Erweiterungen zu verwenden (es sei denn, Sie möchten exotische Komprimierungsmethoden verwenden). Es verfügt über (fast?) 100 %ige Testkompatibilität mit parquet-dotnet, was die Kernfunktionalität betrifft, durchgeführt über PHPUnit.
Dieses Repository (und das zugehörige Paket auf Packagist) ist die offizielle Projektfortsetzung von jocoon/parquet
. Aufgrund verschiedener Verbesserungen und wichtiger Fehlerbehebungen hier in codename/parquet
wird dringend von der Verwendung des Legacy-Pakets abgeraten.
Für einige Teile dieses Pakets mussten einige neue Muster erfunden werden, da ich keine Implementierung gefunden habe, die den Anforderungen entsprach. In den meisten Fällen waren überhaupt keine Implementierungen verfügbar.
Einige Highlights:
Ich habe mit der Entwicklung dieser Bibliothek begonnen, weil es einfach keine Implementierung für PHP gab.
In meinem Unternehmen brauchten wir eine schnelle Lösung, um riesige Datenmengen aus einer Datenbank in einem Format zu archivieren, das weiterhin abfragbar, aus Schema-Perspektive erweiterbar und fehlertolerant ist. Wir haben mit dem Testen von Live-„Migrationen“ über AWS DMS zu S3 begonnen, die aufgrund von Speicherbeschränkungen bei bestimmten Datenmengen zum Absturz führten. Und es war einfach zu datenbankorientiert, abgesehen davon, dass es leicht ist, versehentlich Daten aus früheren Ladevorgängen zu löschen. Da wir eine stark SDS-orientierte und plattformunabhängige Architektur haben, ist es nicht meine bevorzugte Methode, Daten als 1:1-Klon der Datenbank, etwa als Dump, zu speichern. Stattdessen wollte ich die Möglichkeit haben, Daten dynamisch strukturiert zu speichern, so wie ich es wollte, auf die gleiche Weise, wie DMS es nach S3 exportierte. Schließlich scheiterte das Projekt aus den oben genannten Gründen.
Aber das Parkettformat ging mir nicht mehr aus dem Kopf.
Das TOP-1-Suchergebnis (https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code) sah vielversprechend aus, da es nicht so viel Aufwand erfordern würde eine PHP-Implementierung - aber tatsächlich hat es einiges gedauert (ca. 2 Wochen nicht aufeinanderfolgende Arbeit). Für mich als PHP- und C#-Entwickler war parquet-dotnet ein perfekter Ausgangspunkt – nicht nur, weil die Benchmarks einfach zu überzeugend waren. Allerdings habe ich damit gerechnet, dass die PHP-Implementierung diese Leistungsniveaus nicht erreichen würde, da es sich um eine erste Implementierung handelt, die das Prinzip zeigt. Und außerdem hatte es noch niemand zuvor getan.
Da PHP einen großen Anteil an webbezogenen Projekten hat, ist dies ein MUSS in Zeiten des wachsenden Bedarfs an Big-Data-Anwendungen und -Szenarien. Für meine persönliche Motivation ist dies eine Möglichkeit zu zeigen, dass PHP seinen Ruf als „Skriptsprache“ (physisch, virtuell?) übertroffen hat. Ich denke – oder hoffe zumindest –, dass es Menschen da draußen gibt, die von diesem Paket und der Botschaft, die es transportiert, profitieren werden. Nicht nur Gebrauchsgegenstände. Wortspiel beabsichtigt.
Um diese Bibliothek in vollem Umfang nutzen zu können, benötigen Sie mehrere Erweiterungen.
Diese Bibliothek wurde ursprünglich für/mit PHP 7.3 entwickelt, sollte aber auf PHP > 7 funktionieren und wird bei Veröffentlichung auf 8 getestet. Derzeit werden Tests auf PHP 7.1 und 7.2 aufgrund einiger DateTime-Probleme fehlschlagen. Ich werde es mir ansehen. Tests bestehen vollständig auf PHP 7.3 und 7.4. Zum Zeitpunkt des Verfassens dieses Artikels läuft auch 8.0.0 RC2 gut.
Diese Bibliothek hängt stark davon ab
Ab Version 0.2 bin ich außerdem auf einen umsetzungsunabhängigen Ansatz mit der Verwendung von Lese- und Schreibfunktionen umgestiegen. Jetzt beschäftigen wir uns mit BinaryReader(Interface)- und BinaryWriter(Interface)-Implementierungen, die den zugrunde liegenden Mechanismus abstrahieren. Mir ist aufgefallen, dass mdurrant/php-binary-reader einfach viel zu langsam ist. Ich wollte einfach nicht alles umgestalten, nur um Nelexas Lesefähigkeiten auszuprobieren. Stattdessen habe ich die beiden oben genannten Schnittstellen erstellt, um verschiedene Pakete zu abstrahieren, die binäres Lesen/Schreiben ermöglichen. Dies führt schließlich zu einer optimalen Möglichkeit zum Testen/Benchmarking verschiedener Implementierungen – und auch zum Mischen, z. B. mit dem Paket von wapmorgan zum Lesen und mit dem Paket von Nelexa zum Schreiben.
Ab Version 0.2.1 habe ich die binären Lese-/Schreib-Implementierungen selbst durchgeführt, da keine Implementierung die Leistungsanforderungen erfüllte. Speziell beim Schreiben liefert diese ultraleichte Implementierung die dreifache* Leistung des Nelexa-Puffers.
*beabsichtigt, ich liebe dieses Wort
Alternative binäre Lese-/Schreibpakete von Drittanbietern im Umfang:
Installieren Sie dieses Paket über Composer, z
composer require codename/parquet
Die mitgelieferte Docker-Datei gibt Ihnen einen Überblick über die benötigten Systemanforderungen. Das Wichtigste ist, php-ext-snappy zu klonen und zu installieren. Zum Zeitpunkt des Verfassens dieses Artikels war es noch nicht als PECL veröffentlicht .
...
# NOTE: this is a dockerfile snippet. Bare metal machines will be a little bit different
RUN git clone --recursive --depth=1 https://github.com/kjdev/php-ext-snappy.git
&& cd php-ext-snappy
&& phpize
&& ./configure
&& make
&& make install
&& docker-php-ext-enable snappy
&& ls -lna
...
Bitte beachten Sie: Die Kompilierung und Installation von php-ext-snappy unter Windows ist etwas eigenartig, daher handelt es sich hier nur um eine kurze Information zur Installation und Verwendung auf Linux-basierten Systemen. Solange Sie die schnelle Komprimierung nicht zum Lesen oder Schreiben benötigen, können Sie php-parquet verwenden, ohne es selbst zu kompilieren.
Ich habe festgestellt, dass ParquetViewer (https://github.com/mukunku/ParquetViewer) von Mukunku eine großartige Möglichkeit ist, die zu lesenden Daten zu untersuchen oder einige Dinge auf einem Windows-Desktop-Computer zu überprüfen. Dies hilft zumindest dabei, bestimmte Mechanismen zu verstehen, da es mehr oder weniger visuell hilft, indem die Daten einfach als Tabelle angezeigt werden.
Die Verwendung ist nahezu identisch mit der von Parkett-Dotnet. Bitte beachten Sie, dass wir kein using ( ... ) { }
haben, wie in C#. Sie müssen also sicherstellen, dass Sie ungenutzte Ressourcen selbst schließen/entsorgen oder sie dem GC von PHP durch seinen Refcounting-Algorithmus automatisch verarbeiten lassen. (Aus diesem Grund verwende ich keine Destruktoren wie parquet-dotnet.)
Da sich das Typsystem von PHP völlig von dem von C# unterscheidet, müssen wir einige Ergänzungen zum Umgang mit bestimmten Datentypen vornehmen. Beispielsweise ist eine PHP-Ganzzahl irgendwie nullbar. Ein int in C# ist es nicht. Dies ist ein Punkt, bei dem ich immer noch unsicher bin, wie ich damit umgehen soll. Im Moment habe ich int (PHP integer ) so eingestellt, dass es nullbar ist – parquet-dotnet macht dies so, dass es nicht nullbar ist. Sie können dieses Verhalten jederzeit anpassen, indem Sie ->hasNulls = true;
auf Ihrem DataField. Darüber hinaus verwendet PHP-Parkett eine zweifache Methode zur Typbestimmung. In PHP hat ein Grundelement seinen eigenen Typ (Ganzzahl, Bool, Float/Double usw.). Bei Klasseninstanzen (insbesondere DateTime/DateTimeImmutable) ist der von get_type() zurückgegebene Typ immer Objekt. Aus diesem Grund existiert eine zweite Eigenschaft für die DataTypeHandlers, um sie abzugleichen, zu bestimmen und zu verarbeiten: phpClass.
Zum Zeitpunkt des Schreibens wird hier nicht jeder von parquet-dotnet unterstützte Datentyp unterstützt. Ich habe beispielsweise Int16, SignedByte und einige mehr übersprungen, aber die Erweiterung auf vollständige Binärkompatibilität sollte nicht zu kompliziert sein.
Derzeit stellt diese Bibliothek die Kernfunktionalität bereit, die zum Lesen und Schreiben von Parquet-Dateien/-Streams erforderlich ist. Es enthält nicht die Tabellen, Zeilen und Enumeratoren/Helper von parquet-dotnet aus dem C#-Namespace Parquet.Data.Rows
.
use codename parquet ParquetReader ;
// open file stream (in this example for reading only)
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' r ' );
// open parquet file reader
$ parquetReader = new ParquetReader ( $ fileStream );
// Print custom metadata or do other stuff with it
print_r ( $ parquetReader -> getCustomMetadata ());
// get file schema (available straight after opening parquet reader)
// however, get only data fields as only they contain data values
$ dataFields = $ parquetReader -> schema -> GetDataFields ();
// enumerate through row groups in this file
for ( $ i = 0 ; $ i < $ parquetReader -> getRowGroupCount (); $ i ++)
{
// create row group reader
$ groupReader = $ parquetReader -> OpenRowGroupReader ( $ i );
// read all columns inside each row group (you have an option to read only
// required columns if you need to.
$ columns = [];
foreach ( $ dataFields as $ field ) {
$ columns [] = $ groupReader -> ReadColumn ( $ field );
}
// get first column, for instance
$ firstColumn = $ columns [ 0 ];
// $data member, accessible through ->getData() contains an array of column data
$ data = $ firstColumn -> getData ();
// Print data or do other stuff with it
print_r ( $ data );
}
use codename parquet ParquetWriter ;
use codename parquet data Schema ;
use codename parquet data DataField ;
use codename parquet data DataColumn ;
//create data columns with schema metadata and the data you need
$ idColumn = new DataColumn (
DataField:: createFromType ( ' id ' , ' integer ' ), // NOTE: this is a little bit different to C# due to the type system of PHP
[ 1 , 2 ]
);
$ cityColumn = new DataColumn (
DataField:: createFromType ( ' city ' , ' string ' ),
[ " London " , " Derby " ]
);
// create file schema
$ schema = new Schema ([ $ idColumn -> getField (), $ cityColumn -> getField ()]);
// create file handle with w+ flag, to create a new file - if it doesn't exist yet - or truncate, if it exists
$ fileStream = fopen ( __DIR__ . ' /test.parquet ' , ' w+ ' );
$ parquetWriter = new ParquetWriter ( $ schema , $ fileStream );
// optional, write custom metadata
$ metadata = [ ' author ' => ' santa ' , ' date ' => ' 2020-01-01 ' ];
$ parquetWriter -> setCustomMetadata ( $ metadata );
// create a new row group in the file
$ groupWriter = $ parquetWriter -> CreateRowGroup ();
$ groupWriter -> WriteColumn ( $ idColumn );
$ groupWriter -> WriteColumn ( $ cityColumn );
// As we have no 'using' in PHP, I implemented finish() methods
// for ParquetWriter and ParquetRowGroupWriter
$ groupWriter -> finish (); // finish inner writer(s)
$ parquetWriter -> finish (); // finish the parquet writer last
Sie können ParquetDataIterator
und ParquetDataWriter
auch für die Arbeit selbst mit hochkomplexen Schemata (z. B. verschachtelten Daten) verwenden. Obwohl zum Zeitpunkt des Verfassens dieses Artikels noch experimentell, deuten Unit- und Integrationstests darauf hin, dass wir eine 100-prozentige Kompatibilität mit Spark haben, da den meisten anderen Parquet-Implementierungen bestimmte Funktionen oder Fälle von superkomplexer Verschachtelung fehlen.
ParquetDataIterator
und ParquetDataWriter
nutzen die „Dynamik“ des PHP-Typsystems und (assoziativer) Arrays – die nur dann zum Erliegen kommt, wenn vorzeichenlose 64-Bit-Ganzzahlen vollständig verwendet werden – diese können aufgrund der Natur von PHP nur teilweise unterstützt werden.
ParquetDataIterator
iteriert automatisch über alle Zeilengruppen und Datenseiten sowie über alle Spalten der Parkettdatei auf die speichereffizienteste Art und Weise, die möglich ist. Das bedeutet, dass nicht alle Datensätze in den Speicher geladen werden, sondern dies auf einer Basis pro Datenseite/pro Zeilengruppe.
Unter der Haube nutzt es die Funktionalität von DataColumnsToArrayConverter
, der letztendlich die ganze „Schwerarbeit“ in Bezug auf Definition und Wiederholungsebenen übernimmt.
use codename parquet helper ParquetDataIterator ;
$ iterateMe = ParquetDataIterator:: fromFile ( ' your-parquet-file.parquet ' );
foreach ( $ iterateMe as $ dataset ) {
// $dataset is an associative array
// and already combines data of all columns
// back to a row-like structure
}
Umgekehrt ermöglicht Ihnen ParquetDataWriter
das Schreiben einer Parquet-Datei (im Arbeitsspeicher oder auf der Festplatte), indem Sie assoziative PHP-Array-Daten einzeln oder stapelweise übergeben. Intern verwendet es ArrayToDataColumnsConverter
um Daten, Wörterbücher, Definitionen und Wiederholungsebenen zu erstellen.
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
DataField:: createFromType ( ' name ' , ' string ' ),
]);
$ handle = fopen ( ' sample.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
// add two records at once
$ dataToWrite = [
[ ' id ' => 1 , ' name ' => ' abc ' ],
[ ' id ' => 2 , ' name ' => ' def ' ],
];
$ dataWriter -> putBatch ( $ dataToWrite );
// we add a third, single one
$ dataWriter -> put ([ ' id ' => 3 , ' name ' => ' ghi ' ]);
$ dataWriter -> finish (); // Don't forget to finish at some point.
fclose ( $ handle ); // You may close the handle, if you have to.
php-parquet unterstützt die vollständigen Verschachtelungsfunktionen des Parquet-Formats. Möglicherweise stellen Sie fest, dass Sie je nachdem, welche Feldtypen Sie verschachteln, Schlüsselnamen irgendwie „verlieren“. Dies ist beabsichtigt:
Im Allgemeinen sind hier die PHP-Äquivalente der logischen Typen des Parquet-Formats:
Parkett | PHP | JSON | Notiz |
---|---|---|---|
Datenfeld | Primitive | Primitive | z. B. Zeichenfolge, Ganzzahl usw. |
ListField | Array | Array [] | Der Elementtyp kann ein Grundelement oder sogar eine Liste, Struktur oder Karte sein |
StructField | assoziatives Array | Objekt {} | Schlüssel des Vereins Array sind die Feldnamen innerhalb des StructField |
MapField | assoziatives Array | Objekt {} | Vereinfacht: array_keys($data['someField']) und array_values($data['someField']) , aber für jede Zeile |
Das Format ist mit den von Spark generierten JSON-Exportdaten kompatibel, die mit spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False)
konfiguriert wurden. Standardmäßig entfernt Spark beim Export in JSON null
vollständig.
Bitte beachten Sie: Alle diese Feldtypen können auf jeder Verschachtelungsebene nullbar oder nicht nullbar/erforderlich gemacht werden (beeinflusst Definitionsebenen). Einige Nullfähigkeiten werden beispielsweise verwendet, um leere Listen darzustellen und sie von einem null
für eine Liste zu unterscheiden.
use codename parquet helper ParquetDataIterator ;
use codename parquet helper ParquetDataWriter ;
$ schema = new Schema ([
DataField:: createFromType ( ' id ' , ' integer ' ),
new MapField (
' aMapField ' ,
DataField:: createFromType ( ' someKey ' , ' string ' ),
StructField:: createWithFieldArray (
' aStructField '
[
DataField:: createFromType ( ' anInteger ' , ' integer ' ),
DataField:: createFromType ( ' aString ' , ' string ' ),
]
)
),
StructField:: createWithFieldArray (
' rootLevelStructField '
[
DataField:: createFromType ( ' anotherInteger ' , ' integer ' ),
DataField:: createFromType ( ' anotherString ' , ' string ' ),
]
),
new ListField (
' aListField ' ,
DataField:: createFromType ( ' someInteger ' , ' integer ' ),
)
]);
$ handle = fopen ( ' complex.parquet ' , ' r+ ' );
$ dataWriter = new ParquetDataWriter ( $ handle , $ schema );
$ dataToWrite = [
// This is a single dataset:
[
' id ' => 1 ,
' aMapField ' => [
' key1 ' => [ ' anInteger ' => 123 , ' aString ' => ' abc ' ],
' key2 ' => [ ' anInteger ' => 456 , ' aString ' => ' def ' ],
],
' rootLevelStructField ' => [
' anotherInteger ' => 7 ,
' anotherString ' => ' in paradise '
],
' aListField ' => [ 1 , 2 , 3 ]
],
// ... add more datasets as you wish.
];
$ dataWriter -> putBatch ( $ dataToWrite );
$ dataWriter -> finish ();
$ iterateMe = ParquetDataIterator:: fromFile ( ' complex.parquet ' );
// f.e. write back into a full-blown php array:
$ readData = [];
foreach ( $ iterateMe as $ dataset ) {
$ readData [] = $ dataset ;
}
// and now compare this to the original data supplied.
// manually, by print_r, var_dump, assertions, comparisons or whatever you like.
Dieses Paket bietet auch den gleichen Benchmark wie parquet-dotnet. Dies sind die Ergebnisse auf meiner Maschine :
Parquet.Net (.NET Core 2.1) | PHP-Parkett (Bare Metal 7.3) | php-parquet (dockerisiert* 7.3) | Fastparquet (Python) | Parkett-Herr (Java) | |
---|---|---|---|---|---|
Lesen | 255 ms | 1'090ms | 1'244ms | 154 ms** | ungetestet |
Schreiben (unkomprimiert) | 209 ms | 1'272ms | 1'392ms | 237 ms** | ungetestet |
Schreiben (gzip) | 1'945ms | 3'314ms | 3'695ms | 1'737ms** | ungetestet |
Im Allgemeinen wurden diese Tests mit der GZIP-Komprimierungsstufe 6 für PHP-Parkett durchgeführt. Bei 1 (minimale Komprimierung) wird sie sich ungefähr halbieren und bei 9 (maximale Komprimierung) fast verdoppeln. Beachten Sie, dass Letzteres möglicherweise nicht die kleinste Dateigröße, aber immer die längste Komprimierungszeit ergibt.
Da es sich hierbei um eine Teilportierung eines Pakets aus einer völlig anderen Programmiersprache handelt, ist der Programmierstil so ziemlich ein Durcheinander. Ich habe beschlossen, den größten Teil der Groß-/Kleinschreibung beizubehalten (z. B. $writer->CreateRowGroup() statt ->createRowGroup()), um eine gewisse „visuelle Kompatibilität“ mit parquet-dotnet aufrechtzuerhalten. Zumindest ist dies aus meiner Sicht ein wünschenswerter Zustand, da es den Vergleich und die Erweiterung in den ersten Entwicklungsphasen erheblich erleichtert.
Einige Codeteile und Konzepte wurden von C#/.NET portiert, siehe:
php-parquet ist unter der MIT-Lizenz lizenziert. Siehe Datei LIZENZ.
Wenn Sie möchten, können Sie gerne eine PR machen. Da es sich um ein OSS-Freizeitprojekt handelt, helfen Beiträge allen Benutzern dieses Pakets, auch Ihnen selbst. Bitte wenden Sie bei der Erstellung von PRs und/oder Issues eine Prise gesunden Menschenverstand an, es gibt keine Vorlage.