
이는 Apache Foundation에서 제공하는 Thrift 소스를 기반으로 하는 PHP의 첫 번째 쪽모이 세공 파일 형식 판독기/작성기 구현입니다. 코드와 개념의 광범위한 부분이 parquet-dotnet에서 이식되었습니다(https://github.com/elastacloud/parquet-dotnet 및 https://github.com/aloneguid/parquet-dotnet 참조). 따라서 Ivan Gavryliuk(https://github.com/aloneguid)에게 감사드립니다.
이 패키지를 사용하면 이국적인 외부 확장을 사용하지 않고도 Parquet 파일/스트림을 읽고 쓸 수 있습니다(이국적인 압축 방법을 사용하려는 경우는 제외). 핵심 기능과 관련하여 PHPUnit을 통해 수행된 parquet-dotnet과의 테스트 호환성은 (거의?) 100%입니다.
이 저장소(및 Packagist의 관련 패키지)는 jocoon/parquet 의 공식 프로젝트 연속입니다. 다양한 개선 사항과 필수 버그 수정으로 인해 여기 codename/parquet 에서 레거시 패키지를 사용하는 것은 권장되지 않습니다.
이 패키지의 일부 부분에서는 요구 사항을 충족하는 구현을 찾지 못했기 때문에 몇 가지 새로운 패턴을 개발해야 했습니다. 대부분의 경우 사용 가능한 구현이 전혀 없었습니다.
일부 하이라이트:
나는 PHP를 위한 구현이 전혀 없다는 사실 때문에 이 라이브러리를 개발하기 시작했습니다.
우리 회사에서는 쿼리가 가능하고 스키마 관점에서 확장 가능하며 내결함성이 있는 형식으로 데이터베이스의 막대한 양의 데이터를 보관하기 위한 빠른 솔루션이 필요했습니다. 우리는 AWS DMS를 통해 S3로 실시간 '마이그레이션' 테스트를 시작했는데, 결국 메모리 제한으로 인해 특정 양의 데이터에서 충돌이 발생했습니다. 그리고 이전 로드에서 실수로 데이터를 삭제하기 쉽다는 사실 외에도 너무 DB 지향적이었습니다. 우리는 SDS 지향적이고 플랫폼에 구애받지 않는 아키텍처를 갖고 있기 때문에 데이터를 덤프와 같은 데이터베이스의 1:1 복제본으로 저장하는 것이 제가 선호하는 방법은 아닙니다. 대신 DMS가 S3로 내보내는 것과 같은 방식으로 원하는 대로 동적으로 구조화된 데이터를 저장할 수 있는 기능을 갖고 싶었습니다. 결국 프로젝트는 위에서 언급한 이유로 인해 종료되었습니다.
하지만 마루 형식을 머리에서 지울 수가 없었어요..
TOP 1 검색 결과(https://stackoverflow.com/questions/44780419/how-to-create-orc-or-parquet-files-from-php-code)는 그렇게 많은 노력이 필요하지 않을 것으로 예상되었습니다. PHP 구현 - 그러나 실제로는 약간의 시간이 걸렸습니다(비연속 작업으로 약 2주 소요). PHP 및 C# 개발자인 나에게 parquet-dotnet은 완벽한 출발점이었습니다. 단순히 벤치마크가 너무 매력적이라는 사실 때문만은 아닙니다. 하지만 PHP 구현은 원칙을 보여주는 초기 구현이므로 이러한 수준의 성능을 충족하지 못할 것으로 예상했습니다. 게다가 이전에는 누구도 그런 일을 해본 적이 없었습니다.
PHP는 웹 관련 프로젝트에서 큰 비중을 차지하므로 빅 데이터 애플리케이션 및 시나리오에 대한 수요가 증가하는 시대에 반드시 필요한 제품입니다. 개인적인 동기부여를 위해 이는 PHP가 (물리적으로, 사실상?) '스크립팅 언어'로서의 명성을 능가했다는 것을 보여주는 방법입니다. 내 생각에는 - 또는 적어도 희망합니다 - 이 패키지와 그것이 전달하는 메시지로부터 혜택을 받을 사람들이 있을 것입니다. 중고품뿐만 아니라. 말장난 의도.
이 라이브러리를 최대한 활용하려면 여러 가지 확장이 필요합니다.
이 라이브러리는 원래 PHP 7.3을 사용하여 개발되었지만 PHP > 7에서 작동해야 하며 출시되면 8에서 테스트될 예정입니다. 현재 PHP 7.1 및 7.2에 대한 테스트는 일부 DateTime 문제로 인해 실패합니다. 한번 살펴보겠습니다. 테스트는 PHP 7.3 및 7.4에서 완전히 통과되었습니다. 이 글을 쓰는 시점에는 8.0.0 RC2도 잘 작동하고 있습니다.
이 라이브러리는 다음에 크게 의존합니다.
v0.2부터는 구현에 구애받지 않는 판독기와 기록기를 사용하는 접근 방식으로 전환했습니다. 이제 기본 메커니즘을 추상화하는 BinaryReader(Interface) 및 BinaryWriter(Interface) 구현을 다루고 있습니다. mdurrant/php-binary-reader가 너무 느린 것을 발견했습니다. 단지 Nelexa의 읽기 능력을 시험해 보기 위해 모든 것을 리팩터링하고 싶지는 않았습니다. 대신, 위에서 언급한 두 인터페이스를 만들어 바이너리 읽기/쓰기를 제공하는 다양한 패키지를 추상화했습니다. 이는 결국 다양한 구현을 테스트/벤치마킹하는 최적의 방법으로 이어지며, 예를 들어 읽기에는 wapmorgan의 패키지를 사용하고 쓰기에는 Nelexa의 패키지를 사용하는 등의 혼합도 가능합니다.
v0.2.1부터는 성능 요구 사항을 충족하는 구현이 없기 때문에 바이너리 판독기/작성기 구현을 직접 수행했습니다. 특히 글쓰기의 경우 이 초경량 구현은 Nelexa 버퍼 성능의 3배*를 제공합니다.
*의도, 나는 이 단어를 좋아한다
범위 내 대체 타사 바이너리 읽기/쓰기 패키지:
작곡가를 통해 이 패키지를 설치합니다. 예:
composer require codename/parquet포함된 Dockerfile은 필요한 시스템 요구 사항에 대한 아이디어를 제공합니다. 수행해야 할 가장 중요한 작업은 php-ext-snappy 를 복제하고 설치하는 것입니다. 이 글을 쓰는 시점에는 아직 PECL로 출판되지 않았습니다 .
...
# 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
...참고: php-ext-snappy는 Windows에서 컴파일하고 설치하기에는 약간 까다롭습니다. 따라서 이는 Linux 기반 시스템에서의 설치 및 사용에 대한 간단한 정보일 뿐입니다. 읽기나 쓰기에 빠른 압축이 필요하지 않다면 직접 컴파일하지 않고도 php-parquet를 사용할 수 있습니다.
저는 Mukunku의 ParquetViewer(https://github.com/mukunku/ParquetViewer)가 읽을 데이터를 조사하거나 Windows 데스크톱 컴퓨터에서 일부 항목을 확인하는 좋은 방법이라는 것을 알았습니다. 최소한 이는 단순히 데이터를 표로 표시함으로써 시각적으로 어느 정도 도움이 되므로 특정 메커니즘을 이해하는 데 도움이 됩니다.
사용법은 parquet-dotnet과 거의 동일합니다. C#처럼 using ( ... ) { } 수 없습니다. 따라서 사용하지 않는 리소스를 직접 닫거나 폐기하거나 PHP의 GC가 참조 계산 알고리즘을 통해 자동으로 처리하도록 해야 합니다. (이것이 내가 parquet-dotnet과 같은 소멸자를 사용하지 않는 이유입니다.)
PHP의 유형 시스템은 C#과 완전히 다르기 때문에 특정 데이터 유형을 처리하는 방법에 대해 몇 가지 추가 사항을 추가해야 합니다. 예를 들어, PHP 정수는 어떤 식으로든 null을 허용합니다. C#의 int 는 그렇지 않습니다. 이 점은 아직 어떻게 대처해야 할지 난감한 부분입니다. 지금은 int(PHP 정수 )를 null 허용으로 설정했습니다. parquet-dotnet에서는 이를 null 허용 불가능으로 설정했습니다. ->hasNulls = true; 수동으로 설정하여 언제든지 이 동작을 조정할 수 있습니다. 귀하의 DataField에. 또한 php-parquet는 유형을 결정하는 두 가지 방법을 사용합니다. PHP에서 기본 요소는 고유한 유형(정수, 부울, 부동 소수점/이중 등)을 갖습니다. 클래스 인스턴스(특히 DateTime/DateTimeImmutable)의 경우 get_type()에서 반환되는 유형은 항상 object입니다. 이것이 일치, 결정 및 처리하기 위해 DataTypeHandler의 두 번째 속성인 phpClass가 존재하는 이유입니다.
이 글을 쓰는 시점에서 parquet-dotnet에서 지원하는 모든 DataType이 여기에서도 지원되는 것은 아닙니다. Fe Int16, SignedByte 등을 건너뛰었지만 전체 바이너리 호환성으로 확장하기에는 너무 복잡해서는 안 됩니다.
현재 이 라이브러리는 쪽모이 세공 파일/스트림을 읽고 쓰는 데 필요한 핵심 기능을 제공합니다. C# 네임스페이스 Parquet.Data.Rows 의 parquet-dotnet 테이블, 행, 열거자/도우미는 포함되지 않습니다.
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 매우 복잡한 스키마(fe 중첩 데이터) 작업에도 ParquetDataIterator 및 ParquetDataWriter 사용할 수 있습니다. 작성 당시에는 실험적이었지만 단위 및 통합 테스트에 따르면 대부분의 다른 Parquet 구현에는 특정 기능이나 매우 복잡한 중첩 사례가 부족하기 때문에 Spark와 100% 호환성이 있는 것으로 나타났습니다.
ParquetDataIterator 및 ParquetDataWriter 부호 없는 64비트 정수를 완전히 사용할 때만 중단되는 PHP 유형 시스템 및 (연관) 배열의 '동적성'을 활용합니다. 이는 PHP의 특성으로 인해 부분적으로만 지원될 수 있습니다.
ParquetDataIterator 가능한 가장 메모리 효율적인 방식으로 Parquet 파일의 모든 열과 모든 행 그룹 및 데이터 페이지를 자동으로 반복합니다. 즉, 모든 데이터 세트를 메모리에 로드하지 않고 데이터 페이지별/행 그룹별로 로드합니다.
내부적으로는 정의 및 반복 수준 과 관련된 모든 '무거운 작업'을 궁극적으로 수행하는 DataColumnsToArrayConverter 의 기능을 활용합니다.
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
} 반대로, ParquetDataWriter 사용하면 PHP 연관 배열 데이터를 한 번에 하나씩 또는 일괄적으로 전달하여 Parquet 파일(메모리 내 또는 디스크)을 작성할 수 있습니다. 내부적으로는 ArrayToDataColumnsConverter 사용하여 데이터, 사전, 정의 및 반복 수준을 생성합니다.
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는 Parquet 형식의 전체 중첩 기능을 지원합니다. 중첩하는 필드 유형에 따라 키 이름이 '손실'될 수 있습니다. 이는 의도적으로 설계된 것입니다.
일반적으로 다음은 Parquet 형식의 논리 유형에 해당하는 PHP입니다.
| 쪽매 세공 | PHP | JSON | 메모 |
|---|---|---|---|
| 데이터필드 | 원어 | 원어 | fe 문자열, 정수 등 |
| 목록필드 | 정렬 | 배열 [] | 요소 유형은 기본 요소일 수도 있고 List, Struct 또는 Map일 수도 있습니다. |
| 구조체 필드 | 연관 배열 | 물체 {} | 협회의 열쇠. 배열은 StructField 내부의 필드 이름입니다. |
| 지도 필드 | 연관 배열 | 물체 {} | 단순화: array_keys($data['someField']) 및 array_values($data['someField']) , 그러나 각 행에 대해 |
이 형식은 spark.conf.set("spark.sql.jsonGenerator.ignoreNullFields", False) 로 구성된 Spark에서 생성된 JSON 내보내기 데이터와 호환됩니다. 기본적으로 Spark는 JSON으로 내보낼 때 null 값을 완전히 제거합니다.
참고: 이러한 모든 필드 유형은 모든 중첩 수준에서 null을 허용하거나 null을 허용하지 않거나 필수로 만들 수 있습니다 (정의 수준에 영향을 미침). 일부 null 허용 가능성은 빈 목록을 나타내고 목록의 null 값과 구별하기 위해 fe로 사용됩니다.
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. 이 패키지는 parquet-dotnet과 동일한 벤치마크도 제공합니다. 내 컴퓨터 의 결과는 다음과 같습니다.
| Parquet.Net(.NET 코어 2.1) | php-parquet(베어메탈 7.3) | php-parquet(dockerized* 7.3) | Fastparquet(파이썬) | parquet-mr(자바) | |
|---|---|---|---|---|---|
| 읽다 | 255ms | 1'090ms | 1'244ms | 154ms** | 테스트되지 않은 |
| 쓰기(비압축) | 209ms | 1'272ms | 1'392ms | 237ms** | 테스트되지 않은 |
| 쓰기(gzip) | 1'945ms | 3분 314초 | 3분 695초 | 1'737ms** | 테스트되지 않은 |
일반적으로 이러한 테스트는 php-parquet에 대해 gzip 압축 레벨 6을 사용하여 수행되었습니다. 1(최소 압축)에서는 대략 절반으로 줄어들고 9(최대 압축)에서는 거의 두 배가 됩니다. 후자는 가장 작은 파일 크기를 생성하지 않을 수 있지만 항상 가장 긴 압축 시간을 생성합니다.
이는 완전히 다른 프로그래밍 언어에서 패키지를 부분적으로 포트한 것이므로 프로그래밍 스타일은 거의 엉망입니다. 나는 parquet-dotnet에 대한 특정 '시각적 호환성'을 유지하기 위해 대부분의 케이싱(예: ->createRowGroup() 대신 $writer->CreateRowGroup())을 유지하기로 결정했습니다. 적어도 내 관점에서는 이것이 바람직한 상태인데, 초기 개발 단계에서 비교와 확장이 훨씬 쉬워지기 때문이다.
일부 코드 부분과 개념은 C#/.NET에서 이식되었습니다. 다음을 참조하세요.
php-parquet은 MIT 라이선스에 따라 라이선스가 부여됩니다. 파일 라이선스를 참조하세요.
원한다면 자유롭게 PR을 해보세요. 이것은 여가용 OSS 프로젝트이므로 기여는 귀하를 포함하여 이 패키지의 모든 사용자에게 도움이 될 것입니다. PR 및/또는 이슈를 작성할 때 약간의 상식을 적용하십시오. 템플릿은 없습니다.