The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a bytea columnCREATE TABLE app.assets (asset_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, body bytea NOT NULL);View examples
Write a hex literalINSERT INTO app.assets (body) VALUES ('\x89504e47'::bytea);View examples
Decode text into bytesSELECT decode('89504e47', 'hex');View examples
Encode bytes as base64SELECT encode(body, 'base64') FROM app.assets WHERE asset_id = 42;View examples
Measure stored bytesSELECT octet_length(body) FROM app.assets WHERE asset_id = 42;View examples
Hash binary contentSELECT encode(digest(body, 'sha256'), 'hex') FROM app.assets WHERE asset_id = 42;View examples
Inspect datum storageSELECT pg_column_size(body), octet_length(body) FROM app.assets WHERE asset_id = 42;View examples
Set TOAST storage policyALTER TABLE app.assets ALTER COLUMN body SET STORAGE EXTERNAL;View examples
Create a large objectSELECT lo_from_bytea(0, decode('89504e47', 'hex'));View examples
Read a large-object rangeSELECT lo_get(content_oid, 0, 4096) FROM app.large_assets WHERE asset_id = 42;View examples
Write a large-object rangeSELECT lo_put(content_oid, 4096, decode('00010203', 'hex')) FROM app.large_assets WHERE asset_id = 42;View examples
Grant large-object read accessGRANT SELECT ON LARGE OBJECT 24680 TO asset_reader;View examples
Delete a large objectSELECT lo_unlink(24680);View examples
List large-object metadataSELECT oid, lomowner::regrole FROM pg_largeobject_metadata ORDER BY oid;View examples

PostgreSQL offers bytea for binary values stored as ordinary table columns and a separate large-object facility for transaction-scoped streaming through OID references. bytea usually gives simpler ownership, constraints, backup, and replication behavior; large objects help when applications must seek or stream values too large to handle conveniently as one datum. Choose deliberately, bind bytes through the driver, verify hashes and sizes, restrict file-access functions, and make lifecycle cleanup part of the schema rather than an afterthought.

Step by step

Detailed examples

01

Choose bytea by default and large objects for genuine streaming

A bytea value is an ordinary binary datum: it participates naturally in constraints, row privileges, COPY, logical replication, and row deletion, while TOAST can compress or store large values out of line. The large-object facility stores separately chunked data addressed by an OID and exposes seekable, stream-style client APIs. That avoids materializing the whole value but introduces separate privileges, explicit cleanup, transaction-bound descriptors, and replication caveats. For very large public media, object storage plus a database key may be operationally simpler than either option.

Model inline binary content with verified metadata
CREATE TABLE app.assets (
  asset_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  media_type text NOT NULL,
  byte_count bigint NOT NULL CHECK (byte_count >= 0),
  sha256 bytea NOT NULL CHECK (octet_length(sha256) = 32),
  body bytea NOT NULL,
  CHECK (octet_length(body) = byte_count)
);

Note: A CHECK that detoasts and measures very large values adds write cost. Benchmark the invariant or validate size and digest in a trusted ingestion service.

Back to quick reference ↑
02

Bind raw bytes and encode only at text boundaries

Binary strings contain arbitrary octets, including zero bytes, and are not interpreted through the database character encoding. PostgreSQL accepts hex and legacy escape input; hex is the preferred textual form and bytea_output controls display, not storage. Application drivers should use binary parameters and result APIs rather than constructing escaped SQL. encode and decode handle hex, base64, and escape representations when a textual transport is unavoidable; base64 expands the payload and can insert line breaks according to PostgreSQL's format rules.

Round-trip a short signature through textual encodings
WITH sample(bytes) AS (
  VALUES (decode('89504e470d0a1a0a', 'hex'))
)
SELECT encode(bytes, 'hex') AS hex_text,
       encode(bytes, 'base64') AS base64_text,
       octet_length(bytes) AS byte_count,
       decode(encode(bytes, 'base64'), 'base64') = bytes AS round_trip
FROM sample;

Note: Do not cast arbitrary bytea to text. Use convert_from only when bytes are known to be valid in the declared character encoding.

Back to quick reference ↑
03

Index compact identity metadata instead of full payloads

octet_length measures bytes, while bit_length and substring operate on binary data without locale rules. Hashes can provide compact lookup or integrity metadata, but pgcrypto must be installed and a hash is not proof of authenticity. A unique digest index can deduplicate exact content only after the application accepts collision and information-leak considerations; compare the actual bytes before treating a digest match as definitive in high-assurance systems. Keep searchable metadata in ordinary typed columns rather than repeatedly decoding blobs.

Store a digest once and verify it at ingestion
CREATE EXTENSION IF NOT EXISTS pgcrypto;

ALTER TABLE app.assets
ADD COLUMN content_sha256 bytea
GENERATED ALWAYS AS (digest(body, 'sha256')) STORED;

