Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Database Management Systems (DBMS)  Module 8 – Storage and file organisation

Row stores, column stores and compression

Why transactions suit row stores and analytics suit column stores, what PAX mixes, and how dictionary, run-length, delta and bit-packing shrink a column.

  • Advanced
  • 30 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: Three layouts of one table · Which workload each layout serves · Compression, column by column · Files built for columns: Parquet. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Contrast row (NSM), column (DSM) and hybrid (PAX) layouts for transactional and analytical workloads
  • Apply dictionary, run-length, bit-packing and delta encoding to a column and compute the encoded sizes
  • Explain Parquet's row groups, column chunks and statistics, and how a reader uses them to skip data
  • Measure the pages a row store reads for a single-column aggregate

Before you start

What you’ll learn · Before you start

What you will learn

  • Contrast row (NSM), column (DSM) and hybrid (PAX) layouts for transactional and analytical workloads
  • Apply dictionary, run-length, bit-packing and delta encoding to a column and compute the encoded sizes
  • Explain Parquet's row groups, column chunks and statistics, and how a reader uses them to skip data
  • Measure the pages a row store reads for a single-column aggregate

Before you start

On this page

Row stores keep all the values of a row together, so one page read returns whole rows; column stores keep all the values of a column together, so a query reads only the columns it needs. Transactional work touches a few rows and most of their columns, which suits row stores. Analytical queries scan millions of rows but only a few of their columns, which suits column stores, and a column of similar values also takes compression far better than a row of mixed ones. Between the two sits PAX, which keeps rows on the same page but groups each page’s values by column. This lesson measures what each layout reads, names the encodings that compress a column, and shows how Parquet files lay out columns for analytics.

Three layouts of one table

Four rows of id, city and amount stored as whole rows on a page, as one file per column, and as one page divided into a minipage per column.One table, three layoutsrows: (1, Pune, 120) (2, Kochi, 75) (3, Pune, 300) (4, Indore, 45)Row store (NSM): whole rows, one after another1 Pune 1202 Kochi 753 Pune 3004 Indore 45a page: every column of each row, side by sideColumn store (DSM): one file per columnid1 2 3 4cityPune Kochi Pune Indoreamount120 75 300 45PAX: rows on one page, grouped by column inside itid: 1 2 3 4city: Pune Kochi Pune Indoreamount: 120 75 300 45

The same four rows in a row store, a column store and PAX

Text description of the diagram

The table has four rows: (1, Pune, 120), (2, Kochi, 75), (3, Pune, 300) and (4, Indore, 45).

- Row store (NSM): one page holds the rows one after another, each with all its columns side by side: 1 Pune 120, then 2 Kochi 75, then 3 Pune 300, then 4 Indore 45. - Column store (DSM): each column is a file of its own. The id file holds 1, 2, 3, 4; the city file holds Pune, Kochi, Pune, Indore; the amount file holds 120, 75, 300, 45. The n-th value of every file belongs to the n-th row. - PAX: one page holds the same four rows, but inside the page the values are grouped by column into minipages: one for the ids, one for the cities and one for the amounts.

The N-ary storage model (NSM) is the slotted page of the earlier lesson: each record holds every column of its row, one record after another. Ailamaki and colleagues, who proposed PAX in 2001, note that relational systems had long organised records this way, and that most queries use only a fraction of each record.

The decomposition storage model (DSM) splits a table into one sub-relation per column. The n-th value of each column file belongs to the n-th row, so a query that needs two columns reads two files, and a query that needs a whole row has to collect it from every file.

PAX (partition attributes across) stores the same rows on each page as NSM would, but inside the page it groups the values of each column into a minipage. Pages and I/O are unchanged; what improves is what reaches the processor’s cache, because a scan of one column now reads consecutive values of that column.

Which workload each layout serves

The program prices three requests against a table of 10 million rows and 20 columns of 8 bytes each, stored as rows and as columns:

