Tables, Types and Integrity Constraints

First lesson of the Database Modeling course, free to read: about 18 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Translate a business entity into a table with correctly typed columns
  • Choose a primary key (natural key or surrogate key)
  • Protect data with NOT NULL, UNIQUE, CHECK and DEFAULT
  • Check existing data before adding a constraint to it

CREATE TABLEData typesPrimary keyNOT NULL, UNIQUE, CHECK, DEFAULT

Until now, you have queried existing tables. Modeling is the step before: deciding which tables to create, with which columns and which rules. A good model makes queries simple and keeps inconsistent data out; a bad model forces you to fix the data constantly.

From entity to table

You start from the business vocabulary. An entity is something you want to keep track of: a customer, a product, an order. It becomes a table. Its attributes (name, email, sign-up date) become columns, each with a type.

CREATE TABLE clients (
    id            INTEGER PRIMARY KEY,            -- unique identifier
    email         TEXT    NOT NULL UNIQUE,        -- required and without duplicates
    nom           TEXT    NOT NULL,
    code_postal   TEXT    CHECK (length(code_postal) = 5),
    date_inscription TEXT NOT NULL DEFAULT (date('now')),
    actif         INTEGER NOT NULL DEFAULT 1 CHECK (actif IN (0, 1))
);

Choosing types

Data PostgreSQL SQLite
Identifier bigint GENERATED ALWAYS AS IDENTITY INTEGER PRIMARY KEY
Text text or varchar(n) TEXT
Amount numeric(10,2) INTEGER in cents
Date / timestamp date, timestamptz TEXT in ISO format
Boolean boolean INTEGER 0/1

Two rules: a postal code or a phone number is text (a leading zero must not disappear), and an amount is never stored as a floating-point number (real, float), which produces rounding errors.

SQLite has flexible typing: an INTEGER column accepts text. Since version 3.37, the STRICT keyword at the end of CREATE TABLE enforces types. PostgreSQL always enforces types.

The primary key

The primary key identifies each row: it is unique and never null. Two options:

  • a natural key, coming from the business (social security number, a country’s ISO code);
  • a surrogate key, a meaningless identifier generated by the database.

The surrogate key is generally preferred: an email or a customer number can change, and changing them would break all the references. The business data stays protected by a UNIQUE constraint.

Constraints

  • NOT NULL: the value is required.
  • UNIQUE: no two rows with the same value (several NULLs are still allowed).
  • CHECK (condition): the condition must be true (prix > 0, quantite BETWEEN 1 AND 100).
  • DEFAULT value: value used if the insert does not provide one.

Constraints apply to all applications that write to the database: import script, back office, API. This is far safer than a check in a single form.

Check before constraining

On an already populated table, adding a constraint fails if some rows violate it. So you start by looking for them:

-- Duplicate emails, ignoring case
SELECT lower(email) AS email_normalise, COUNT(*) AS nb
FROM import_clients
GROUP BY lower(email)
HAVING COUNT(*) > 1;

You fix or merge the rows, then add the constraint. In PostgreSQL: ALTER TABLE clients ADD CONSTRAINT clients_email_key UNIQUE (email);. SQLite does not let you add a constraint to an existing table: you recreate the table and copy the data into it.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 2SQL (SQLite) in the browser2 hints

    Duplicate emails before a UNIQUE constraint

    Available table: import_clients(id, email, nom, code_postal), imported without constraints.

    Before adding a uniqueness constraint on the email, list the duplicate emails, ignoring case: display the lowercase email (alias email_normalise) and the number of occurrences (alias nb).

    Exercise data (SQL)
    -- Fichier importé tel quel, sans aucune contrainte
    CREATE TABLE import_clients (
      id INTEGER,
      email TEXT,
      nom TEXT,
      code_postal TEXT
    );
    INSERT INTO import_clients VALUES
     (1, 'lea@exemple.fr', 'Léa', '69001'),
     (2, 'Lea@Exemple.fr', 'Léa M.', '69001'),
     (3, 'marc@exemple.fr', 'Marc', '75011'),
     (4, 'marc@exemple.fr', 'Marc', '75011'),
     (5, NULL, 'Nina', '13008'),
     (6, 'sam.exemple.fr', 'Sam', '33000'),
     (7, 'ines@exemple.fr', 'Inès', '5900'),
     (8, 'paul@exemple.fr', 'Paul', '44000');
    SELECT lower(email) AS email_normalise, COUNT(*) AS nb
    FROM import_clients
    GROUP BY ...
    HAVING ...;
  2. CodeDifficulty 2SQL (SQLite) in the browser2 hints

    Rows that would violate the constraints

    Available table: import_clients(id, email, nom, code_postal).

    You want to add these rules: email required and containing an @ preceded and followed by at least one character, postal code of 5 characters. Display the id of the rows that violate at least one rule.

    Exercise data (SQL)
    -- Fichier importé tel quel, sans aucune contrainte
    CREATE TABLE import_clients (
      id INTEGER,
      email TEXT,
      nom TEXT,
      code_postal TEXT
    );
    INSERT INTO import_clients VALUES
     (1, 'lea@exemple.fr', 'Léa', '69001'),
     (2, 'Lea@Exemple.fr', 'Léa M.', '69001'),
     (3, 'marc@exemple.fr', 'Marc', '75011'),
     (4, 'marc@exemple.fr', 'Marc', '75011'),
     (5, NULL, 'Nina', '13008'),
     (6, 'sam.exemple.fr', 'Sam', '33000'),
     (7, 'ines@exemple.fr', 'Inès', '5900'),
     (8, 'paul@exemple.fr', 'Paul', '44000');
    SELECT id
    FROM import_clients
    WHERE email IS NULL
       OR ...
       OR ...;
  3. TaskDifficulty 22 hints

    Write the clean table

    Write the CREATE TABLE clients statement (PostgreSQL) that will hold the cleaned data from import_clients. It must have a surrogate key, a required and unique email, a required name, a 5-character postal code and an automatically filled creation date.

    • The primary key is an automatically generated id column (GENERATED ALWAYS AS IDENTITY)
    • email is NOT NULL and UNIQUE
    • code_postal is a text type with a CHECK on length (or char(5))
    • A cree_le column of type timestamptz has DEFAULT now()
    • No amount or identifier uses a floating-point type

End-of-lesson quiz

  1. Which type should you choose to store a price in PostgreSQL?

    • real
    • numeric(10,2)
    • text
  2. Why is a surrogate key often preferred over the email as a primary key?

    • Because an email can change and the primary key is referenced elsewhere
    • Because an email cannot be unique
    • Because text keys are forbidden
  3. Can a UNIQUE column contain several NULLs?

    • No, never
    • Yes, NULL is not equal to any value (default behavior of PostgreSQL and SQLite)
    • Only if the column is also NOT NULL

The rest of the course

  1. Relationships and Foreign Keys20 min
  2. Normalization: 1NF, 2NF, 3NF and Anomalies20 min
  3. Dimensional Modeling: The Star Schema18 min
  4. From Model to Physical Schema: Views, Indexes and Migrations18 min

Database Modeling · Intermediate · 3 h 4 min