Enforce AtlasMart identity invariants with unique and collation-aware indexes while handling optional/missing values and dirty historical duplicates safely.

Unique Indexes, Missing/Null Values, Collations, and Application Invariants

Batch heterogeneous writes safely, interpret partial success, compare ordered and unordered execution, and use modern cross-namespace bulk APIs without assuming all-or-nothing behavior.

Intermediate110–150 minutesUnique/null/collation invariant labMongoDB 8.3.8 · mongosh 2.10.0Last reviewed: September 2026

Learning objectives

01

Use unique indexes as database-enforced invariants and interpret duplicate-key failures as correctness signals.

02

Explain how missing fields become null-like index keys for uniqueness and why optional fields often need a partial unique index.

03

Use collation-aware unique indexes for case/diacritic semantics and match query collation to index collation.

04

Detect dirty duplicates before attempting a unique index build and repair data instead of treating the build error as an outage surprise.

05

State the scope of an invariant precisely: uniqueness applies to the indexed key combination, not to every business rule around the entity.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim and mongosh 2.10.0. The server is a disposable standalone published only on loopback 127.0.0.1:27065. Authentication and TLS are disabled only for this isolated lab. Feature Compatibility Version (FCV) is observed but never changed. Default read/write concern and primary read preference apply. Atlas, Search, KMS, Enterprise Advanced, and paid services are not required. Runtime output shown as “expected” is documentation-derived because this generation environment has no Docker/mongod/mongosh runtime.

Evidence, not index folklore

Index behavior depends on query shape, projection, sort, data distribution, planner choice, cache state, topology, and patch version. The labs therefore inspect winningPlan, totalKeysExamined, totalDocsExamined, index definitions/sizes, and target query results. Small fixtures prove semantics, not production latency. Timing snippets are comparative demonstrations only, not benchmarks.

1. Uniqueness is an invariant, not a lookup optimization

A unique index rejects a write when its index key would duplicate an existing key in another document. For AtlasMart, email may be unique only inside one tenant, so the invariant key is (tenantId,email), not global email. Uniqueness is enforced during writes; an application-level “check then insert” has a race window that a unique index closes.

Case Unique-key interpretation
tenant-a + alice@example.test Unique tuple for that tenant.
tenant-b + alice@example.test Different tuple because tenant differs; allowed.
tenant-a + missing email The missing indexed field contributes a null-like key component; another same-prefix missing/null key can collide.
Collation strength 2 String comparison is case-insensitive for the indexed collation, so Alice and alice compare equal for uniqueness in that tenant.

2. Controlled failure: naïve unique index on an optional email

bash · isolated Chapter 10 Lesson 4 lab setup
docker rm -f atlasmart-mongo-ch10-l4 2>/dev/null || truedocker volume rm atlasmart-mongo-ch10-l4-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch10-l4 \  -p 127.0.0.1:27065:27017 \  -v atlasmart-mongo-ch10-l4-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27065/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))' 
javascript · seed customers
const c=db.customers_ch10_l4;c.drop();c.insertMany([ {_id:1,tenantId:"tenant-a",customerId:"C1",username:"Alice",email:"alice@example.test"}, {_id:2,tenantId:"tenant-a",customerId:"C2",username:"Bob"}, {_id:3,tenantId:"tenant-b",customerId:"C3",username:"alice",email:"alice@example.test"}]);printjson(c.find({}).sort({_id:1}).toArray());
javascript · create naïve unique tenant/email index and insert another missing email
const c=db.customers_ch10_l4;print(c.createIndex({tenantId:1,email:1},{name:"uq_tenant_email_naive",unique:true}));try{ c.insertOne({_id:4,tenantId:"tenant-a",customerId:"C4",username:"Carol"}); print("UNEXPECTED: second tenant-a document missing email inserted");}catch(e){ printjson({code:e.code,codeName:e.codeName,message:e.message}); }printjson(c.getIndexes());
Why the insert fails

For a unique index, missing indexed fields are represented in the index with null-like key values. Tenant-a already has one document with missing email; another tenant-a document with missing email attempts the same unique tuple. Flexible schema did not remove the invariant—it made the missing-value policy part of index design.

3. Repair optional uniqueness with a partial unique index

If “email must be unique when it is a string, but email is optional” is the real invariant, index only documents that carry an email string. The partial filter changes which documents participate in the unique constraint.

javascript · replace naïve index with partial unique constraint
const c=db.customers_ch10_l4;c.dropIndex("uq_tenant_email_naive");print(c.createIndex( {tenantId:1,email:1}, {name:"uq_tenant_email_present",unique:true,partialFilterExpression:{email:{$type:"string"}}}));printjson(c.insertOne({_id:4,tenantId:"tenant-a",customerId:"C4",username:"Carol"}));try{ c.insertOne({_id:5,tenantId:"tenant-a",customerId:"C5",username:"Alicia",email:"alice@example.test"});}catch(e){ printjson({expectedDuplicate:true,code:e.code,message:e.message}); }printjson(c.find({tenantId:"tenant-a"},{_id:0,customerId:1,email:1}).sort({customerId:1}).toArray());
Invariant statement

