SQL Fundamentals
DDL: Defining Schema
In many databases (notably MySQL and Oracle),
JrCodex·6 min read
Jr Codex DBMS Notes
Level: Beginner Prerequisites: Module 2, Chapter 4: From Algebra to SQL Time to complete: ~20 minutes
Table of Contents
- The Three Sublanguages
- Data Types
- CREATE TABLE
- The Working Dataset
- ALTER and DROP
- Summary & Next Steps
1. The Three Sublanguages
SQL Splits Into Three
─────────────────────────────────────────
DDL Data Definition Language
CREATE, ALTER, DROP, TRUNCATE
Defines and changes STRUCTURE.
── this chapter
DML Data Manipulation Language
SELECT, INSERT, UPDATE, DELETE
Works with the DATA.
── Chapters 2-6
DCL Data Control Language
GRANT, REVOKE
Controls PERMISSIONS.
─────────────────────────────────────────
A Trap Worth Knowing Now
─────────────────────────────────────────
In many databases (notably MySQL and Oracle),
DDL statements cause an IMPLICIT COMMIT — they
end any transaction in progress and cannot be
rolled back.
PostgreSQL is the notable exception: DDL is
transactional there, so a failed migration rolls
back cleanly.
This is why migration tooling differs so much
between databases, and why you test migrations
on a copy first.
─────────────────────────────────────────
2. Data Types
The Families
─────────────────────────────────────────
NUMERIC
INTEGER / BIGINT whole numbers
NUMERIC(p,s) EXACT decimal — use for
MONEY
REAL / DOUBLE floating point —
NEVER for money
TEXT
VARCHAR(n) variable, capped length
TEXT unbounded
CHAR(n) fixed, space-padded
TEMPORAL
DATE, TIME, TIMESTAMP
TIMESTAMPTZ with time zone — prefer
this one
OTHER
BOOLEAN, BLOB/BYTEA, UUID, JSON/JSONB, ARRAY
─────────────────────────────────────────
The Money Rule
─────────────────────────────────────────
Use NUMERIC(10,2), never REAL or DOUBLE.
Floating point cannot represent 0.1 exactly, so
sums drift. After a million transactions your
ledger disagrees with itself by a few cents and
nobody can find where.
NUMERIC is slower and exact. For money, exact
wins every time.
─────────────────────────────────────────
Choosing Well
─────────────────────────────────────────
- Smallest type that fits: INTEGER over BIGINT
unless you truly need it. Every row and every
index entry pays the difference (Module 5).
- TIMESTAMPTZ over TIMESTAMP — store an instant,
not a wall-clock reading with no time zone.
- VARCHAR(n) where n is a real business rule;
TEXT otherwise. An arbitrary VARCHAR(255) is
cargo cult.
- A constrained TEXT with CHECK beats an
unconstrained one for status-like columns.
─────────────────────────────────────────
3. CREATE TABLE
CREATE TABLE students (
id INTEGER PRIMARY KEY, -- surrogate key (Module 2, Ch.2)
email TEXT NOT NULL UNIQUE, -- natural key, still enforced
name TEXT NOT NULL,
city TEXT, -- nullable: genuinely optional
marks INTEGER CHECK (marks BETWEEN 0 AND 100),
enrolled_on TEXT NOT NULL DEFAULT (date('now'))
);Column vs Table Constraints
─────────────────────────────────────────
COLUMN constraint — attached to one column,
can only reference that column.
TABLE constraint — declared separately, CAN
reference several columns. Required for
composite keys and cross-column CHECKs.
─────────────────────────────────────────
CREATE TABLE enrollments (
student_id INTEGER NOT NULL REFERENCES students(id) ON DELETE CASCADE,
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE RESTRICT,
grade TEXT CHECK (grade IN ('A','B','C','D','F')),
graded_on TEXT,
PRIMARY KEY (student_id, course_id), -- TABLE constraint: composite
CHECK (grade IS NULL OR graded_on IS NOT NULL) -- TABLE constraint: 2 columns
);Name Your Constraints
─────────────────────────────────────────
CONSTRAINT chk_marks_range CHECK (marks BETWEEN 0 AND 100)
Unnamed constraints get generated names like
`students_marks_check1`. Named ones produce
readable error messages and can be dropped by
name in a migration.
Worth the extra words on anything non-trivial.
─────────────────────────────────────────
4. The Working Dataset
Every remaining chapter in this module uses this. Run it once.
import sqlite3
conn = sqlite3.connect("school.db")
conn.execute("PRAGMA foreign_keys = ON")
conn.executescript("""
DROP TABLE IF EXISTS enrollments;
DROP TABLE IF EXISTS students;
DROP TABLE IF EXISTS courses;
CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
marks INTEGER CHECK (marks BETWEEN 0 AND 100)
);
CREATE TABLE courses (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
credits INTEGER NOT NULL CHECK (credits > 0),
dept TEXT NOT NULL
);
CREATE TABLE enrollments (
student_id INTEGER NOT NULL REFERENCES students(id) ON DELETE CASCADE,
course_id INTEGER NOT NULL REFERENCES courses(id) ON DELETE RESTRICT,
grade TEXT CHECK (grade IN ('A','B','C','D','F')),
PRIMARY KEY (student_id, course_id)
);
INSERT INTO students (id, name, city, marks) VALUES
(1,'Asha','Pune',88), (2,'Ravi','Delhi',74),
(3,'Meera','Pune',91), (4,'Karan','Mumbai',65),
(5,'Divya','Delhi',82), (6,'Sam','Pune',NULL); -- NULL marks: not yet assessed
INSERT INTO courses (id, title, credits, dept) VALUES
(10,'Databases',4,'CS'), (11,'Networks',3,'CS'),
(12,'Statistics',4,'Math'),(13,'Ethics',2,'Phil');
INSERT INTO enrollments (student_id, course_id, grade) VALUES
(1,10,'A'), (1,11,'B'), (1,12,'A'),
(2,10,'C'), (2,12,'B'),
(3,10,'A'), (3,11,'A'), (3,12,'A'), (3,13,'B'),
(4,11,'D'),
(5,10,'B'), (5,13,NULL); -- enrolled, not yet graded
-- NOTE: student 6 (Sam) is enrolled in NOTHING, and course 13 has few students.
-- Both are deliberate — they make outer joins and NULL handling visible later.
""")
conn.commit()Why the Dataset Has Awkward Bits
─────────────────────────────────────────
Sam has NULL marks and no enrollments.
One enrollment has a NULL grade.
A tidy dataset hides exactly the behaviour that
causes real bugs. These rows make outer joins,
aggregate NULL-skipping and IS NULL visible in
every later chapter.
─────────────────────────────────────────
5. ALTER and DROP
ALTER TABLE students ADD COLUMN phone TEXT;
ALTER TABLE students RENAME COLUMN marks TO score;
ALTER TABLE students DROP COLUMN phone;
ALTER TABLE students ADD CONSTRAINT chk_score CHECK (score >= 0); -- not in SQLiteSafe vs Unsafe Changes
─────────────────────────────────────────
SAFE (additive)
add a NULLABLE column
add a column with a DEFAULT
add an index (Module 5)
relax a constraint
UNSAFE (breaking)
drop a column — breaks SELECT *
rename anything — breaks every reference
add NOT NULL without a default on a populated
table — fails immediately
narrow a type — may fail on existing
rows
This is Module 1, Chapter 3's LOGICAL DATA
INDEPENDENCE being partial: additive changes are
absorbed, removals are not.
─────────────────────────────────────────
DROP vs DELETE vs TRUNCATE
─────────────────────────────────────────
DROP TABLE t removes the TABLE and its data.
Structure gone.
TRUNCATE TABLE t removes all ROWS fast. Structure
stays. Usually not rollback-able,
and ignores row triggers.
DELETE FROM t removes rows one at a time,
logged, transactional,
rollback-able, fires triggers.
DELETE when you may need to undo it. TRUNCATE
when you are certain and the table is large.
─────────────────────────────────────────
6. Summary & Next Steps
Key Takeaways
- SQL splits into DDL, DML and DCL; DDL implicitly commits in most databases, which is why migrations are tested on a copy first.
- Use
NUMERICfor money — floating point cannot represent decimal fractions exactly, and the drift is unfindable later. - Table-level constraints are required for composite keys and any rule spanning two columns; naming constraints buys readable errors and droppable migrations.
- Additive schema changes are safe and removals are breaking, which is the practical face of partial logical data independence.
Concept Check
- Why is
REALthe wrong type for a price column, and what goes wrong specifically? - When must a constraint be declared at the table level rather than on a column?
- Which of
DROP,TRUNCATEandDELETEcan be rolled back, and when would you choose each?
Next Chapter
→ Chapter 2: Querying with SELECT
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index