Skip to content

Commit 894ce19

Browse files
authored
Allow to chose encoding for parquet columns (#1765)
* Allow user to defined column encoding * Move column chunk builders initializatio to dedicated factory
1 parent c638cbf commit 894ce19

10 files changed

Lines changed: 1205 additions & 25 deletions

File tree

documentation/components/libs/parquet.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,217 @@ If you want to achieve the best compression, you should use `GZIP` or `SNAPPY` w
307307

308308
For not yet supported algorithms, please check our [Roadmap](https://github.com/orgs/flow-php/projects/1) to understand when they will be supported.
309309

310+
## Column Encodings
311+
312+
Parquet supports various column encoding algorithms that can significantly impact file size and query performance.
313+
You can specify custom encodings for individual columns using flat path notation.
314+
315+
### Available Encodings
316+
317+
#### PLAIN
318+
The default encoding that stores values as-is without any compression scheme.
319+
320+
**When to use:**
321+
- Small datasets where compression overhead isn't justified
322+
- Columns with high cardinality and random distribution
323+
- When you need maximum compatibility with other Parquet readers
324+
325+
**Supported types:** All column types
326+
327+
```php
328+
use Flow\Parquet\Options;
329+
use Flow\Parquet\Option;
330+
331+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
332+
'description' => 'PLAIN',
333+
'uuid' => 'PLAIN'
334+
]);
335+
```
336+
337+
#### RLE_DICTIONARY
338+
Run Length Encoding with Dictionary compression. Values are stored in a dictionary and replaced with indices.
339+
340+
**When to use:**
341+
- Columns with low cardinality (many repeated values)
342+
- String columns with repeated categories (status, country, department)
343+
- Enumeration-like data
344+
- Significant file size reduction (often 50-90% smaller)
345+
346+
**Supported types:** All types except `FIXED_LEN_BYTE_ARRAY`
347+
348+
```php
349+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
350+
'status' => 'RLE_DICTIONARY', // 'active', 'inactive', 'pending'
351+
'country_code' => 'RLE_DICTIONARY', // 'US', 'UK', 'DE', 'FR'
352+
'department' => 'RLE_DICTIONARY' // 'engineering', 'sales', 'marketing'
353+
]);
354+
```
355+
356+
#### DELTA_BINARY_PACKED
357+
Delta encoding with binary packing for integer columns. Stores differences between consecutive values.
358+
359+
**When to use:**
360+
- Sequential or monotonic integer data (IDs, timestamps, counters)
361+
- Time series data with incremental values
362+
- Can achieve 70-95% compression for sequential data
363+
364+
**Supported types:** Only `INT32` and `INT64`
365+
366+
```php
367+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
368+
'user_id' => 'DELTA_BINARY_PACKED', // 1, 2, 3, 4, 5...
369+
'timestamp_ms' => 'DELTA_BINARY_PACKED', // 1634567890123, 1634567890124...
370+
'order_number' => 'DELTA_BINARY_PACKED' // Sequential order IDs
371+
]);
372+
```
373+
374+
### Using Custom Encodings
375+
376+
#### Basic Column Encoding
377+
378+
```php
379+
use Flow\Parquet\{Writer, Options, Option};
380+
use Flow\Parquet\ParquetFile\Schema;
381+
use Flow\Parquet\ParquetFile\Schema\FlatColumn;
382+
383+
$schema = Schema::with(
384+
FlatColumn::int64('user_id'),
385+
FlatColumn::string('status'),
386+
FlatColumn::string('description')
387+
);
388+
389+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
390+
'user_id' => 'DELTA_BINARY_PACKED', // Sequential IDs
391+
'status' => 'RLE_DICTIONARY', // Limited set of values
392+
'description' => 'PLAIN' // High variance text
393+
]);
394+
395+
$writer = new Writer(compressions: Compressions::SNAPPY, options: $options);
396+
```
397+
398+
#### Nested Column Encoding (Flat Path Notation)
399+
400+
For nested structures, use dot notation to specify the exact column path.
401+
The flat path follows Parquet's internal structure conventions:
402+
403+
**Flat Path Patterns:**
404+
- **Struct fields**: `parent.field_name`
405+
- **List elements**: `list_name.list.element`
406+
- **Map keys**: `map_name.key_value.key`
407+
- **Map values**: `map_name.key_value.value`
408+
409+
```php
410+
use Flow\Parquet\ParquetFile\Schema\{NestedColumn, ListElement, MapKey, MapValue};
411+
412+
$schema = Schema::with(
413+
NestedColumn::struct('user', [
414+
FlatColumn::int64('id'),
415+
FlatColumn::string('name'),
416+
NestedColumn::struct('address', [
417+
FlatColumn::string('street'),
418+
FlatColumn::string('city'),
419+
FlatColumn::string('country')
420+
])
421+
]),
422+
NestedColumn::list('tags', ListElement::string()),
423+
NestedColumn::map('metadata', MapKey::string(), MapValue::string())
424+
);
425+
```
426+
427+
**Understanding Flat Paths:**
428+
429+
```php
430+
// STRUCT: Direct field access with dot notation
431+
'user.id' // user struct → id field
432+
'user.name' // user struct → name field
433+
'user.address.street' // user struct → address struct → street field
434+
'user.address.city' // user struct → address struct → city field
435+
'user.address.country' // user struct → address struct → country field
436+
437+
// LIST: Always includes intermediate '.list.element' structure
438+
'tags.list.element' // tags list → list wrapper → element (the actual string values)
439+
440+
// MAP: Always includes intermediate '.key_value' structure
441+
'metadata.key_value.key' // metadata map → key_value wrapper → key (string keys)
442+
'metadata.key_value.value' // metadata map → key_value wrapper → value (string values)
443+
```
444+
445+
**Applying Custom Encodings:**
446+
447+
```php
448+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
449+
// Struct fields - direct access
450+
'user.id' => 'DELTA_BINARY_PACKED',
451+
'user.name' => 'RLE_DICTIONARY',
452+
'user.address.street' => 'PLAIN',
453+
'user.address.city' => 'RLE_DICTIONARY',
454+
'user.address.country' => 'RLE_DICTIONARY',
455+
456+
// List elements - note the '.list.element' suffix
457+
'tags.list.element' => 'RLE_DICTIONARY',
458+
459+
// Map key/value pairs - note the '.key_value.key/value' suffix
460+
'metadata.key_value.key' => 'RLE_DICTIONARY',
461+
'metadata.key_value.value' => 'PLAIN'
462+
]);
463+
```
464+
465+
**Complex Nested Example:**
466+
467+
```php
468+
// Complex nested structure with lists of structs and maps
469+
$schema = Schema::with(
470+
NestedColumn::list('orders', ListElement::structure([
471+
FlatColumn::int64('order_id'),
472+
FlatColumn::string('status'),
473+
NestedColumn::map('attributes', MapKey::string(), MapValue::string())
474+
]))
475+
);
476+
477+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
478+
// List of structs: list_name.list.element.field_name
479+
'orders.list.element.order_id' => 'DELTA_BINARY_PACKED',
480+
'orders.list.element.status' => 'RLE_DICTIONARY',
481+
482+
// Map inside list element: list_name.list.element.map_name.key_value.key/value
483+
'orders.list.element.attributes.key_value.key' => 'RLE_DICTIONARY',
484+
'orders.list.element.attributes.key_value.value' => 'PLAIN'
485+
]);
486+
```
487+
488+
#### Mixed Encoding Strategy
489+
490+
```php
491+
$options = Options::default()->set(Option::COLUMNS_ENCODINGS, [
492+
// High cardinality sequential data
493+
'order_id' => 'DELTA_BINARY_PACKED',
494+
'created_timestamp' => 'DELTA_BINARY_PACKED',
495+
496+
// Low cardinality categorical data
497+
'order_status' => 'RLE_DICTIONARY',
498+
'payment_method' => 'RLE_DICTIONARY',
499+
'shipping_country' => 'RLE_DICTIONARY',
500+
501+
// High variance descriptive data
502+
'customer_notes' => 'PLAIN',
503+
'product_description' => 'PLAIN'
504+
]);
505+
```
506+
507+
### Encoding Compatibility
508+
509+
| Encoding | INT32/INT64 | BYTE_ARRAY | BOOLEAN | FLOAT/DOUBLE | FIXED_LEN_BYTE_ARRAY |
510+
|----------|-------------|------------|---------|--------------|----------------------|
511+
| PLAIN ||||||
512+
| RLE_DICTIONARY ||||||
513+
| DELTA_BINARY_PACKED ||||||
514+
515+
### Performance Guidelines
516+
517+
1. **Analyze your data first** - Check cardinality and distribution patterns
518+
2. **Use RLE_DICTIONARY for categorical data** - Countries, statuses, departments
519+
3. **Use DELTA_BINARY_PACKED for sequential integers** - IDs, timestamps, counters
520+
4. **Use PLAIN for high-variance data** - Descriptions, UUIDs, random data
521+
5. **Test different combinations** - Measure file size and query performance
522+
6. **Consider query patterns** - Frequently filtered columns benefit from dictionary encoding
523+

src/lib/parquet/src/Flow/Parquet/Option.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ enum Option
2121
*/
2222
case BYTE_ARRAY_TO_STRING;
2323

24+
/**
25+
* Per-column encoding configuration using flat path notation.
26+
* Accepts array<string, string> where key is column flat path and value is encoding name.
27+
*
28+
* Flat path examples:
29+
* - Simple columns: 'column_name'
30+
* - Nested structs: 'user.address.street'
31+
* - List elements: 'items.list.element'
32+
* - Map keys/values: 'metadata.key_value.key', 'metadata.key_value.value'
33+
*
34+
* Supported encodings: PLAIN, RLE_DICTIONARY, DELTA_BINARY_PACKED
35+
*
36+
* Default: null (use automatic encoding selection)
37+
*/
38+
case COLUMNS_ENCODINGS;
39+
2440
/**
2541
* Whenever cardinality ratio of the dictionary goes below this value, PagesBuilders is going to fallback to PLAIN encoding.
2642
* Cardinality ration is calculated as distinct values / total values.

src/lib/parquet/src/Flow/Parquet/Options.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use Flow\Filesystem\SizeUnits;
88
use Flow\Parquet\Exception\InvalidArgumentException;
9+
use Flow\Parquet\Options\ColumnsEncodings;
10+
use Flow\Parquet\ParquetFile\Encodings;
911

1012
final class Options
1113
{
1214
/**
13-
* @var array<string, bool|float|int>
15+
* @var array<string, null|array<mixed>|bool|ColumnsEncodings|float|int>
1416
*/
1517
private array $options;
1618

@@ -33,6 +35,7 @@ public function __construct()
3335
Option::ZSTD_COMPRESSION_LEVEL->name => 3,
3436
Option::WRITER_VERSION->name => 1,
3537
Option::VALIDATE_DATA->name => true,
38+
Option::COLUMNS_ENCODINGS->name => null,
3639
];
3740
}
3841