Pages touched by three requests, rows against columns Python · bytes_read.py
"""What a row layout and a column layout read for three typical requests.

A sales table of 10 million rows and 20 columns of 8 bytes each, stored in 8 KB
pages. A row store keeps each row's 160 bytes together (NSM); a column store keeps
each column's values together (DSM), one file per column. Page headers, slots and
compression are left out, so the numbers show the layouts alone.
"""
from math import ceil

ROWS, COLUMNS, WIDTH, PAGE = 10_000_000, 20, 8, 8192
rows_per_page = PAGE // (COLUMNS * WIDTH)          # whole rows per row-store page
values_per_page = PAGE // WIDTH                    # values per column-store page
row_pages = ceil(ROWS / rows_per_page)
column_pages = ceil(ROWS / values_per_page)        # pages of one column

requests = [
    ("sum(amount) by region over all rows", row_pages, 2 * column_pages),
    ("read one order by its key", 1, COLUMNS),
    ("insert one order", 1, COLUMNS),
]
print(f"{ROWS:,} rows x {COLUMNS} columns: row store {row_pages:,} pages ({rows_per_page} rows each), "
      f"column store {column_pages:,} pages per column")
print(f"{'request':<38}{'row store':>12}{'column store':>15}   pages touched")
for name, row_store, column_store in requests:
    print(f"{name:<38}{row_store:>12,}{column_store:>15,}")
gb = lambda pages: pages * PAGE / 10**9
print(f"bytes read by the aggregate: {gb(row_pages):.2f} GB against {gb(2 * column_pages):.2f} GB")

Output

10,000,000 rows x 20 columns: row store 196,079 pages (51 rows each), column store 9,766 pages per column
request                                  row store   column store   pages touched
sum(amount) by region over all rows        196,079         19,532
read one order by its key                        1             20
insert one order                                 1             20
bytes read by the aggregate: 1.61 GB against 0.16 GB

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bytes_read.py

The aggregate reads 196,079 pages from the row store and 19,532 from the column store, 1.61 GB against 0.16 GB, because it needs only 2 of the 20 columns. The other two requests go the opposite way: reading or inserting one order touches one page of the row store and one page of each of the 20 column files. That is the whole trade-off, before compression changes the numbers again in favour of columns.

Compression, column by column

A column holds values of one type, and often only a few distinct ones, so it compresses far better than a row that mixes a name, a date and a price. The encodings that column stores and columnar files use most:

  • Dictionary encoding replaces each value with a small integer code and stores each distinct value once.
  • Run-length encoding stores a run of equal neighbouring values as the value and the length of the run.
  • Bit-packing stores each integer in exactly as many bits as the largest value needs, instead of 32 or 64.
  • Delta encoding stores the difference between neighbouring values, which is small when the column is sorted.
  • Frame of reference stores each value minus the column’s minimum, so a narrow range needs few bits.

For example, the city column of the diagram, Pune, Kochi, Pune, Indore, has three distinct values: as a dictionary it becomes the codes 2, 1, 2, 0 of 2 bits each, one byte for all four rows, plus the three names once. Sort order decides how much each encoding saves, and the encodings can be combined: dictionary codes are themselves bit-packed or run-length encoded.

Files built for columns: Parquet

Parquet is a file format built on these ideas. Its documentation describes the hierarchy: a file holds one or more row groups; a row group, a horizontal slice of the rows, holds exactly one column chunk per column; and column chunks are divided into pages, the unit of encoding and compression. The metadata is written at the end of the file, after the data, and a reader reads it first to find the column chunks it needs. DuckDB, an analytical engine that needs no server and runs inside the process that uses it, reads and writes Parquet; its documentation says that it reads only the columns a query needs and can use the stored minimum and maximum values to skip parts of a file.