The repaired invariant is now: for documents whose email is a BSON string, the pair (tenantId,email) is unique. It does not validate email syntax or guarantee every customer has an email; Chapter 07-style validation handles those separate rules.

4. Collation changes string equality and index eligibility

A collation defines language-aware string comparison rules. With locale en and strength 2, case differences are ignored for comparison. The case-insensitive unique index therefore rejects tenant-a alice because tenant-a already has Alice. The same username in tenant-c is allowed because tenantId is part of the key.

javascript · case-insensitive tenant username invariant
const c=db.customers_ch10_l4;print(c.createIndex( {tenantId:1,username:1}, {name:"uq_tenant_username_ci",unique:true,collation:{locale:"en",strength:2}}));try{ c.insertOne({_id:6,tenantId:"tenant-a",customerId:"C6",username:"alice"});}catch(e){ printjson({caseInsensitiveDuplicate:true,code:e.code,message:e.message}); }printjson(c.insertOne({_id:7,tenantId:"tenant-c",customerId:"C7",username:"alice"}));printjson(c.find({tenantId:"tenant-a",username:"alice"},{_id:0,customerId:1,username:1}).collation({locale:"en",strength:2}).explain("executionStats"));
Query/index matching

To use a non-simple-collation index for string comparisons, the query must specify the same collation. A query that omits or uses a different collation may not use that index for those string comparisons. Collation is semantic, not decorative.

5. Existing duplicates can make a unique index build fail

Adding a unique index to dirty historical data is a migration, not just DDL. The disposable dirty collection contains a duplicate key before the build. MongoDB rejects the build rather than silently deleting a row.

javascript · detect duplicate data after rejected unique build
const d=db.customers_ch10_l4_dirty;d.drop();d.insertMany([ {_id:1,tenantId:"tenant-a",email:"dup@example.test"}, {_id:2,tenantId:"tenant-a",email:"dup@example.test"}, {_id:3,tenantId:"tenant-a",email:"ok@example.test"}]);try{ d.createIndex({tenantId:1,email:1},{name:"uq_dirty",unique:true});}catch(e){ printjson({buildRejected:true,code:e.code,message:e.message}); }printjson(d.aggregate([ {$group:{_id:{tenantId:"$tenantId",email:"$email"},n:{$sum:1},ids:{$push:"$_id"}}}, {$match:{n:{$gt:1}}}]).toArray());
Safe rollout

Inventory duplicates first, define deterministic repair ownership, fix or quarantine them, then build the unique index and verify future duplicate writes fail. On a production replica set/sharded deployment also plan disk headroom, replication lag, commit quorum, and rollback.

6. Topology and application boundary

Unique constraints on sharded collections have additional shard-key restrictions because enforcement must be routable across the cluster. Keep tenant or shard identity in the unique-key design when the business rule is scoped that way, and re-check the exact sharded-index rule before deployment. A unique index also does not replace authorization: tenantId in the key prevents duplicate identifiers, not cross-tenant reads.

Do not hide a unique index to disable the invariant

A hidden unique index is still maintained and still rejects duplicate keys. Hiding only removes it from the query planner. Lesson 5 uses this property to evaluate read-path removal safely without weakening uniqueness.

7. Verification, cleanup, and production judgment

Verification checklist

  • The naïve optional-email unique index allows one tenant-a missing email and rejects a second.
  • The partial unique index allows multiple tenant-a documents without string email values.
  • The partial index still rejects duplicate tenant/email strings.
  • The collation-aware unique index rejects Alice/alice in the same tenant but allows the username in another tenant.
  • The dirty unique-index build fails and the duplicate group is observable before repair.
  • No application “check first” is presented as equivalent to database-enforced uniqueness.

Production judgment. Unique indexes are one of MongoDB’s strongest data-quality tools because they move race-sensitive identity invariants into the database write path. Their semantics depend on missing/null participation, collation, partial filters, arrays, and topology. Introduce them with data-quality scans and rollback plans, not during an incident. Monitor duplicate-key errors as both client feedback and a possible signal of broken upstream deduplication. Lesson 5 completes the lifecycle: create, observe, hide, safely retire, and deal correctly with the fact that indexes cannot be renamed in place.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch10-l4docker volume rm atlasmart-mongo-ch10-l4-data

Check your understanding

  1. Why can two documents missing an indexed field conflict under a unique compound index?
  2. When is a partial unique index useful?
  3. What does collation strength 2 change in the example?
  4. Does a hidden unique index stop enforcing uniqueness?
  5. Why should duplicates be checked before building a new unique index?
Review the answers

1. Missing fields contribute null-like index key values, so the same compound prefix plus missing/null component can duplicate an existing unique key.

2. When the business invariant applies only to documents meeting a condition, such as “email is unique when email is present as a string.”

3. It makes the index comparison case-insensitive, so Alice and alice compare equal for the indexed tenant/username key.

4. No. Hidden indexes remain maintained and unique constraints still apply.

5. The build will reject duplicate keys; finding and repairing them first turns an outage surprise into a controlled migration.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.