CREATE INDEX assets_sha256_idx
ON app.assets (content_sha256);

SELECT asset_id, encode(content_sha256, 'hex') AS sha256
FROM app.assets
WHERE content_sha256 = decode('dffd6021bb2bd32bafce2ea9b9f04865ee0c3e2f2657b283980e24a345bd1dbf', 'hex');

Note: Adding a stored generated column rewrites or computes existing rows and the index build consumes I/O. Stage this migration for large tables.

Back to quick reference ↑
04

Measure TOAST and update amplification before tuning storage

Large bytea values are normally compressed and/or stored out of line by TOAST, but they remain part of the row's transactional value. Selecting a row without referencing the binary column can avoid detoasting it, so keep list queries narrow. Replacing a large value creates a new version and WAL, replication, vacuum, and backup work. STORAGE EXTERNAL favors out-of-line uncompressed values and can help substring access for already-compressed formats, while EXTENDED is the general default. Changing the policy affects future values, not existing storage, and requires measurement.

Inspect logical and physical sizes without fetching content to the client
SELECT asset_id,
       octet_length(body) AS logical_bytes,
       pg_column_size(body) AS datum_bytes,
       pg_size_pretty(pg_column_size(body)::bigint) AS datum_size
FROM app.assets
ORDER BY pg_column_size(body) DESC
LIMIT 20;

SELECT pg_size_pretty(pg_total_relation_size('app.assets')) AS table_with_indexes_and_toast;

Note: Size functions can detoast values and become expensive at scale. Sample bounded rows and use relation-level statistics for routine monitoring.

Back to quick reference ↑
05

Stream large objects inside explicit short transactions

Large-object client descriptors exist only for the current transaction; write operations are forbidden in read-only transactions, and libpq large-object APIs are unavailable in pipeline mode. Use chunk sizes of at most a few megabytes, check every return code, and commit only after the reference row and content are consistent. Server-side lo_get and lo_put are convenient for bounded pieces but can still move large bytea values through SQL. The 64-bit seek and truncate interfaces are required beyond 2 GB and have existed since PostgreSQL 9.3.

Create the object and reference it atomically
BEGIN;

WITH created AS (
  SELECT lo_from_bytea(0, decode('89504e470d0a1a0a', 'hex')) AS content_oid
)
INSERT INTO app.large_assets (media_type, content_oid)
SELECT 'image/png', content_oid
FROM created
RETURNING asset_id, content_oid;

COMMIT;

Note: A real streaming client should create, write in chunks, insert the reference, and handle rollback on the same connection and transaction.

Back to quick reference ↑
06

Manage large-object privileges and deletion separately

Access to a row containing an OID does not automatically grant access to the referenced large object, and access to the object does not authorize its metadata row. Grant SELECT or UPDATE ON LARGE OBJECT narrowly and preserve ownership during migrations. Deleting or updating an OID reference does not delete the object, because references are not tracked as foreign keys; explicitly unlink it only after proving no other row refers to it. Server-side lo_import and lo_export access the server filesystem as the database OS account and are superuser-restricted by default; granting them is effectively a critical security capability.

Delete an owned reference and object in one transaction
BEGIN;

WITH removed AS (
  DELETE FROM app.large_assets
  WHERE asset_id = 42
  RETURNING content_oid
)
SELECT lo_unlink(content_oid)
FROM removed;

COMMIT;

Note: This assumes one reference owns one large object. If sharing is allowed, enforce a reference model and unlink only after a locked zero-reference check.

Back to quick reference ↑
07

Include large objects explicitly in migration and replication plans

Physical backup and streaming replication include both bytea and large objects because they copy the cluster's physical state. Logical publications replicate bytea table columns but do not publish large objects as table row changes, so a logical migration needs a separate large-object transfer and consistency boundary. pg_dump includes large objects by default in whole-database dumps, but selection switches and restore ownership options can change the result. Verify counts, owners, ACLs, hashes, and application references in restore rehearsals, and consult the exact major-version tool documentation before moving data.

Inventory large-object ownership before a migration
SELECT lomowner::regrole AS owner, count(*) AS object_count
FROM pg_largeobject_metadata
GROUP BY lomowner
ORDER BY owner::text;

SELECT count(*) AS referenced_objects,
       count(DISTINCT content_oid) AS distinct_references
FROM app.large_assets;

Note: Catalog counts do not prove referential completeness. Compare application references with metadata using a privileged, audited migration role.

Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupPostgreSQL: Binary Data Typespostgresql.org
  2. PostgreSQL Global Development GroupPostgreSQL: Binary String Functions and Operatorspostgresql.org
  3. PostgreSQL Global Development GroupPostgreSQL: Large Objectspostgresql.org
  4. PostgreSQL Global Development GroupPostgreSQL: Large Object Server-Side Functionspostgresql.org
  5. PostgreSQL Global Development GroupPostgreSQL: Large Object Client Interfacespostgresql.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback