Skip to content
EL
Contents

How a table is stored on disk

Should invoices store their PDFs?

DecisionAdvanced3 min read15 min hands-on#storage#operations

The idea in 30 seconds

PostgreSQL can store files in a bytea column, and TOAST keeps them out of the way of ordinary queries. The costs show up elsewhere: every byte goes through the write-ahead log, every backup and every replica, and a careless SELECT * drags megabytes to the application. Weigh that against the one real advantage, files that change together with their rows in one transaction.

Helps to know first: Relational database concepts

On this page

The situation

Customers download their invoices as PDFs. Today the PDFs are generated on the fly. An AI assistant opened a pull request to store them.

Review taskStore invoice PDFs in the database

Pull request: “Store each invoice’s PDF in the invoices table so everything is in one place and backed up together.”

ALTER TABLE invoices ADD COLUMN pdf bytea;
# invoices.py
def list_invoices(customer_id):
    return db.query("SELECT * FROM invoices WHERE customer_id = %s ORDER BY issued_on DESC",
                    [customer_id])

Would you approve it? Write the review you would leave.

Your call

How to decide

Prefer object storage, with the key in a column, when files are large, numerous or growing without limit, or are served straight to browsers.

Prefer a bytea column when files are small and few, must change in the same transaction as their row, and you want one backup that covers everything.

Either way, never SELECT * from a table with large columns in code that does not need them.

Check yourself

  1. Why did summarising all invoices stay fast even with 400 MB of PDFs in the table?

    Show answer

    The PDFs were moved to the TOAST table. The main table held only the small columns and pointers, 144 kB, and a query that does not read pdf never touches the TOAST table.

  2. Why does EXPLAIN ANALYZE alone hide the cost of SELECT * here?

    Show answer

    Without SERIALIZE, EXPLAIN does not convert rows for the client, so it never fetches the out-of-line PDF values. EXPLAIN (ANALYZE, SERIALIZE) includes that work.

  3. Where else does a PDF stored in the database get copied?

    Show answer

    Into the write-ahead log, and from there to every replica, every point-in-time backup archive, and every base backup.

Finished reading?

Saved in this browser only.

Sources

Where each claim on this page comes from.

  1. 1.
    PostgreSQL documentation: 66.2 TOAST
    Primary· §66.2 TOAST

    Large values are moved out of line into a TOAST table; an update that does not change them leaves them as they are.

  2. 2.
    PostgreSQL documentation: EXPLAIN
    Primary· Parameters: SERIALIZE

    EXPLAIN SERIALIZE measures converting output for the client, including fetching TOASTed values from out-of-line storage.

  3. 3.
    PostgreSQL documentation: 28.3 Write-Ahead Logging (WAL)
    Primary· §28.3 Write-Ahead Logging (WAL)

    Changes are written to the write-ahead log before the data files.

  4. 4.
    PostgreSQL documentation: 25.3 Continuous Archiving and Point-in-Time Recovery
    Primary· §25.3

    Point-in-time recovery combines a base backup with archived WAL files.

  5. 5.
    PostgreSQL documentation: 26.2 Log-Shipping Standby Servers
    Primary· §26.2.5 Streaming Replication

    Streaming replication sends WAL records from the primary to standbys as they are generated.

Go further

What to read next, and why it is worth the time.

  1. 1.
    PostgreSQL documentation: 66.2 TOAST
    Primary· §66.2

    Explains the four storage strategies and when PostgreSQL compresses instead of moving values out of line.

Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.