@@ -41,7 +44,10 @@ public static function default() : self
4144
return new self;
4245
}
4346

44-
public function get(Option $option) : bool|int|float
47+
/**
48+
* @return null|array<mixed>|bool|ColumnsEncodings|float|int
49+
*/
50+
public function get(Option $option) : bool|int|float|array|ColumnsEncodings|null
4551
{
4652
return $this->options[$option->name];
4753
}
@@ -57,6 +63,21 @@ public function getBool(Option $option) : bool
5763
return $value;
5864
}
5965

66+
public function getColumnsEncodings() : ?ColumnsEncodings
67+
{
68+
$value = $this->options[Option::COLUMNS_ENCODINGS->name] ?? null;
69+
70+
if ($value === null) {
71+
return null;
72+
}
73+
74+
if ($value instanceof ColumnsEncodings) {
75+
return $value;
76+
}
77+
78+
throw new InvalidArgumentException('Option COLUMNS_ENCODINGS is not a ColumnsEncodings instance, but: ' . \gettype($value));
79+
}
80+
6081
public function getInt(Option $option) : int
6182
{
6283
$value = $this->options[$option->name];
@@ -68,9 +89,32 @@ public function getInt(Option $option) : int
6889
return $value;
6990
}
7091

71-
public function set(Option $option, bool|int|float $value) : self
92+
public function has(Option $option) : bool
93+
{
94+
$value = $this->options[$option->name] ?? null;
95+
96+
return $value !== null;
97+
}
98+
99+
/**
100+
* @param null|array<mixed>|bool|ColumnsEncodings|float|int $value
101+
*/
102+
public function set(Option $option, bool|int|float|array|ColumnsEncodings|null $value) : self
72103
{
73-
$this->options[$option->name] = $value;
104+
if ($option === Option::COLUMNS_ENCODINGS) {
105+
if ($value === null) {
106+
$this->options[$option->name] = null;
107+
} elseif ($value instanceof ColumnsEncodings) {
108+
$this->options[$option->name] = $value;
109+
} elseif (\is_array($value)) {
110+
/** @var array<string, Encodings|string> $value */
111+
$this->options[$option->name] = ColumnsEncodings::fromArray($value);
112+
} else {
113+
throw new InvalidArgumentException('Option COLUMNS_ENCODINGS must be an array, ColumnsEncodings instance, or null, got: ' . \gettype($value));
114+
}
115+
} else {
116+
$this->options[$option->name] = $value;
117+
}
74118

75119
return $this;
76120
}

0 commit comments

Comments
 (0)