Größe

Um einen Vergleich vom gesparten Platz zu bekomen, habe ich identische Daten in eine Parquet- und eine CSV-Datei geschrieben. So sehen die Daten etwa aus:

┌──────────────────────────────────────┬─────────┬───────┬─────────────────┬────────────┬────────────────────────────┐
│                  id                  │  name   │ count │ additional_data │    date    │          dateTime          │
│                 uuid                 │ varchar │ int32 │     varchar     │    date    │  timestamp with time zone  │
├──────────────────────────────────────┼─────────┼───────┼─────────────────┼────────────┼────────────────────────────┤
│ e70cc671-caeb-49a8-915b-39aea51b3494 │ name_0  │     1 │ {"foo":"bar"}   │ 2026-01-17 │ 2026-01-17 18:11:54.21+01  │
│ ceace8b4-cdd9-4c4a-abb9-c2e5eb199427 │ name_1  │     2 │ {"foo":"bar"}   │ 2026-01-17 │ 2026-01-17 18:11:54.225+01 │
│ 2c2dfae8-0d41-45e0-aaca-190b21e70520 │ name_2  │     3 │ {"foo":"bar"}   │ 2026-01-17 │ 2026-01-17 18:11:54.228+01 │
│ 7933e804-ac48-4756-8ad9-af9ebe5d8378 │ name_3  │     4 │ {"foo":"bar"}   │ 2026-01-17 │ 2026-01-17 18:11:54.228+01 │
│ f1379882-1815-4952-8547-16519df47eee │ name_4  │     5 │ {"foo":"bar"}   │ 2026-01-17 │ 2026-01-17 18:11:54.228+01 │
└──────────────────────────────────────┴─────────┴───────┴─────────────────┴────────────┴────────────────────────────┘

Die CSV-Datei ist 11 MB groß. Die Parquet-Datei hingegen nur 1,7 MB. Selbst mit einer xz-Komprimierung bekommt man die CSV-Datei nur auf 2,4 MB verkleinert. Parquet profitiert hier vermutlich start von der binären Speicherung der UUIDs und der Zeitangaben.

UUID

Erst neuere Versionen von parquet-floor (> 1.60) unterstützen die Angabe von UUID. Um eine UUID semantisch ordentlich in einer Parquet-Datei zu speichern, muss der Typ wie folgt angegeben werden:

Types.required(FIXED_LEN_BYTE_ARRAY).length(16).as(LogicalTypeAnnotation.uuidType()).named("id")

Geschrieben werden die Werte folgendermaßen:

UUID uuid = UUID.randomUUID();
valueWriter.write("id", ByteBuffer.allocate(16)
                    .putLong(record.id.getMostSignificantBits())
                    .putLong(record.id.getLeastSignificantBits())
                    .array());

Datumsangaben

Eine Spalte für einen String legt man so an:

Types.required(BINARY).as(LogicalTypeAnnotation.stringType()).named("name")

Möchte man nun ein Datum abspeichern, könnte man auf die Idee kommen, das ähnlich zu machen, nur statt stringType() dateType() zu verwenden. Das funktioniert aber nicht, da die Annotation dateType() nur für INT32-Spalten verwendet werden darf. Würde man es trotzdem versuchen:

Types.required(BINARY).as(LogicalTypeAnnotation.dateType()).named("creation_date")

wird eine Exception ausgelöst (zum Glück gleich mit einer hilfreichen Nachricht) :

Exception in thread "main" java.lang.IllegalStateException: DATE can only annotate INT32

Richtig ist also diese Typangabe:

Types.required(INT32).as(LogicalTypeAnnotation.dateType()).named("creation_date")

Doch schon bald tritt das nächste Problem auf, wenn man nämlich versucht, an diese Stelle ein Date oder LocalDate Objekt zu schreiben. Der ParquetWriter erwartet nämlich für Spalten imt dem Typ INT32 nur Integer-Werte. Die Parquet-Dokumentation verrät, was zu tun ist. Man muss die Anzahl der Tage seit dem 1. Januar 1970 angeben. Bei einem LocalDate-Objekt ist das ziemlich einfach:

LocalDate date = LocalDate.now()
valueWriter.write("creation_date", (int)date.toEpochDay());

Bei einem Date-Objekt ist es ein klein wenig komplizierter:

Date date = new Date();
valueWriter.write("creation_date", (int)TimeUnit.MILLISECONDS.toDays(date.getTime()));

Nebenbemerkung: Da die Zahl nur positiv sein darf, ist dieser Datentyp für historische Datumsangaben nicht geeignet.

Zeitangaben

Ähnlich wie bei den Datumsangaben muss man bei Zeitangaben den passenden Basistyp und die richtige Annotation wählen. Man kann angeben, in welcher Genauigkeit man die Zeitangabe speichern möchte. Für meine Anwendungen reichen Millisekunden völlig aus. Dann kann man den Typ folgendermaßen definieren:

types.add(Types.required(INT64).as(LogicalTypeAnnotation.timestampType(true, LogicalTypeAnnotation.TimeUnit.MILLIS)).named("last_modification"));

Das Schreiben des Wertes bei einem LocalDateTime-Objekt geht so:

LocalDateTime timestamp = LocalDateTime.now();
valueWriter.write("dateTime", timestamp.atZone(ZoneId.systemDefault())
                    .toInstant()
                    .toEpochMilli());