Pseudonymization is useful when developers need realistic data without exposing the original identities stored in a production database.
PostgreSQL Anonymizer 3.2 changes an important part of that workflow. The release introduces anon.seeded_* functions as the newer pseudonymization functions and reports that they are significantly faster than the older anon.pseudo_* functions. The new functions also support localization.
That makes version 3.2 a good opportunity to test pseudonymization performance on a realistic PostgreSQL dataset.
The important point is to separate two questions:
Does pseudonymization produce the data characteristics the application needs?
How much database work does the masking operation require?
A benchmark should answer both.
What Pseudonymization Does
Pseudonymization replaces identifying information with another value while preserving a relationship between the original and replacement values.
For example:
Original:
[email protected]
Pseudonymized:
[email protected]
The replacement should not expose the original value, but the same source value can produce the same replacement when deterministic pseudonymization is used.
That property is useful when a test database needs consistent relationships.
For example:
Customer ID: 1024
Customer Email: [email protected]
Order Customer ID: 1024
After pseudonymization, the customer identity can change while the relationship between the customer and orders remains usable.
This is different from random masking where a new value can be generated independently each time.
PostgreSQL Anonymizer 3.2 Pseudonymization
PostgreSQL Anonymizer provides several masking strategies, including static masking, dynamic masking, pseudonymization, faking, randomization, shuffling, partial scrambling, and noise.
Version 3.2 introduces the anon.seeded_* family for pseudonymization.
For example:
SELECT anon.seeded_first_name('customer-1024');
A deterministic seed can produce a consistent replacement for the same input.
The newer functions can also accept a locale where supported:
SELECT anon.seeded_last_name('customer-1024', 'fr_FR');
This matters when test data should look appropriate for a particular locale.
The older anon.pseudo_* functions remain for backward compatibility in 3.2, but they are deprecated.
Pseudonymization Is Not Anonymization
This distinction is important when working with sensitive data.
Pseudonymization changes the identifying value, but the resulting data can still be linked to the original person when additional information or the appropriate mapping is available.
Therefore, pseudonymized data should not automatically be treated as anonymous data.
For example:
Original customer
|
v
Pseudonymization
|
v
Consistent fake identity
|
v
Still potentially linkable
The protection goal should determine which masking strategy is appropriate.
If the original information must be permanently removed, static masking or another irreversible transformation may be more appropriate.
Why Benchmark Pseudonymization?
Pseudonymization is often applied to large datasets.
A development team might need to prepare:
10,000 rows
100,000 rows
1,000,000 rows
10,000,000 rows
The execution cost can become important as the dataset grows.
A useful benchmark should measure:
total execution time
rows processed per second
CPU consumption
I/O
transaction log or WAL impact where relevant
resulting data quality
consistency of generated values
collision rate for fields that must remain unique
Do not report a single benchmark number without describing the environment. PostgreSQL version, hardware, dataset shape, indexes, storage, and concurrency can all affect the result.
Creating a Test Dataset
Start with a representative table:
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
city TEXT,
country TEXT
);
Populate it with realistic test data.
For benchmarking, avoid using real production PII. Generate synthetic data or use an already approved test dataset.
The dataset should contain enough variation to expose problems such as collisions and uneven value distributions.
Declaring Masking Rules
PostgreSQL Anonymizer uses security labels to define masking rules.
For example:
SECURITY LABEL FOR anon
ON COLUMN customers.first_name
IS 'MASKED WITH FUNCTION anon.seeded_first_name(id::text)';
SECURITY LABEL FOR anon
ON COLUMN customers.last_name
IS 'MASKED WITH FUNCTION anon.seeded_last_name(id::text)';
SECURITY LABEL FOR anon
ON COLUMN customers.email
IS 'MASKED WITH FUNCTION anon.seeded_email(id::text)';
Using a stable identifier as the seed gives the generated values deterministic behavior.
The actual seed strategy should be chosen carefully. Do not use a sensitive value as a seed if exposing or recovering information from that relationship would create a security problem.
Benchmarking Static Pseudonymization
For a simple benchmark, copy the source table before applying the transformation.
CREATE TABLE customers_benchmark AS
SELECT *
FROM customers;
Then apply the masking operation:
SELECT anon.anonymize_table('customers_benchmark');
Measure the operation with PostgreSQL timing enabled:
\timing on
SELECT anon.anonymize_table('customers_benchmark');
Run the test multiple times rather than relying on a single execution.
A practical benchmark might look like:
Dataset | Old function | Seeded function | Rows |
|---|---|---|---|
Small | Measure | Measure | 10K |
Medium | Measure | Measure | 100K |
Large | Measure | Measure | 1M |
Very large | Measure | Measure | 10M |
Replace the placeholders with measurements from your own environment.
Do not copy benchmark results from another machine and present them as universal PostgreSQL performance numbers.
What Changed in Version 3.2?
The PostgreSQL Anonymizer 3.2 release reports that the new anon.seeded_* functions can be much faster than the older anon.pseudo_* functions, with the project reporting a 40x improvement for the new implementation.
That is useful information, but it should be treated as a release-level performance claim, not as a guaranteed result for every workload.
A local benchmark is still necessary.
For example:
Old:
anon.pseudo_email(...)
New:
anon.seeded_email(...)
Run both against the same dataset, PostgreSQL instance, hardware, and masking conditions.
The comparison should change only the function being measured.
Measuring Determinism
Performance is not the only thing to test.
A pseudonymization function should produce consistent output when the same seed and configuration are used.
For example:
SELECT
anon.seeded_email('customer-100') AS first_result,
anon.seeded_email('customer-100') AS second_result;
The two values should be compared.
A practical automated test can check:
same seed
|
v
same pseudonym
Then test different seeds:
customer-100 -> value A
customer-101 -> value B
customer-102 -> value C
If the application depends on unique values, measure actual collisions instead of assuming that a pseudonymization function guarantees uniqueness.
Testing Unique Columns
Suppose email has a unique constraint:
ALTER TABLE customers
ADD CONSTRAINT uq_customers_email UNIQUE (email);
After pseudonymization, check:
SELECT email, COUNT(*)
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
If this query returns rows, different source values produced the same pseudonym.
This can cause a masking operation to fail or produce data that cannot be loaded into the target environment.
The masking function and generated dataset need to provide enough output space for the application's uniqueness requirements.
Testing Referential Integrity
Masking one table without considering related tables can destroy relationships.
Consider:
customers.id
|
+---- orders.customer_id
|
+---- support_tickets.customer_id
If the customer identifier changes independently in each table, the relationships can break.
Pseudonymization needs to preserve the same relationship across related records.
This is one reason deterministic transformations and coordinated masking rules are important.
For some relationships, shuffling or pseudonymizing foreign keys may be more appropriate than independently generating fake values.
Localization Testing
One useful change in 3.2 is localization support in the seeded functions.
For example:
SELECT anon.seeded_city('customer-100', 'fr_FR');
and:
SELECT anon.seeded_city('customer-100', 'en_US');
can be tested separately.
A benchmark should check both performance and data characteristics.
If an application is localized, generated names, cities, or addresses that match the expected locale can make test environments more realistic.
Comparing the Old and New Functions
Area |
|
|
|---|---|---|
Deterministic pseudonymization | Yes | Yes |
Localization | Limited compared with new functions | Supported where applicable |
Status in 3.2 | Deprecated | Recommended newer approach |
Performance | Older implementation | Newer, faster implementation |
Existing applications | Backward compatibility | Preferred for new masking rules |
Migration consideration | Existing rules may need updating | Requires testing before replacement |
The important migration point is that changing the function should be treated as a data transformation change, not just a cosmetic function rename.
Verify the resulting values and application assumptions before switching a production masking pipeline.
Security Considerations
Version 3.2 also includes important security changes.
The release addresses critical vulnerabilities related to privilege elevation and SQL injection in certain masking-related operations.
It also introduces a security barrier that prevents superusers from using masking by default.
This changes an important operational assumption.
If an existing masking workflow runs as a superuser, create a dedicated role for masking rather than relying on the previous behavior.
For example:
Application role
|
+--> Normal application access
Masking role
|
+--> Approved masking operations
Keep privileges narrow.
The pseudonymization salt is also sensitive. If deterministic pseudonymization relies on a secret salt, protect that salt with the same level of care as other sensitive security material.
Common Benchmarking Mistakes
Measuring Only One Dataset
A function that performs well on 10,000 rows may behave differently on millions of rows.
Using Production PII
Never turn a benchmark into an unnecessary copy of sensitive production data.
Running Only One Test
Database caches, system load, and background activity can affect execution time.
Ignoring Indexes
Indexes can change the cost of updates and data validation.
Measuring Only Execution Time
A faster operation that creates collisions or breaks relationships is not a successful masking solution.
Treating Pseudonymized Data as Anonymous
Pseudonymization reduces exposure but does not automatically remove the possibility of re-identification.
Keeping Superuser-Based Masking
Version 3.2's security changes make this an important migration consideration.
A Practical Benchmark Procedure
Use the same environment for every test:
1. Create a synthetic dataset
2. Record PostgreSQL configuration
3. Record table size and indexes
4. Create a clean benchmark copy
5. Run the old pseudonymization function
6. Record execution metrics
7. Restore the original benchmark data
8. Run the seeded function
9. Record execution metrics
10. Validate determinism
11. Check collisions
12. Verify referential integrity
13. Compare the resulting data
For larger datasets, repeat the test under realistic database load.
This gives you a useful comparison instead of a single synthetic timing number.
Best Practices
Use synthetic or approved data for benchmarking.
Benchmark the exact PostgreSQL and Anonymizer versions used in production.
Compare old and new functions on identical datasets.
Run multiple iterations.
Test deterministic behavior.
Check unique constraints after masking.
Verify foreign-key relationships.
Protect pseudonymization salts and configuration.
Replace deprecated
pseudo_*functions withseeded_*functions after testing.Use dedicated masking roles instead of relying on superuser execution.
Treat pseudonymized data as potentially linkable sensitive data.
Record the benchmark environment with every performance result.
Advantages and Disadvantages
Advantages | Disadvantages |
|---|---|
Keeps test data useful while reducing direct exposure | Pseudonymized data can remain personal data |
Deterministic output can preserve relationships | Poor seed design can create security risks |
Seeded functions improve the newer pseudonymization workflow | Existing rules may require migration |
Localization makes generated data more realistic | Results depend on the database workload |
Masking rules can be defined inside PostgreSQL | Complex schemas require careful relationship testing |
Supports several masking strategies | No masking strategy automatically guarantees anonymity |
Final Takeaway
PostgreSQL Anonymizer 3.2 gives teams a good reason to revisit existing pseudonymization workloads.
The newer anon.seeded_* functions provide deterministic pseudonymization with localization support, and the project reports a substantial performance improvement over the older functions.
But the right benchmark is not simply:
Old function: X seconds
New function: Y seconds
A useful evaluation also checks deterministic output, uniqueness, relationships, data quality, security configuration, and behavior on production-sized datasets.
For teams upgrading to PostgreSQL Anonymizer 3.2, the practical approach is to benchmark the new functions against the existing workload, validate the resulting data, review the new security requirements, and then migrate masking rules deliberately.

Join the conversation! Your thoughts help the community grow.