Columnar Data, from the inside out Arrow overview →

One slot. Several buffers.

city / validity
0110213141516070
city / int32 offsets
00162636412516622
city / UTF-8 values
0c41b027a36d4695726e679d8b19e410ba11ac124f1373146c156f16c417b0187a196d20692172

Slot 0: press Begin to follow these buffers

Start with the artefact

One column. Three buffers.

These are the original city buffers. Begin with a string, then follow the missing and empty values.

What Arrow is

Arrow specifies how typed columns are laid out in memory so that languages and engines can exchange arrays without rebuilding each value. Parquet encodes and compresses columns for storage; a reader decodes them into arrays that an engine can compute on. Arrow describes the validity, offsets and value buffers of those arrays. PyArrow, DuckDB and DataFusion produce and consume record batches. Arrow IPC carries these layouts between processes or stores them in files.

ParquetColumns stored as bytesIceberg · Delta LakeFiles selected by a versionArrowTyped arrays in memoryDuckDB · DataFusionOperators turn values into answers
Table metadata selects files. Readers decode stored columns into arrays. Engines compute on those arrays.
About this fixture

Original arrays written and independently read back with PyArrow 25.0.1. The browser decodes these recorded bytes. Native slices share their original buffers. IPC downloads preserve the same logical records; sizes describe their representation.

A record batch gathers rows from columns.

This batch has 6 rows and 6 columns. Every array has the same logical length, so slot 0 identifies the same record across all columns. Its city, ID and readings live in different buffers. A row emerges when the reader gathers those matching positions.

Selected original slot: 0. Follow it through the buffer diagrams below.

