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.
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 (aliasnb).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, '[email protected]', 'Léa', '69001'), (2, '[email protected]', 'Léa M.', '69001'), (3, '[email protected]', 'Marc', '75011'), (4, '[email protected]', 'Marc', '75011'), (5, NULL, 'Nina', '13008'), (6, 'sam.exemple.fr', 'Sam', '33000'), (7, '[email protected]', 'Inès', '5900'), (8, '[email protected]', 'Paul', '44000');SELECT lower(email) AS email_normalise, COUNT(*) AS nb FROM import_clients GROUP BY ... HAVING ...;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, '[email protected]', 'Léa', '69001'), (2, '[email protected]', 'Léa M.', '69001'), (3, '[email protected]', 'Marc', '75011'), (4, '[email protected]', 'Marc', '75011'), (5, NULL, 'Nina', '13008'), (6, 'sam.exemple.fr', 'Sam', '33000'), (7, '[email protected]', 'Inès', '5900'), (8, '[email protected]', 'Paul', '44000');SELECT id FROM import_clients WHERE email IS NULL OR ... OR ...;Write the clean table
Write the
CREATE TABLE clientsstatement (PostgreSQL) that will hold the cleaned data fromimport_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
Which type should you choose to store a price in PostgreSQL?
- real
- numeric(10,2)
- text
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
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
- Relationships and Foreign Keys20 min
- Normalization: 1NF, 2NF, 3NF and Anomalies20 min
- Dimensional Modeling: The Star Schema18 min
- From Model to Physical Schema: Views, Indexes and Migrations18 min