Enforcing Data Integrity

Introduction

Welcome to lesson four of Indexes and Performance! You've made excellent progress learning about indexes: first single-property indexes, then compound indexes for multi-property queries. These tools help Neo4j find data efficiently by creating organized lookup structures.

Now we're shifting focus from performance optimization to constraints. While indexes help queries run faster, they don't prevent bad data from entering your graph. What stops someone from creating two users with the same email address? This lesson introduces constraints, which enforce rules about what data can exist in your graph. While Neo4j supports several types—including Existence constraints (ensuring a property exists) and Node Key constraints (combining existence and uniqueness)—this lesson focuses on the most common type: UNIQUE constraints. We'll learn how to prevent duplicate values and understand what happens when these rules are violated.

The Role of Constraints in Data Quality

Constraints act as guardians of your graph database, enforcing business rules at the database level rather than relying solely on application code. Think of them as quality gates that every write operation must pass through. If a constraint rule is violated, Neo4j rejects the entire transaction, ensuring your data stays consistent.

This approach provides several benefits. First, constraints prevent inconsistencies regardless of which application or user performs the write. Even if application code has bugs or users access the database directly, constraints maintain data integrity. Second, they make your data model explicit and self-documenting: reading the constraints tells you which properties must be unique. Finally, constraints catch errors early, at write time, rather than letting bad data accumulate and cause problems later during queries or analytics.

UNIQUE Constraints

Neo4j provides UNIQUE constraints to prevent duplicate values within a property, ensuring each value appears at most once across all nodes of a given label. This is perfect for identifiers like email addresses, usernames, or product SKUs where duplication would cause ambiguity or errors.

Understanding when and how to apply UNIQUE constraints is essential for maintaining a reliable graph database.

Creating UNIQUE Constraints

Let's create our first constraint to prevent duplicate email addresses among users. The syntax resembles index creation but uses CREATE CONSTRAINT instead:

// Make emails unique
CREATE CONSTRAINT user_email_unique 
FOR (u:User) REQUIRE u.email IS UNIQUE

This statement creates a constraint named user_email_unique that applies to all nodes with the User label. The REQUIRE u.email IS UNIQUE clause tells Neo4j that no two User nodes can share the same email value. From this point forward, any attempt to create or update a user with an email that already exists will fail. The constraint name follows a helpful convention: including the label and property makes it clear what rule this constraint enforces.

Understanding UNIQUE Constraint Behavior

UNIQUE constraints have an important characteristic worth understanding: they only enforce uniqueness for values that actually exist. If a User node has no email property or has email set to null, the constraint allows it. Multiple users can have missing or null emails without violating uniqueness.

This behavior makes sense for optional identifiers. Perhaps email is an optional contact method, but when provided, it must be unique so you can reliably look up users by email. The constraint prevents duplicates among provided emails while permitting nodes to omit the property entirely. This flexibility lets you model different business rules where certain identifiers are optional but must be unique when present.

Viewing All Constraints

After creating constraints, we need a way to inspect what rules are currently active. The SHOW CONSTRAINTS command provides a comprehensive view:

// View all constraints
SHOW CONSTRAINTS

This command returns a table listing every constraint in your database. The output includes several columns: name shows the constraint name we provided; type indicates whether it's a UNIQUENESS constraint; entityType specifies NODE or RELATIONSHIP; labelsOrTypes shows which label the constraint applies to; and properties lists the constrained properties.

╒═══════════════════════╤═══════════════════════════╤═════════════╤═══════════════╤════════════╕
│ name                  │ type                      │ entityType  │ labelsOrTypes │ properties │
╞═══════════════════════╪═══════════════════════════╪═════════════╪═══════════════╪════════════╡
│ user_email_unique     │ UNIQUENESS                │ NODE        │ ["User"]      │ ["email"]  │
└───────────────────────┴───────────────────────────┴─────────────┴───────────────┴────────────┘

This output confirms our constraint is active: user_email_unique enforces uniqueness on email. The table format makes it easy to review all active constraints at once and verify your data quality rules are properly configured.

What Happens When Constraints Are Violated

When a write operation violates a constraint, Neo4j rejects the transaction immediately and returns an error. Let's see what happens if we try to create duplicate emails:

// First user succeeds
CREATE (:User {name: 'Alice', email: 'alice@example.com'})

// Second user with same email fails
CREATE (:User {name: 'Bob', email: 'alice@example.com'})

The second statement triggers a constraint violation error. The error message typically includes the constraint name, the conflicting value, and references to the nodes involved. These immediate failures prevent bad data from entering your graph. The entire transaction rolls back, leaving your database unchanged, which maintains consistency even when errors occur.

Automatic Index Creation for UNIQUE Constraints

Here's an important detail: when you create a UNIQUE constraint, Neo4j automatically creates a backing index for that property. This index serves two purposes. First, it enables efficient uniqueness checking: before inserting or updating a value, Neo4j can quickly look up whether that value already exists. Second, it accelerates queries that filter by the constrained property.

This means our user_email_unique constraint gave us both data integrity and query performance improvements. When you execute:

MATCH (u:User {email: 'alice@example.com'})
RETURN u

Neo4j uses the constraint's backing index for a fast lookup, similar to the indexes we created in previous lessons. This dual benefit makes UNIQUE constraints a powerful tool for both maintaining data quality and optimizing query performance.

Conclusion and Next Steps

In this lesson, we've learned how constraints enforce data integrity rules in Neo4j. We explored UNIQUE constraints that prevent duplicate values, learned how to create them, view them with SHOW CONSTRAINTS, and understand what happens when violations occur.

We also discovered that UNIQUE constraints automatically create backing indexes, providing both data quality enforcement and query performance benefits. Understanding constraints completes our foundation in graph database schema design: indexes optimize read performance, while constraints ensure data quality. Together, they create a robust, efficient database structure. Now it's time to put this knowledge into practice and start enforcing your own data integrity rules!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal