Thuta Learning
IntermediateData & Databasesbeginner

Constraints, NULL, and Normalization

What you'll walk away with

  • Explain the core ideas behind Constraints, NULL, and Normalization
  • Read the diagram/table and identify the shape of the data model, schema, or architecture
  • Explain how this concept or system choice applies to a real project

Build the mental model

A constraint is a rule the database itself enforces to protect the shape and integrity of your data.

  • NOT NULL - a column must have a value
  • UNIQUE - no two rows may share the same value
  • FOREIGN KEY - a value must reference a real row in another table
  • CHECK - a value must satisfy a condition
  • PRIMARY KEY - the column (or columns) that uniquely identifies each row

These rules need to live inside the database, not only in your frontend form validation. Frontend checks can be skipped, broken by a bug, or bypassed by a second application talking to the database directly - the database is the last line of defense.

NULL means "absence," "unknown," or "not applicable," depending on context. It is not the number 0, not the boolean false, and not an empty string - those are real values, while NULL represents the lack of one.

NULL Is Not 0, False, or Empty String

Treating NULL as equivalent to any of these causes real bugs - a NULL "discount" field is not the same as a $0 discount; one means "never set," the other means "deliberately zero." Comparisons involving NULL don't behave like normal comparisons either - NULL equals nothing, not even another NULL.

Normalization is the practice of organizing data to avoid unnecessary duplication - storing a user's full profile once and referencing it from an orders table, instead of repeating it in every order row.

This reduces duplication, protects integrity, and makes updates safer. Denormalization - deliberately duplicating data - is a legitimate tradeoff for performance, not a mistake. PostgreSQL's "Normalization and Schema Design" lesson covers this hands-on.

NULL
The complete absence of a value - meaning "unknown," "not applicable," or "no value" depending on context, and never equivalent to 0, false, or an empty string.
Normalization
The practice of organizing tables so each fact is stored once and referenced, reducing data duplication.
text
DUPLICATED DATA VS NORMALIZED REFERENCE
---------------------------------------
BAD: DUPLICATED USER DATA IN EVERY ORDER ROW
-----
orders
+----+---------+------------------+-------+
| id | name    | email            | total |
+----+---------+------------------+-------+
| 1  | Aye Aye | aye@example.com  | 42.00 |
| 2  | Aye Aye | aye@example.com  | 15.50 |
| 3  | Bo Bo   | bo@example.com   |  9.00 |
+----+---------+------------------+-------+
Problem: name/email repeated every row, can drift

BETTER: NORMALIZED, ORDERS REFERENCES USERS
-----
users                    orders
+----+---------+------+  +----+---------+-------+
| id | name    | ...  |  | id | user_id | total |
+----+---------+------+  +----+---------+-------+
| 1  | Aye Aye | ...  |  | 1  | 1       | 42.00 |
| 2  | Bo Bo   | ...  |  | 2  | 1       | 15.50 |
+----+---------+------+  | 3  | 2       |  9.00 |
                          +----+---------+-------+
orders.user_id references users.id (foreign key)

Connect it to a real scenario

You declare constraints when you create or alter a table, and the database rejects any operation that would violate them.

  • rejecting a duplicate email
  • rejecting a missing required field
  • rejecting a foreign key that points to nothing

Application code should turn these rejections into a friendly message instead of a crash. Check for NULL explicitly before comparing or displaying a value too - a null discount field is not the same as a $0 discount.

Default to normalized structures for new tables, and reach for denormalization only with a measured reason. The runnable example below shows a simple constraint validator.

NULL Is Not 0, False, or Empty String

Treating NULL as equivalent to any of these causes real bugs - a NULL "discount" field is not the same as a $0 discount. Comparisons involving NULL don't behave like normal comparisons either - NULL equals nothing, not even another NULL.

Try the working example

javascript
function validateRow(row, existingRows, constraints) {
  const results = {};

  for (const field of constraints.notNull) {
    const value = row[field];
    const pass = value !== null && value !== undefined && value !== "";
    results[`NOT NULL(${field})`] = pass;
  }

  for (const field of constraints.unique) {
    const value = row[field];
    const duplicate = existingRows.some((r) => r[field] === value);
    results[`UNIQUE(${field})`] = !duplicate;
  }

  return results;
}

const existingUsers = [{ id: 1, name: "Aye", email: "aye@example.com" }];
const constraints = { notNull: ["name", "email"], unique: ["email"] };

console.log("Valid row:", validateRow(
  { name: "Bo", email: "bo@example.com" },
  existingUsers,
  constraints
));

console.log("Invalid row:", validateRow(
  { name: null, email: "aye@example.com" },
  existingUsers,
  constraints
));
You should see
Valid row: { 'NOT NULL(name)': true, 'NOT NULL(email)': true, 'UNIQUE(email)': true }
Invalid row: { 'NOT NULL(name)': false, 'NOT NULL(email)': true, 'UNIQUE(email)': false }

The invalid row fails NOT NULL(name) because name is null, and fails UNIQUE(email) because that email already exists in existingUsers.

5-minute try-it

Extend validateRow to support a CHECK-style rule too - for example, an "age" field that must be greater than 0. Test it with one row that fails and one that passes.

One important caution

Treating NULL as equivalent to 0 or an empty string when writing comparison logic

Leaving constraints only in frontend form validation and never enforcing them in the database itself

Wikipedia: Null (SQL)How Databases Work

Easy traps

  • Treating NULL as equivalent to 0 or an empty string when writing comparison logic
  • Leaving constraints only in frontend form validation and never enforcing them in the database itself
  • This course teaches database concepts and the product landscape at a framework-neutral level -- for hands-on SQL syntax or PostgreSQL/MongoDB/Redis depth, continue to the SQL, PostgreSQL, MongoDB, or Redis tutorials.

Exercise

Extend validateRow to support a CHECK-style rule too - for example, an "age" field that must be greater than 0. Test it with one row that fails and one that passes.

You'll know it worked when: Valid row: { 'NOT NULL(name)': true, 'NOT NULL(email)': true, 'UNIQUE(email)': true } Invalid row: { 'NOT NULL(name)': false, 'NOT NULL(email)': true, 'UNIQUE(email)': false } The invalid row fails NOT NULL(name) because name is null, and fails UNIQUE(email) because that email already exists in existingUsers.

Constraints, NULL, and Normalization | Thuta Learning