Apache ORC is a columnar file format from the Hive ecosystem. This page walks through its physical layout, how its layered statistics let engines skip data, how it encodes columns, and how it compares with Parquet and Avro.
Apache ORC (Optimized Row Columnar) is an open-source columnar file format for analytic data. It was created in 2013 within the Apache Hive project, by Hortonworks working with Facebook, to replace Hive's older RCFile format, and became a top-level Apache project in 2015. Hive, Spark, Trino, Presto and Impala can all read it, and Apache Iceberg supports it as a data file format alongside Parquet and Avro.
Like Parquet, ORC stores each column's values together instead of storing whole rows together, which is why it compresses well and lets queries read only the columns they need. If that idea is new, read Row vs Columnar Storage first.
Analytic queries typically scan many rows but touch few columns, and filter heavily. ORC is built for exactly that: column pruning skips columns, built-in indexes skip row groups, and type-aware encodings shrink the bytes that remain. For a Hive-centric warehouse it was historically the default and best-supported format, including Hive's ACID transactional tables, which are implemented on ORC. Knowing ORC also helps you reason about Parquet, since the two share most ideas and differ in the details.
An ORC file is a sequence of stripes followed by a file footer and a postscript. Stripes are large (the default target is 64 MB) so that reads are long sequential I/O. Each stripe holds three parts:
The file footer stores the schema, the list of stripes with their offsets and row counts, and file-level statistics per column. Stripe-level statistics live in the file metadata section. Because the postscript sits at the very end and its length is stored in the final byte, a reader opens the file by reading the tail, then decides which stripes and streams to fetch.
This three-level statistics hierarchy (file, stripe, row group) is what makes predicate pushdown effective: the engine passes the query's filter to the ORC reader, which eliminates whole stripes or row groups whose ranges cannot match. Pushdown works best when data is sorted or clustered on the filter column, because then each row group covers a narrow range.
Within a stream ORC applies type-specific encodings first: run-length and delta encoding with bit-packing for integers, dictionary encoding for strings with few distinct values, a bit-per-row PRESENT stream for nulls. A general codec (zlib by default, or Snappy, LZ4, Zstandard) then compresses the result in chunks so that seeks remain possible.
PyArrow can write and read ORC directly. The example writes a sorted table, reads only two columns, and uses a filter so the reader can skip stripes.
import numpy as np, pyarrow as pa, pyarrow.orc as orc, pyarrow.dataset as ds
n = 1_000_000
rng = np.random.default_rng(0)
table = pa.table({
"order_id": np.arange(n),
"country": rng.choice(["US", "DE", "JP", "BR"], n),
"amount": rng.gamma(2.0, 150.0, n).round(2),
}).sort_by("amount") # clustering improves min/max skipping
orc.write_table(table, "orders.orc", compression="zstd", stripe_size=8 * 1024 * 1024)
f = orc.ORCFile("orders.orc")
print("stripes:", f.nstripes, "rows:", f.nrows, "schema:", f.schema)
# Column pruning: only two columns are decoded
small = orc.read_table("orders.orc", columns=["country", "amount"])
print(small.num_columns)
# Dataset API with a filter expression
dset = ds.dataset("orders.orc", format="orc")
big = dset.to_table(columns=["order_id", "amount"], filter=ds.field("amount") > 900)
print("rows > 900:", big.num_rows)In Spark the equivalent is df.write.format("orc").option("compression","zstd").save(path); with spark.sql.orc.filterPushdown enabled (the default in recent versions) filters are pushed to the reader. In Hive, CREATE TABLE ... STORED AS ORC TBLPROPERTIES ("orc.compress"="ZSTD", "orc.bloom.filter.columns"="customer_id") configures compression and Bloom filters.
Inspect a file before tuning: orc-tools meta file.orc (from the Java ORC tools) or orc-metadata (from the C++ library) prints stripes, encodings and statistics, so you can see whether ranges are narrow enough to skip and whether dictionaries were used.
| Aspect | ORC | Parquet | Avro |
|---|---|---|---|
| Layout | Columnar: stripes > streams | Columnar: row groups > column chunks > pages | Row-oriented blocks |
| Origin | Hive (Hortonworks, Facebook) | Twitter and Cloudera, Dremel-inspired | Hadoop project, schema-first serialization |
| Built-in skipping | File, stripe and row-group stats; optional Bloom filters | Row-group and page stats, page index; optional Bloom filters | None (sync markers only) |
| Nested data | Struct, list, map, union types | Dremel repetition/definition levels | Records, arrays, maps, unions |
| Ecosystem default | Hive, Hive ACID tables | Spark, Delta Lake, most cloud warehouses and pandas | Kafka, streaming ingestion |
| Best for | Hive-centric warehouses | General analytic lakes | Write-heavy row-at-a-time pipelines |
In practice ORC and Parquet perform similarly; the choice is usually decided by ecosystem support. Parquet has broader support outside Hadoop, so new lakes usually default to it, while ORC remains common where Hive is central.
pyarrow.orc module.