ColumnArrow typeSelected value
idint649007199254740993
city· selectedstring"İzmir"
readingslist<item: int32>[4, null, 7]
categorydictionary<values=string, indices=int8, ordered=0>"coast"
amountdecimal128(24, 2)12345678901234567890.12
momenttimestamp[us, tz=UTC]1700000000123456 microseconds since epoch
id900719925474099323456city"İzmir"null"""東京""Oslo""İzmir"readings[4, null, 7]null[][9][2, 3][]
One logical row gathers the same slot from each array. Lines follow the same identity between columns.

The schema supplies the interpretation: an integer width, a string layout, a list child type. A batch combines that schema with equally long arrays. Engines can pass a batch between operators while each operator chooses which columns it needs.

A null keeps its slot.

The city bitmap contains 3d. Bit 0 is one, so this slot is valid. Start at the least significant bit of the first byte and move right through logical slots. Packed bits let the reader check missingness separately from the value bytes.

City validity bits in logical slot order
011021314151
Positions above count bits.

Compare the missing city with the empty city from the opening. Both occupy a logical position. Only the missing city has a cleared validity bit. Counting the array's positions therefore includes nulls; counting valid values checks the bitmap.

An all-valid array can omit this buffer. The ID array does exactly that: every slot has an integer. Nested arrays carry validity at each level. A valid readings list can contain a null child, because the parent and child answer different questions.

Fixed width gives every value an address.

The ID type is int64. Each physical slot reserves eight bytes, so its address is the values-buffer start plus the slot number times eight. The selected logical slot maps to physical slot 0. Read its bytes in little-endian order.

Selected ID / original eight bytes
001100200300400500620700
Positions above count bytes.

The decoded integer is 9007199254740993. The first ID, 9007199254740993, exceeds JavaScript's exact Number integer range. This guide reads the signed buffer with bigint, preserving every bit before producing display text.

Direct addressing is the payoff of fixed width. A reader can locate a later slot by multiplication, without walking earlier values. Validity still comes first for a nullable fixed-width array: the reserved bytes belong to a slot even when its value is missing.

A string occupies the space between two offsets.

Variable-width strings need a second address calculation. Each slot has a start offset; the next slot's start also serves as its end. The final offset closes the final string, so the offsets buffer contains one more entry than the physical array has slots.

city / validity
0110213141516070
Positions above count bits.
city / int32 offsets
00162636412516622
Positions above count offsets.
city / UTF-8 values
0c41b027a36d4695726e679d8b19e410ba11ac124f1373146c156f16c417b0187a196d20692172
Positions above count bytes.

Slot 0: "İzmir"

Subtract neighboring offsets to get the byte length. The first city, "İzmir", occupies 6 UTF-8 bytes. The offsets advance over encoded bytes, while the decoded string has its own character count.

Try the null and empty slots. Both use equal neighboring offsets in this fixture. The bitmap resolves the ambiguity: one value is absent, the other is present with no characters. A reader that used only the difference between offsets would collapse those distinct values.

These int32 offsets can be read directly from the original little-endian buffer. The values buffer simply holds adjoining string bytes. String boundaries come from offsets, so delimiters and terminators are unnecessary.

A list points into another array.

The readings column uses the same start-and-end idea, but its offsets count child slots. A list's contents therefore inherit the child's type and validity. The parent first decides whether a list exists; its offsets then choose the child positions to gather.

readings / parent offsets
00132333445666
Positions above count offsets.
readings / decoded child slots
041null27394253
Positions above count slots.

Selected list: [4, null, 7]. Its offsets span child slots 0 through 3, excluding the end.

The first list is [4, null, 7]. Its parent is valid and its middle child is null. The next parent is null; the following parent is valid but empty. Each case needs both the parent bitmap and the child layout to preserve its meaning.

This separation lets a child itself be another nested array. A list of structs, for example, uses parent offsets to select struct slots, then follows the struct's children. Each level contributes its own addressing and validity rules.

A small index reaches a shared value.

The category array separates an index from its string dictionary. An index chooses a dictionary slot, then that string uses its own offsets and bytes. Repeated categories can share a dictionary value instead of repeating its string bytes in every row.

category / signed int8 indices
001120304150
Positions above count slots.
category / dictionary values
0coast1inland
Positions above count entries.

The selected category is "coast". Its own validity bitmap decides whether the index is meaningful. A null index stays null, even when the corresponding byte happens to be zero and dictionary entry zero contains a string.

A Parquet dictionary page participates in a storage encoding: pages store encoded dictionary indices for a column chunk. An Arrow dictionary array is already an in-memory typed array. Decoding Parquet can produce dictionary arrays or expand values, depending on the reader and requested representation.

A slice changes the start, keeping the buffers.

Switch the array view to the slice. Its first slot addresses original slot 1; its logical length is 3. The selected view currently has offset 0. Every bitmap and offset lookup adds that array offset before reading a position.

Original city array0: "İzmir"1: null2: ""3: "東京"4: "Oslo"5: "İzmir"Native slice0: null1: ""2: "東京"
A slice retains a window onto the original slots. Lines follow the same identity between columns.

The native verifier checks that the slice shares its original values buffer. Creating that view changes offset and length metadata, so it can avoid copying string contents. Those offset values still address the same original byte buffer.

Keeping a tiny slice can therefore retain a large backing allocation. Copying a selected range may be useful when releasing that larger buffer matters. The choice depends on ownership and lifetime as well as the cost of copying.

The type supplies the decimal point and the clock.

The amount array has type decimal128(24, 2). Its bytes hold signed integers; scale supplies the decimal point. To display a value, the reader preserves the integer's full width and then applies the scale. Floating-point conversion would lose precision before formatting began.

Selected amount / original bytes
01413a2203d840b53b6127ed842900100011001200130014001500
Positions above count bytes.

Selected amount: 12345678901234567890.12. Selected moment: 1700000000123456 microseconds since epoch.

The moment type, timestamp[us, tz=UTC], records a time unit and timezone. Its integer counts microseconds from the epoch. Displaying a calendar time requires that type information; the integer bytes alone cannot tell a reader which time unit to use.

Arrow carries both the bytes and their schema. Preserve both when handing a batch to another process. The SQL workbench also keeps exact database text beside typed inspection, so a displayed number can be checked against its original representation.

IPC puts the arrays into messages.

The IPC downloads serialize the same logical records. A schema message describes types; a dictionary batch supplies shared dictionary values; record-batch messages describe array buffers and carry their bodies. Readers rebuild typed arrays from that sequence.

IPC streamSchemaDictionary batchRecord batchesEnd of streamIPC fileSchemaDictionary batchRecord batchesFooter → batch locations
A stream proceeds through messages; a file adds a footer that locates batches. Lines follow the same identity between columns.

The file footer lets a reader locate record batches without replaying the stream from its beginning. The stream works for sequential transfer, including results arriving from a running producer. Both formats preserve the schema and the dictionary information their arrays need.

Open either download in the IPC inspector to move between its batches and inspect their buffers. Then compare the matching Parquet records: the logical values agree while the storage representation changes.

Parquet stores. Arrow supplies arrays.

QuestionParquetArrow
EncodingPages encode values for storage.Typed buffers expose array positions.
CompressionA reader decompresses encoded page bodies.In-memory arrays expose values; IPC can compress buffer bodies.
Random accessMetadata locates chunks and pages to decode.Slot arithmetic or offsets locate a value.
NestingDefinition and repetition levels encode structure.Child arrays, offsets and validity encode structure.

Use the distinction to trace an actual query. A scan locates Parquet pages, decompresses them and decodes their columns. The resulting arrays can feed filters and joins. Returning an Arrow batch preserves those typed columns at the next boundary.

These roles explain why a compact Parquet file can expand in memory and why an Arrow slice can be cheap. Storage size and the cost of reaching a value answer different questions. Start from the operation you need, then follow the representation it touches.

Inspect the original buffers.

Native array metadata and bytes · Pinned reproduction script · Apache Arrow format specification.

File sizes and SHA-256 hashes

sample.arrow: 2650 bytes
7f4477100d6fc8b37b1d4bb494f1351f0c68a2a8857e5b3e34d09866e8d3f32c

sample.arrows: 2056 bytes
c22c228f8927027d0aa97d017bf2391e02952a544070961a84091ef9cbd988bc

sample.parquet: 3508 bytes
d3776c1610be5d08bc0500866a3a14c0d1f8f28d6379851a04ec4895f2d7f3c4

Try the empty readings list and then the null readings list. Both span zero child slots. Which buffer changes their meaning?

Check the reasoning

The parent validity bitmap. A set bit gives the empty list a value; a cleared bit makes the other list null.

Your turn.

Open an IPC file or stream. Select a batch, choose a nullable column, and follow one slot through its buffers.