YugabyteDB supports the PostgreSQL pgcrypto extension for column-level encryption. But encrypting a column raises an interesting sizing question:
- How much larger does the data become after it is encrypted?
The answer depends on several things:
- ● The size of the original value.
- ● The original data type.
- ● Whether symmetric or public-key encryption is used.
- ● Whether PGP compression is enabled.
- ● Whether the encrypted result is stored as raw
BYTEA, hexadecimal text, or ASCII-armored text.
The PGP encryption functions in pgcrypto return BYTEA. Internally, OpenPGP adds metadata around the original payload, including session-key information, random data similar to an IV, and integrity information. Public-key encryption additionally generates a random session key and encrypts that session key with the public key.
YugabyteDB includes pgcrypto as a pre-bundled extension, so it can be enabled with CREATE EXTENSION pgcrypto. YugabyteDB also documents both symmetric and public-key column-level encryption using this extension.
There is no single “pgcrypto adds X%” answer. PGP encryption has some relatively fixed overhead, so small values can grow substantially as a percentage of their original size. As the plaintext becomes larger, that fixed overhead becomes a much smaller percentage of the total.
How Does pgcrypto Encryption Work?
For PGP encryption, pgcrypto supports both symmetric encryption and public-key encryption.
With symmetric encryption, a password is processed using a String-to-Key algorithm and used to derive encryption key material.
With public-key encryption, pgcrypto generates a random session key, encrypts the data using that session key, and then encrypts the session key using the supplied public key.
In both cases, the PGP format adds overhead beyond the original plaintext. PostgreSQL documents that the data may be compressed, is given a random prefix and integrity information, then encrypted and packaged into PGP packets.
Here are some of the approaches available through pgcrypto:
| Method | Function | Output | Size Consideration |
|---|---|---|---|
| Symmetric PGP | pgp_sym_encrypt() |
BYTEA |
Adds the OpenPGP envelope and key-derivation metadata. |
| Public-Key PGP | pgp_pub_encrypt() |
BYTEA |
Also includes an encrypted session-key packet, so small values can have additional overhead. |
| ASCII Armor | armor() |
TEXT |
Adds Base64-style encoding, headers, formatting, and a checksum on top of the binary ciphertext. |
| Raw Cipher | encrypt() / encrypt_iv() |
BYTEA |
Less PGP packaging, but key, IV, integrity, and other cryptographic details must be managed manually. |
PostgreSQL specifically discourages using the raw encryption functions as a replacement for the PGP functions because they do not provide the PGP features such as integrity checking and require the caller to manage cryptographic parameters such as the IV.
For a complete example of generating a public/private PGP key pair and using pgp_pub_encrypt() and pgp_pub_decrypt() in YSQL, see Column Level Public-Key Encryption in YSQL.
The existing tip walks through GPG key generation, public-key encryption, and private-key decryption.
Demo
For this experiment, we’ll use symmetric PGP encryption. That keeps the demo self-contained and lets us concentrate specifically on how the size of the encrypted payload changes.
Start a Single-Node YugabyteDB Cluster
This demonstration only needs a single YugabyteDB node.
./bin/yugabyted start --advertise_address 127.0.0.1
Connect with ysqlsh:
./bin/ysqlsh -h 127.0.0.1 -p 5433 -U yugabyte
A single-node yugabyted deployment uses RF=1 and is intended for local development and testing rather than a production deployment.
Create a database for the experiment:
CREATE DATABASE pgcrypto_size_demo;
Connect to it:
\c pgcrypto_size_demo
Enable pgcrypto:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
Create a Random-Data Generator
Rather than requiring an external data generator, we can build a simple helper function using pgcrypto itself.
The function generates chunks of cryptographically random bytes, converts them to printable hexadecimal characters, and keeps appending them until the requested length has been reached:
CREATE OR REPLACE FUNCTION demo_random_text(p_length integer)
RETURNS text
LANGUAGE sql
VOLATILE
AS $$
SELECT left(
string_agg(encode(gen_random_bytes(32), 'hex'), ''),
p_length
)
FROM generate_series(
1,
greatest(1, ceil(p_length / 64.0)::integer)
);
$$;
Test it:
SELECT demo_random_text(80);
You should see an 80-character random value.
Create the Test Table
We’ll test several different cases:
- ● A very small
TEXTvalue. - ● A large 16 KB
TEXTvalue. - ● A native
DATE. - ● An approximately 8 KB
JSONdocument. - ● An approximately 16 KB
JSONBdocument.
Each plaintext column has a corresponding encrypted BYTEA column.
CREATE TABLE pgcrypto_size_test
(
id integer PRIMARY KEY,
text_small_original text,
text_small_encrypted bytea,
text_large_original text,
text_large_encrypted bytea,
date_original date,
date_encrypted bytea,
json_original json,
json_encrypted bytea,
jsonb_original jsonb,
jsonb_encrypted bytea
);
The PGP encryption functions return BYTEA. Storing the binary ciphertext directly avoids adding another text-encoding layer. Converting ciphertext to hexadecimal text approximately doubles the number of characters required, while ASCII armor adds Base64 and formatting overhead.
Create a Demo Encryption Key
For convenience, we’ll put the demonstration passphrase into a ysqlsh variable:
\set demo_key 'YugabyteDB-pgcrypto-size-demo'
This is just for the experiment.
Do not hard-code production encryption keys in application SQL, table definitions, stored procedures, or source code. Production applications should obtain encryption material from an appropriate secret-management or key-management system and should have a key-rotation strategy.
pgcrypto executes inside the database server. PostgreSQL therefore recommends using local or SSL/TLS connections and trusting the database and system administrators; otherwise encryption should be performed by the client application instead.
Generate and Encrypt 100 Rows
We’ll explicitly disable PGP compression for the first test so that we’re measuring the encryption overhead rather than mixing compression into the results.
AES-256 is used for this demonstration.
WITH source_data AS
(
SELECT
g AS id,
demo_random_text(32) AS text_small_original,
demo_random_text(16384) AS text_large_original,
DATE '2000-01-01'
+ floor(random() * 9500)::integer
AS date_original,
json_build_object(
'record_id', g,
'customer', demo_random_text(64),
'description', demo_random_text(4096),
'notes', demo_random_text(4096)
) AS json_original,
jsonb_build_object(
'record_id', g,
'customer', demo_random_text(64),
'payload', demo_random_text(16384),
'active', (random() > 0.5)
) AS jsonb_original
FROM generate_series(1, 100) AS g
)
INSERT INTO pgcrypto_size_test
(
id,
text_small_original,
text_small_encrypted,
text_large_original,
text_large_encrypted,
date_original,
date_encrypted,
json_original,
json_encrypted,
jsonb_original,
jsonb_encrypted
)
SELECT
id,
text_small_original,
pgp_sym_encrypt(
text_small_original,
:'demo_key',
'cipher-algo=aes256,compress-algo=0'
),
text_large_original,
pgp_sym_encrypt(
text_large_original,
:'demo_key',
'cipher-algo=aes256,compress-algo=0'
),
date_original,
pgp_sym_encrypt(
date_original::text,
:'demo_key',
'cipher-algo=aes256,compress-algo=0'
),
json_original,
pgp_sym_encrypt(
json_original::text,
:'demo_key',
'cipher-algo=aes256,compress-algo=0'
),
jsonb_original,
pgp_sym_encrypt(
jsonb_original::text,
:'demo_key',
'cipher-algo=aes256,compress-algo=0'
)
FROM source_data;
Verify that we inserted 100 rows:
SELECT count(*) FROM pgcrypto_size_test;
Why Are DATE, JSON, and JSONB Cast to TEXT?
The PGP text encryption function accepts TEXT, not arbitrary PostgreSQL/YSQL data types.
That means:
date_original::text
json_original::text
jsonb_original::text
are the values actually passed into pgp_sym_encrypt().
This creates an important distinction when measuring size.
For example, a native DATE is stored much more compactly internally than its printable value:
2026-08-20
So there are actually two questions we can ask:
- 1. How much larger is the ciphertext than the plaintext fed to
pgcrypto? - 2. How much larger is the encrypted column than the original native YSQL value?
We’ll measure both.
Measure the Encryption Overhead
Run:
WITH measurements AS
(
SELECT
1 AS sort_order,
'TEXT - 32 characters' AS data_type,
pg_column_size(text_small_original) AS source_column_bytes,
octet_length(text_small_original) AS plaintext_input_bytes,
pg_column_size(text_small_encrypted) AS encrypted_column_bytes,
octet_length(text_small_encrypted) AS ciphertext_bytes
FROM pgcrypto_size_test
UNION ALL
SELECT
2,
'TEXT - 16 KB',
pg_column_size(text_large_original),
octet_length(text_large_original),
pg_column_size(text_large_encrypted),
octet_length(text_large_encrypted)
FROM pgcrypto_size_test
UNION ALL
SELECT
3,
'DATE',
pg_column_size(date_original),
octet_length(date_original::text),
pg_column_size(date_encrypted),
octet_length(date_encrypted)
FROM pgcrypto_size_test
UNION ALL
SELECT
4,
'JSON - approximately 8 KB',
pg_column_size(json_original),
octet_length(json_original::text),
pg_column_size(json_encrypted),
octet_length(json_encrypted)
FROM pgcrypto_size_test
UNION ALL
SELECT
5,
'JSONB - approximately 16 KB',
pg_column_size(jsonb_original),
octet_length(jsonb_original::text),
pg_column_size(jsonb_encrypted),
octet_length(jsonb_encrypted)
FROM pgcrypto_size_test
)
SELECT
data_type,
round(avg(source_column_bytes), 1)
AS avg_source_column_bytes,
round(avg(plaintext_input_bytes), 1)
AS avg_plaintext_input_bytes,
round(avg(ciphertext_bytes), 1)
AS avg_ciphertext_bytes,
round(
avg(ciphertext_bytes - plaintext_input_bytes),
1
) AS avg_pgp_added_bytes,
round(
100.0 *
(
avg(ciphertext_bytes) /
NULLIF(avg(plaintext_input_bytes), 0)
- 1
),
2
) AS pgp_growth_pct,
round(
100.0 *
(
avg(encrypted_column_bytes) /
NULLIF(avg(source_column_bytes), 0)
- 1
),
2
) AS column_growth_pct
FROM measurements
GROUP BY sort_order, data_type
ORDER BY sort_order;
Actual Example Results
| Data Type | Source Column | Plaintext Input | Ciphertext | PGP Added | PGP Growth | Column Growth |
|---|---|---|---|---|---|---|
| TEXT (32 characters) | 36 bytes | 32 bytes | 98 bytes | 66 bytes | 206.25% | 183.33% |
| TEXT (16 KB) | 16,388 bytes | 16,384 bytes | 16,452 bytes | 68 bytes | 0.42% | 0.41% |
| DATE | 4 bytes | 10 bytes | 76 bytes | 66 bytes | 660.00% | 1900.00% |
| JSON (approximately 8 KB) | 8,328.9 bytes | 8,324.9 bytes | 8,392.9 bytes | 68 bytes | 0.82% | 0.82% |
| JSONB (approximately 16 KB) | 16,528 bytes | 16,512.3 bytes | 16,580.3 bytes | 68 bytes | 0.41% | 0.34% |
With compression disabled, this test showed remarkably consistent PGP overhead. The encrypted payload was only about 66–68 bytes larger than the plaintext supplied to pgcrypto. What changes dramatically is the percentage: adding 66 bytes to a 32-byte string is significant, while adding 68 bytes to a 16 KB value is almost negligible.
The results show why there isn’t a single percentage that can be used to estimate the storage impact of pgcrypto.
For the 32-character TEXT value, the ciphertext grew by more than 200% compared with the plaintext supplied to pgcrypto.
For the 16 KB TEXT value, however, the ciphertext increased by only 0.42%.
The same effect can be seen with the larger JSON and JSONB values.
The approximately 8 KB JSON value grew by only 0.82%, while the approximately 16 KB JSONB value grew by only 0.41% when comparing the ciphertext with the serialized plaintext.
In other words, the OpenPGP overhead becomes proportionally less important as the size of the value being encrypted increases.
Why Does DATE Grow So Much?
The DATE result is particularly interesting:
- ● Native DATE column: 4 bytes
- ● Plaintext sent to pgcrypto: 10 bytes
- ● Encrypted ciphertext: 76 bytes
- ● PGP growth: 660%
- ● Column growth: 1900%
A YSQL DATE occupies only 4 bytes, but pgp_sym_encrypt() does not encrypt the native DATE representation directly. The value is first converted to its 10-byte textual representation, such as 2026-08-20. The resulting PGP ciphertext averaged 76 bytes. When that encrypted value is compared with the original 4-byte DATE column, the apparent column-size increase becomes approximately 1900%.
This is a good example of why percentage growth can be misleading for very small native data types.
The encrypted date is 19 times larger than the original native DATE, but we’re still talking about only:
- 4 bytes → 76 bytes
The absolute difference is only 72 bytes.
PGP Growth vs. Column Growth
There are two different measurements in the results:
| Measurement | What It Tells Us |
|---|---|
pgp_growth_pct |
How much larger the PGP ciphertext is than the serialized plaintext actually supplied to pgcrypto. |
column_growth_pct |
How the YSQL representation of the encrypted BYTEA value compares with the original native column value. |
For TEXT, these numbers are fairly close because the original value is already text.
For types such as DATE, however, they can be dramatically different because the native value must first be converted to text before it is passed to pgcrypto.
An Interesting JSONB Result
There is another subtle result worth pointing out.
For JSONB, we measured:
- ● Native JSONB column: 16,528.0 bytes
- ● Serialized plaintext: 16,512.3 bytes
- ● Encrypted ciphertext: 16,580.3 bytes
- ● PGP growth: 0.41%
- ● Column growth: 0.34%
Notice that the column-growth percentage is actually slightly lower than the PGP-growth percentage.
That’s because JSONB has its own binary representation and structural metadata. In this particular dataset, the native JSONB representation was already slightly larger than the textual representation that was passed to pgcrypto.
Once a value is passed to pgp_sym_encrypt(), pgcrypto is primarily concerned with the bytes being encrypted. However, when comparing the encrypted column with the original column, the native YSQL representation also matters. A 4-byte DATE, TEXT value, JSON document, and JSONB document do not have identical native storage characteristics.
Don’t Confuse pg_column_size() with Actual YugabyteDB Disk Usage
The measurements above use functions such as:
pg_column_size()
and:
octet_length()
These are useful for understanding the relative size of the values being stored, but they should not be interpreted as the exact number of additional bytes that will appear in YugabyteDB SST files.
YugabyteDB ultimately stores YSQL data using DocDB and its LSM-based storage engine. Actual physical storage also includes factors such as:
- ● DocDB’s internal representation.
- ● Row and column metadata.
- ● RocksDB/SST block structures.
- ● Compression.
- ● Compaction.
- ● Replication factor.
- ● Indexes.
- ● Multiple versions of data that may temporarily exist in the LSM tree.
Encryption can also affect compression. Ciphertext is intentionally high-entropy data and generally does not compress as effectively as repetitive plaintext.
Use these SQL measurements to understand the logical expansion caused by column-level encryption, but don’t use them as an exact prediction of physical YugabyteDB disk consumption. For capacity planning, test representative application data at realistic scale and measure the resulting database storage.
What Did We Learn?
The results from this test are surprisingly straightforward.
With PGP compression disabled, pgp_sym_encrypt() added approximately the same amount of PGP overhead to every value we tested:
- Approximately 66–68 bytes
What changed dramatically was how significant those additional bytes were relative to the original data.
Final Takeaway
In this 100-row YugabyteDB test using pgp_sym_encrypt(), AES-256, and PGP compression disabled, pgcrypto added only about 66–68 bytes of PGP overhead to the serialized plaintext payload.
- ● A 32-byte TEXT value grew by about 206% at the PGP payload level.
- ● A native DATE showed about 1900% column growth when comparing its 4-byte native representation with the encrypted BYTEA column. At the PGP payload level, the 10-byte serialized DATE grew to 76 bytes, or about 660%.
- ● Approximately 8 KB of JSON grew by only 0.82%.
- ● Approximately 16 KB of TEXT grew by only 0.42%.
- ● Approximately 16 KB of JSONB grew by only 0.34% when comparing the encrypted column with the original native column.
The important lesson is that pgcrypto’s PGP overhead was relatively small in absolute bytes in this test, but the percentage can look enormous for very small values. For the larger TEXT, JSON, and JSONB values tested here, the PGP payload increase was less than 1%.
Have Fun!
Congrats to my wife! After 19 years as a nurse at one of Pittsburgh’s largest health networks, she is finally retiring!
This is the cake her boss got for her goodbye party at work.
Luckily for me, it was a little too sweet for her taste… which meant I got to eat most of the leftovers.
Retirement is already working out pretty well for me! 😂🎂