CSV / Excel to Parquet Converter Turn a CSV file into Parquet and compare the file sizes of the two layouts. Parquet, Avro & Arrow File Viewer Open a Parquet file to see its row groups, column chunks and the statistics stored for each column.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 A column holds 1,000,000 values drawn from 12 distinct cities. How many bytes do the dictionary codes take when each code uses as few bits as 12 values need, packed together?

    Type a number.

    Show the answer to question 1

    Answer: 500000 bytes

    12 values need 4 bits (3 bits hold only 8), so the codes take 1,000,000 × 4 ÷ 8 = 500,000 bytes, plus a dictionary of 12 names.

  2. Question 2 of 4 How many runs does run-length encoding produce for the column A, A, A, B, B, A, C, C?

    Type a number.

    Show the answer to question 2

    Answer: 4 runs

    (A, 3), (B, 2), (A, 1), (C, 2): a run ends whenever the value changes, so the A after the Bs starts a new run.

  3. Question 3 of 4 A query reads 2 of a table's 20 columns, all of the same width. What percentage of the row store's bytes does a column store read for it, ignoring compression and headers?

    Type a number.

    Show the answer to question 3

    Answer: 10 %

    The column store reads only the two columns' files, 2 ÷ 20 of the data. The row store must read every page, because each page holds all 20 columns of its rows.

  4. Question 4 of 4 An order-entry service inserts one order of 20 columns at a time, thousands of times a second, and reads orders back by id. Which layout suits it?

    Choose one answer.

    Show the answer to question 4

    Answer: A row store, which writes and reads each order in one page

    Each insert and each lookup touches one whole row, and a row store keeps a row in one place. A column store would touch a page in each of the 20 column files for every order.

Encodings worked byte by byte, with sizes you can check Pro: opens with a Pro, Premium or Ultimate pass

Every encoding stores the same values in fewer bytes only when the column has the shape it expects. This part measures run-length, dictionary, delta and frame-of-reference encoding on 100,000 sales rows, reproduces run-length encoding making a column larger than raw, and computes on compressed data without decoding it.

In this part:

  • Four encodings on one month of sales, measured
  • Bit-packing and deltas worked by hand
  • When run-length encoding backfires, reproduced
  • Computing on compressed data without decoding it
  • Exercise: compute encoded sizes
    • Compute the encoded size of a column

1 runnable example (Python) · 1 exercise with sample tests · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Row groups, statistics and late materialisation, measured Pro: opens with a Pro, Premium or Ultimate pass

A column store wins most when it does not read data at all. This part measures how much row-group statistics let a reader skip, counts what late materialisation saves, measures the pages that SQLite, a row store, reads for a single column, and closes with a choice of layout for each kind of workload.

In this part:

  • Skipping row groups with min and max, sorted against shuffled
  • Late materialisation: fetch only the rows that pass
  • A row store reading one column, measured in SQLite
  • Choosing a layout, with the trade-offs in one table

2 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    When does a column store beat a row store, and when does it lose?

    A column store keeps each column's values together, so a query that reads a few columns of many rows, such as a report that sums two of twenty columns over millions of rows, reads only those columns' files, often a tenth of the bytes or less, and similar values in a column compress well. A row store keeps each row's values together, so inserting one row, or reading one row by its key, touches a single page, while a column store touches a page in every column file. Column stores therefore win for analytics over wide tables, and lose for transactional work that writes and reads single rows, which is why many systems run both.

  2. Mid to senior

    A monthly report sums two columns of a 120-column PostgreSQL table of 300 million rows and takes 40 minutes. Estimate what it reads, explain why, and propose two designs that cut the time, with what each costs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A team writes daily event files in a columnar format. After a change to the writer, the same day's data takes 9 GB instead of 3 GB. Explain how sort order can change compressed size this much, and how you would choose a sort order.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Queries on a store of Parquet files filter on customer_id and match about 0.1 % of rows, yet they read nearly every byte of the column. The writer emits row groups of 10,000 rows in arrival order. What is wrong, and what would you change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Row stores keep rows together and suit transactions; column stores keep columns together and suit analytics that read few columns of many rows; PAX groups a page’s values by column without changing the page.
  • Dictionary, run-length, bit-packing, delta and frame-of-reference encodings shrink a column, and sort order decides which one works.
  • Parquet files hold row groups of column chunks of pages, with the metadata at the end of the file.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.