Text order is part of a query’s contract. A list sorted using byte order can differ from one sorted using linguistic rules, and a case-insensitive comparison can change which names a uniqueness rule considers duplicates. PostgreSQL supports database defaults as well as column and expression collations, so behavior should be made explicit where it affects business identity or pagination.
When it breaks
A locale name alone does not mean that different byte strings compare equal. PostgreSQL’s deterministic collations break otherwise equal comparisons using bytes. A nondeterministic collation can let its provider treat different bytes as equivalent. For ICU, und-u-ks-level2 with deterministic=false ignores case while retaining accent differences; describing that combination as both case- and accent-insensitive is incorrect.
Changing comparison semantics can make previously distinct values collide. A unique index created under the new rules may therefore fail on existing data. Ordering also needs a stable tie-breaker when several strings compare equally: using only a case-insensitive name in pagination does not define which equivalent row comes first. Collation selection belongs in schema and query review, not merely in a display preference.
Check it quickly
Use PostgreSQL 18 built with ICU support, a UTF-8 disposable database, and a fresh psql -X -v ON_ERROR_STOP=1 session. Creating this collation requires the appropriate database privileges. If the server lacks ICU, the setup should fail explicitly; do not silently replace it with another locale and interpret the results as equivalent. Create a case-insensitive rule and five names:
CREATE SCHEMA collation_demo;
CREATE COLLATION collation_demo.ci
(provider=icu,locale='und-u-ks-level2',deterministic=false);
CREATE TABLE collation_demo.names (id integer PRIMARY KEY,name text COLLATE "C");
INSERT INTO collation_demo.names VALUES (1,'a'),(2,'A'),(3,'á'),(4,'b'),(5,'B');
SELECT 'a' COLLATE "C" = 'A' AS c_equal,
'a' COLLATE collation_demo.ci = 'A' AS case_equal,
'a' COLLATE collation_demo.ci = 'á' AS accent_equal;
The comparisons return false, true, false. Under C, lowercase and uppercase differ. Under the new rule, a and uppercase A compare equally, but accented á remains different. The nondeterministic flag controls the bytewise tie-break; the ICU locale controls which linguistic differences matter. Removing the flag or changing the strength changes the contract being tested.
Now compare the two orderings and a case-insensitive equality lookup:
SELECT string_agg(name,',' ORDER BY name COLLATE "C",id) AS byte_order,
string_agg(name,',' ORDER BY name COLLATE collation_demo.ci,id) AS ci_order
FROM collation_demo.names;
SELECT id,name FROM collation_demo.names
WHERE name COLLATE collation_demo.ci='A' ORDER BY id;
The byte ordering is A,B,a,b,á. The case-insensitive ordering is a,A,á,b,B, with identifiers resolving ties between equivalent names. The lookup for uppercase A returns rows one and two. Nothing rewrites stored text into lowercase: the values remain a and uppercase A, and comparison semantics determine which rows match.
Try to impose uniqueness under the case-insensitive rule while those equivalent values still exist:
\set ON_ERROR_STOP off
CREATE UNIQUE INDEX unique_ci ON collation_demo.names(name COLLATE collation_demo.ci);
\echo existing_duplicates :SQLSTATE
\set ON_ERROR_STOP on
Expect existing_duplicates 23505. The index cannot be created because the existing data violate its proposed comparison rule. This is useful migration evidence, not a transient error to hide with a retry. In the disposable fixture, remove the uppercase duplicate rows deliberately, create the index, and test another conflicting insert:
DELETE FROM collation_demo.names WHERE id IN (2,5);
CREATE UNIQUE INDEX unique_ci ON collation_demo.names(name COLLATE collation_demo.ci);
\set ON_ERROR_STOP off
INSERT INTO collation_demo.names VALUES (6,'A');
\echo duplicate_case :SQLSTATE
\set ON_ERROR_STOP on
SELECT id,name FROM collation_demo.names ORDER BY id;
The insert reports duplicate_case 23505. The surviving rows are identifiers one, three, and four, containing a, accented á, and b. Their distinctness under the chosen accent-sensitive rule is intentional. A real migration needs an approved policy for merging or renaming collisions; deleting fixture rows here is not a production data-cleanup strategy. Verify the exact surviving values and remove the experimental schema:
DO $$ BEGIN
IF (SELECT array_agg(name ORDER BY id) FROM collation_demo.names)
IS DISTINCT FROM ARRAY['a','á','b'] THEN
RAISE EXCEPTION 'collation uniqueness result mismatch';
END IF;
END $$;
DROP SCHEMA collation_demo CASCADE;
Keep comparisons and indexes aligned
An index orders its keys using its defined collation. A query requesting a different collation cannot assume that index order satisfies its sort. Use the same expression semantics where indexed lookup or ordering is required, then inspect the actual plan. Adding a unique identifier after the display name gives an explicit order among ties without changing which names compare equal for uniqueness.
Database defaults are chosen at creation, but they are not the only available comparison rules: an expression can specify COLLATE, and columns can declare their own collations. Do not claim every query-level change requires rebuilding the entire database. Conversely, changing the provider’s library version can affect existing index order. Check recorded and actual collation versions, rebuild affected objects when required, and refresh version metadata only after that work; refreshing the recorded version alone does not repair an index.
Sources
- PostgreSQL 18 collation support, deterministic comparisons, ICU strengths, and expression collations, checked 2026-09-10.
- PostgreSQL 18 CREATE COLLATION, provider and deterministic settings, checked 2026-09-10.
- PostgreSQL 18 ALTER COLLATION, version mismatch handling and rebuilding dependent objects, checked 2026-09-10.