What stops working when an application encrypts a column's values before storing them in a relational database, and how do teams work around each limitation?
answer
- randomized = safe, kills equality; deterministic = searchable, leaks frequency
- blind index = HMAC of normalised plaintext, truncate to blur
- ranges: coarse bucket + app-side filter
- version every ciphertext for online key rotation
- AEAD + associated data stops row-swap attacks
basics
~20 sThe engine can no longer interpret the value: range queries, sorting, pattern search, joins, uniqueness and server-side functions break, and values grow. Deterministic encryption restores equality lookups but leaks which rows are equal; keyed hashes give searchable blind indexes; ranges move into coarse buckets or the application.
solid answer
~60 sEncrypting above the database turns a typed column into opaque bytes, so everything the engine did with meaning stops: - **Ordering** — ciphertext order is unrelated to plaintext order, so range predicates, ORDER BY and min/max are gone. Workaround: store a coarse non-sensitive bucket alongside (birth year, amount band) and filter precisely in the application. - **Equality** — randomized encryption produces different ciphertext each time, so lookups fail. Workaround: deterministic encryption, or a **blind index** — a keyed HMAC of the normalised plaintext, indexed and used as the lookup key. Both leak equality and frequency, which is dangerous on low-cardinality data. - **Pattern search** — LIKE and full text are impossible without tokenised HMACs that leak more. - **Constraints and joins** — unique and foreign keys work only under a deterministic scheme, and joins need the same key on both sides. - **Operations** — values grow (IV, tag, padding), the type becomes binary, and key rotation means re-encrypting every row, so version every ciphertext. Apply it to a handful of fields, never a whole schema.
code
sql · 9 linesCREATE TABLE patient (
id bigint PRIMARY KEY,
ssn_ct bytea NOT NULL, -- randomized AEAD ciphertext
ssn_key_ver smallint NOT NULL, -- enables online key rotation
ssn_bidx bytea NOT NULL, -- HMAC(key, normalize(ssn)), truncated
dob_ct bytea NOT NULL,
birth_year smallint NOT NULL -- coarse bucket for range queries
);
CREATE INDEX ON patient (ssn_bidx);go deeper
Say the database sees only bytes, so searching, sorting and indexing that column no longer work normally.
Distinguish randomized from deterministic encryption and their effects on equality lookups, uniqueness and joins.
Add blind indexes with normalisation, coarse buckets for ranges, key versioning for online rotation, AEAD with associated data, and the cost of key availability on every read path.
Decide per field against the threat model, weigh tokenising the field out of the database entirely, and own the consequences for analytics, incident response and crypto-shredding.
## Why anything breaks at all A relational engine's value comes from understanding values: B-trees exploit ordering, hash joins exploit equality, constraints exploit comparison, and the optimizer estimates selectivity from distribution statistics. Encrypt above the engine and it sees uniform random bytes — all of that machinery becomes inapplicable. Every workaround is a controlled leak of some property back to the server. ## Randomized vs deterministic **Randomized** (fresh IV per encryption) is the secure default: identical plaintexts produce different ciphertexts, so the server learns nothing beyond length. It also destroys equality lookup, uniqueness and joins. **Deterministic** (fixed or synthetic IV) yields the same ciphertext for the same plaintext, restoring equality predicates, unique constraints, GROUP BY and equi-joins. The leak is real: the server — and anyone who steals the data — sees which rows share a value and how often each value occurs. On a low-cardinality column such as country, diagnosis code or status, frequency analysis recovers the plaintext with no key at all. Deterministic encryption is defensible on high-cardinality identifiers, dangerous on categorical data. ## Blind indexes A blind index stores `HMAC(key, normalize(plaintext))` in a separate indexed column while the real column holds randomized ciphertext. Exact-match search works by computing the HMAC in the application and querying that column. Normalisation (case folding, trimming) must be defined once, or lookups silently miss. Truncating the HMAC deliberately creates collisions, blurring frequency at the cost of false positives the application filters after decryption — a genuine privacy/performance dial. ## Ranges and sorting There is no cheap safe answer. Options: store a coarse bucket in the clear (order year, salary band) and post-filter precisely in the application; maintain an encrypted index structure client-side; or accept application-side filtering over a bounded candidate set. Order-preserving and order-revealing schemes exist but leak the ordering of the whole column, which for many datasets is close to leaking the data. ## Operational consequences - **Size and type.** The column becomes binary and grows by IV, authentication tag and padding — relevant to row width, page fill and index size. - **Key rotation.** Rotating the data key means decrypting and re-encrypting every row online while both versions are valid. Store a key-version tag with every ciphertext from day one; retrofitting it is painful. - **Authenticated encryption and context binding.** Use an AEAD mode and bind associated data — table, column, primary key — so an attacker with write access cannot copy one row's ciphertext into another row and change what it means. - **Loss of server-side processing.** Aggregations, validation triggers, reporting and BI tools cannot see the value; analytics must be fed a separate, differently governed dataset. - **Availability and correctness.** Every read path needs the key. A key-service outage becomes an application outage; a lost key is permanent data loss — which is also the mechanism behind deliberate crypto-shredding. - **Backup and restore.** Ciphertext restores fine, but only if the key version used at write time still exists. ## The judgement to express Column-level encryption is the only layer that protects against an attacker already inside the database, so it is right for a small set of crown-jewel fields — government IDs, card data, secrets, health details — and wrong as a blanket policy. Decide per field: what queries must it support, what does the chosen scheme leak, and would tokenising the field out of the database entirely be simpler than making the database work with ciphertext?
- Why is deterministic encryption risky on a column such as country or diagnosis code?Deterministic ciphertext preserves the plaintext frequency distribution. On a low-cardinality column an attacker holding only ciphertext can match observed frequencies against known population distributions and label the values without ever recovering the key. High-cardinality identifiers such as account numbers leak far less, which is why determinism is defensible there.
- How do you rotate the key for an encrypted column without downtime?Keep both key versions active, tag every ciphertext with the version used, and have readers select the key by tag. Then run a throttled background job that re-encrypts rows in batches under the new version. Only when the old version's row count reaches zero do you retire the key — and only after every backup that depends on it has aged out.
saying these in an interview costs you the question
- Encrypting a column and expecting range queries or ORDER BY to still work
- Using deterministic encryption on low-cardinality data without acknowledging frequency leakage
- Storing the encryption key in the same database or beside the connection string
- No key-version tag, so rotation requires downtime or never happens
- Unauthenticated encryption, letting an attacker with write access swap ciphertexts between rows