ML data engineering
Storage Layers, File Formats, and Table Layout
Choose storage layers, formats, partitions, clustering, and immutable references for ML workloads.
By the end you can
- Distinguish raw, curated, analytical, and release storage layers
- Compare row, columnar, and table-format abstractions
- Design partitioning and layout from access and change patterns
- Identify small-file, overpartitioning, and mutable-path traps
Example
Physical layouts that become operational traps
Performance and correctness problems often share the same root design. The two most expensive ones have published price tags rather than cautionary tales.
- Tiny-file explosion: partitioning by minute and customer creates millions of files, and the metadata cost of that is a measured number. The Delta Lake paper put it plainly in 2020: “For example, S3’s LIST operations can only return up to 1000 objects per requests, and take tens to hundreds of milliseconds, so clients need to issue hundreds of LISTs in parallel to list large buckets or “directories”.” Amazon's own S3 API reference states the same ceiling from the other side: “By default, the action returns up to 1,000 key names. The response might contain fewer keys but will never contain more.” Five million files at a thousand keys per call is five thousand round trips of job planning. Not one row has been read yet.
- Mutable “latest” path: training references a directory whose contents change, making experiment reproduction impossible.
- Partial overwrite: a failed job replaces several partitions before crashing, and on an object store that is the default behaviour rather than bad luck. The Delta Lake team listed it in 2020 among the most common challenges their customers hit: “Any transaction that needs to write or update multiple objects risks having partial writes visible to other clients. Moreover, if such a transaction fails, data is left in a corrupt state.” Both halves are spelled out. While the job runs, “readers will see partial updates as the query updates each object individually”. When it dies, “if an update query crashes, the table is in a corrupted state”. The same paper puts a frequency on it: “Anecdotally, in the first few years of Databricks’ cloud service (2014–2016), around half the support escalations we received were due to data corruption, consistency or performance issues due to cloud storage strategies”. Half the escalations. This is not an edge case.
- Hidden retention conflict: raw files are deleted before delayed labels can be joined or an incident can be reconstructed.
- Overly coarse partition: every daily job scans years of data because the layout does not match the dominant filters.
Storage is part of the data contract
The rows a dataset logically holds are only part of what it is. Its file boundaries, partition scheme, snapshot mechanism, retention policy, and metadata determine whether consumers can read it consistently.
ML workloads often scan a few columns across a long history, and the size of that advantage has been measured rather than asserted. Google ran the same single-field aggregation over 85 billion records on 3,000 nodes, once record-wise and once by column. Its Dremel paper reported the gap in 2010: “Dremel and MR-on-columns read about 0.5TB of compressed columnar data vs. 87TB read by MR-on-records.” The same paper marks the limit of its own result. The win holds “when few columns are read, the gains of columnar representation are of about an order of magnitude”, while the crossover back to record-wise storage “often lies at dozens of fields”. Columnar layout is not a virtue. It is a bet on narrow projections, and the paper says where the bet stops paying.
Physical layout also creates risk. Over-partitioning produces tiny files, mutable paths weaken reproducibility, and manual overwrites can expose partial results to readers.
The goal is not to choose one universal format. It is to align storage with access patterns, update behavior, reproducibility, governance, and recovery.
Row groups have a recommended size, and it is a number rather than a principle. “We recommend large row groups (512MB - 1GB). Since an entire row group might need to be read, we want it to completely fit on one HDFS block.”, says the Apache Parquet documentation. Larger groups mean longer sequential reads. They are not free on the write side: “Larger groups also require more buffering in the write path (or a two pass write)”. For pages the advice inverts: “We recommend 8KB for page sizes”, small enough for fine-grained reading.
The engines people actually run default far below that recommendation. The Amazon Athena User Guide: “Note that Parquet and ORC are internally organized by row groups (Parquet) and stripes (ORC). The default size for row groups is 128 MB, and for stripes, 64 MB.” It adds that “Decreasing the row group or stripe size to less than their default values is not recommended.” A 2023 benchmark of the shipping implementations found the two formats do not even count in the same unit: “(non-Java) Parquet uses a row-group size based on the number of rows (e.g., 1M rows) whereas ORC uses fixed physical storage size (e.g., 64 MB).” Between 512MB - 1GB, 128 MB and 1M rows there is a real decision. Nobody has made it for you.
A reproducible dataset needs a stable logical snapshot, not merely a directory that once contained the right files.
Visual
Different layers preserve different kinds of value
A mature design avoids forcing every consumer to read raw sources or treating every derived table as permanent truth.
- 01
Raw landing
Preserves source payloads and delivery metadata for replay and investigation.
- 02
Canonical domain tables
Resolve source quirks into stable entities, events, units, and semantics.
- 03
ML-ready datasets
Publish point-in-time features, labels, splits, and snapshot metadata for a defined task.
- 04
Serving state
Stores low-latency feature values or materialized outputs needed by live systems.
- 05
Archive and deletion controls
Support retention, legal holds, reproducibility, and governed disposal.
Layers should have explicit promotion rules rather than informal copies named “final” or “final_v2.”
Comparison
Row-oriented, columnar, and table-format abstractions
These are different layers of design. They can be combined rather than treated as exclusive brands.
Row-oriented records keep the fields of one record together. That suits transactional reads and message exchange, and it makes analytical scans read fields nobody asked for. Columnar files group values by column, which is why the Dremel measurement above exists at all. Their limitation for ML is not only that file-level updates require rewrite strategies. It is width. Zeng and colleagues projected a fixed 10 randomly chosen columns out of tables of growing width, and reported what happened: “As the number of attributes (i.e., features) in the table grows, the metadata parsing overhead increases almost linearly even though the number of projection columns stays fixed. This is because the footer structures in Parquet and ORC do not support efficient random access.” Meta built a format around that constraint. Its Nimble README says the format “is better suited for workloads that are wide in nature, such as tables with thousands of columns (or streams) which are commonly found in feature engineering workloads and training tables for machine learning”, and lists among its choices “Use Flatbuffers instead of thrift/protobuf to more efficiently access large metadata sections.” A feature table with thousands of columns is exactly the shape the benchmark penalises.
The third abstraction is a mechanism, not a brand. A table becomes a tracked set of files: “This table format tracks individual data files in a table instead of directories”, says the Apache Iceberg spec. Publication becomes one swap: “Table state is maintained in metadata files. All changes to table state create a new metadata file and replace the old metadata with an atomic swap.” The spec's goals answer the partial-overwrite failure listed at the top of this lesson in one sentence: “Serializable isolation -- Reads will be isolated from concurrent writes and always use a committed snapshot of a table’s data. Writes will support removing and adding files in a single operation and are never partially visible.” Partitioning stops being directory structure: “Partitioning will be table configuration. Reads will be planned using predicates on data values, not partition values. Tables will support evolving partition schemes.” And this is not a specification waiting for an implementer. The Amazon Athena User Guide says “Iceberg manages large collections of files as tables, and it supports modern analytical data lake operations such as record-level insert, update, delete, and time travel queries”, that “Athena supports Apache Iceberg version 1.4.2”, and that “Athena only creates and operates on Iceberg v2 tables”. Check that version number against your engine before you promise anyone time travel.
Row-oriented records
Keep fields for one record together.
- Useful for transactional reads and message exchange
- Simple for record-by-record processing
- Analytical scans may read unused fields
- Example: JSON event payload or database row
Columnar files
Group values by column for analytical scans.
- Efficient projection and compression
- Good fit for training and aggregation workloads
- File-level updates require rewrite strategies
- Example: Parquet files in object storage
Analytical table format
Adds metadata, snapshots, transactions, and evolution over files.
- Supports consistent table snapshots and time travel
- Can evolve schema and partition specifications
- Requires catalog and metadata maintenance
- Example: an Iceberg-style table over columnar files
Key idea
Partition columns are not free indexes
A partition creates physical groups and metadata, and the platforms publish the ceiling. Google caps the table: “Each partitioned table can have up to 10,000 partitions. If you exceed this limit, consider using clustering in addition to, or instead of, partitioning.” The write side is capped too, at 4,000 partitions modified by a single job: “BigQuery rejects any query or load job that attempts to modify more than 4,000 partitions.” Time partitions are common. The grain you choose has to fit inside those numbers as well as match arrival volume and query windows.
There are floors as well as ceilings. Databricks tells users “With less than 1 TB of data, don't partition.” and “For partitions, verify that each partition contains at least 1 GB of data. Tables with fewer, larger partitions tend to outperform tables with many smaller partitions.” It then rules out precisely the fields that look most attractive in a filter: “Partitioning works well only for low or known cardinality fields (for example, date fields or physical locations), but not for fields with high cardinality such as timestamps.” The Amazon Athena User Guide states the consequence of ignoring that: “Having too many partition keys can result in fragmented datasets with too many files and files that are too small.” That is the tiny-file trap arriving by a second route.
High-cardinality lookups have their own tool, and it is not partitioning. “Bucketing is useful when you have a key with high cardinality and many of your queries look up specific values of the key.”, the same guide says, with the case that tempts everyone: “If the data is bucketed by user ID, Athena knows in advance which files contain records for a specific ID and which files do not.” Modern table formats can additionally hide partition transforms and evolve layouts without changing query syntax. The tradeoff underneath never moves. Pruning has to be worth what it costs to keep the metadata and the files in order.
Partition for coarse pruning and operational manageability, not for every column that appears in a filter.
Steps
Design the table from access and change patterns
Begin with queries and lifecycle behavior rather than copying the source layout. Step 4 is the one with published numbers to check against rather than taste: a partition count inside the platform's ceiling, a partition size above its recommended floor, and a row-group or stripe size no smaller than the engine's default.
1. List read patterns
Record filters, selected columns, history ranges, join keys, and serving latency needs.
2. Describe mutations
State whether data is append-only, corrected, deleted, compacted, or restated.
3. Choose snapshot semantics
Define how readers obtain one consistent version and reproduce it later.
4. Select partition and clustering keys
Use common pruning dimensions without creating excessive cardinality or small files.
5. Plan maintenance
Schedule compaction, metadata cleanup, retention, validation, and rollback.
The best layout is the one the team can query, evolve, compact, reproduce, and repair predictably.
Analogy
How a research library separates its layers
Original manuscripts, edited editions, subject indexes, reading-room copies, and an archive occupy separate parts of a research library. Each layer serves a different purpose.
Columnar layout resembles shelving pages by subject so readers can scan one topic efficiently. A table snapshot resembles a catalog edition that lists exactly which volumes belonged to the collection at a given time.
Files are rewritten while readers are still reading them. Transactional metadata is what keeps a reader from seeing a half-updated collection while a new snapshot is being published.
Parquet puts its catalogue at the back of the file for the same reason. “File metadata is written after the data to allow for single pass writing. Readers are expected to first read the file metadata to find all the column chunks they are interested in.”, says the Apache Parquet documentation, which also states that “The format is explicitly designed to separate the metadata from the data.” A writer therefore never has to seek backwards to fix up an index. The index is read first and written last, which is how a good archive behaves.
The archive charges admission for that arrangement, and the charge has been measured. Zeng and colleagues record that “The entry point of a Parquet/ORC file is called a footer” and that “Reading a Parquet file requires several round trips, including fetching the footer length, the footer, and lastly the column chunks.” Three round trips to open one file. That is why the tiny-file explosion at the top of this lesson is not only a listing problem.
Good storage separates preservation, curation, task-specific publication, and low-latency access.
Key takeaways
- Storage design affects analytical cost, consistency, evolution, replay, and experiment reproducibility: the same single-field aggregation over 85 billion records read about 0.5TB columnar against 87TB record-wise (Google's Dremel paper, 2010).
- Raw, canonical, ML-ready, serving, and archival layers preserve different kinds of value and should have explicit promotion rules.
- Columnar files optimize analytical access; table formats add snapshots, transactions, and evolution over file collections. The Apache Iceberg spec publishes each change by creating a new metadata file and replacing the old one with an atomic swap, and Amazon Athena ships that as time travel on Iceberg v2 tables.
- Partitioning should enable coarse pruning without creating unmanageable metadata or tiny-file explosions: BigQuery caps a table at 10,000 partitions and rejects jobs modifying more than 4,000, Databricks says don't partition below 1 TB and keep at least 1 GB per partition, and high-cardinality lookups belong to bucketing or clustering instead.
- Training runs should reference immutable snapshots or manifests rather than mutable “latest” locations; on an object store a multi-object update can leave partial writes visible to readers and, on failure, a corrupt table (the Delta Lake paper, 2020).
- Compaction, retention, metadata cleanup, and rollback are part of operating an ML dataset, not optional housekeeping — around half of the support escalations in the first few years of Databricks' cloud service (2014–2016) were data corruption, consistency or performance issues due to cloud storage strategies.