Skip to content

CREATE, INSERT, UPDATE, DELETE

Chapter 10 19 code blocks 3708 words

Everything so far has been read-only. This chapter changes data — which means it is the chapter where a mistake actually costs something. Teach the safety habits alongside the syntax, not afterwards.

Analysts often think they will never need this. They do: building a temporary table for an analysis, loading a CSV, fixing a data-quality issue, creating a view for a dashboard.

CREATE TABLE

sql9 lines
CREATE TABLE IF NOT EXISTS assignments (
    assignment_id  INTEGER PRIMARY KEY,
    course_id      INTEGER NOT NULL,
    title          TEXT    NOT NULL,
    max_points     INTEGER NOT NULL DEFAULT 100,
    due_date       DATE,
    created_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE
);

No result table (1 statements executed, 0 ms)

Reading it line by line is the lesson: the type says what can be stored, and the constraints say what is allowed.

💡 `IF NOT EXISTS` and its sibling

CREATE TABLE IF NOT EXISTS makes a setup script re-runnable without errors. DROP TABLE IF EXISTS does the same at the other end — which is why the Chapter 1 setup script starts with four of them.

You can also create a table directly from a query — extremely useful for snapshotting an analysis:

sql9 lines
CREATE TABLE student_summary AS
SELECT
    s.student_id,
    s.first_name || ' ' || s.last_name AS full_name,
    COUNT(e.enrollment_id)             AS courses,
    ROUND(AVG(e.grade), 1)             AS avg_grade
FROM students s
LEFT JOIN enrollments e ON s.student_id = e.student_id
GROUP BY s.student_id;

No result table (1 statements executed, 1 ms)

⚠️ `CREATE TABLE AS` copies data, not rules

The new table gets the rows and column types, but no primary key, no foreign keys, no constraints, no indexes. It is a snapshot, not a real table. Fine for analysis; never the basis of a production table.

Constraints

Constraints are rules the database enforces for you. Every rule you push into the schema is a rule that can never be broken by a buggy script at 3 a.m.

Constraint Guarantees Example
PRIMARY KEY Unique + not NULL, one per table student_id INTEGER PRIMARY KEY
NOT NULL A value must be supplied first_name TEXT NOT NULL
UNIQUE No two rows share this value email TEXT UNIQUE
CHECK A custom condition holds CHECK (grade BETWEEN 0 AND 100)
DEFAULT Value used when none is given status TEXT DEFAULT 'active'
FOREIGN KEY Must point at an existing parent row REFERENCES courses(course_id)
sql9 lines
CREATE TABLE students_v2 (
    student_id  INTEGER PRIMARY KEY,
    email       TEXT    UNIQUE NOT NULL,
    first_name  TEXT    NOT NULL,
    gender      TEXT    CHECK (gender IN ('M','F')),
    birth_date  DATE    CHECK (birth_date < DATE('now')),
    status      TEXT    NOT NULL DEFAULT 'active',
    CHECK (LENGTH(first_name) >= 2)          -- table-level check
);

No result table (1 statements executed, 0 ms)

📌 Why constraints belong in the database, not the application

Application code can be bypassed — by a second app, an admin script, a manual fix, an intern with a SQL client. The database is the last line of defence, and it never forgets. A CHECK (grade BETWEEN 0 AND 100) makes a grade of 5000 impossible, not merely unlikely.

ON DELETE — what happens to the children

python3 lines
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE RESTRICT   -- default: refuse
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE    -- delete children too
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE SET NULL   -- orphan them
⚠️ CASCADE is more dangerous than it looks

DELETE FROM courses WHERE course_id = 2; with ON DELETE CASCADE silently deletes five enrollments as well — and if those had cascading children, those go too. One statement, an unknown blast radius. Prefer the default RESTRICT and delete deliberately, in order.

⚠️ SQLite ignores foreign keys unless you switch them on

For backwards compatibility, SQLite does not enforce foreign keys by default. You must run this in every session:

PRAGMA foreign_keys = ON; Without it, an insert referencing a non-existent student succeeds happily. This surprises everybody the first time.

INSERT (and upsert)

python15 lines
-- Named columns — the only form you should use in saved code
INSERT INTO students (student_id, first_name, last_name, gender, city, birth_date, enrolled_date)
VALUES (11, 'Nodira', 'Yusupova', 'F', 'Tashkent', '2005-04-01', '2026-09-01');

-- Positional — shorter, but breaks the day a column is added
INSERT INTO students VALUES (11, 'Nodira', 'Yusupova', 'F', 'Tashkent', '2005-04-01', '2026-09-01');

-- Many rows in one statement — much faster than many statements
INSERT INTO courses (course_id, title, category, credits, price) VALUES
    (7, 'Power BI',      'Data', 3, 1600000),
    (8, 'Excel for Analysts', 'Data', 2,  800000);

-- Columns with a DEFAULT or allowing NULL may be omitted
INSERT INTO courses (course_id, title) VALUES (9, 'Statistics II');
-- credits gets its DEFAULT of 3; category and price become NULL

INSERT ... SELECT

Insert the result of a query — how you copy, archive or transform data in bulk:

sql11 lines
CREATE TABLE top_students (student_id INTEGER, full_name TEXT, avg_grade REAL);

INSERT INTO top_students (student_id, full_name, avg_grade)
SELECT s.student_id,
       s.first_name || ' ' || s.last_name,
       AVG(e.grade)
FROM students s JOIN enrollments e USING (student_id)
WHERE e.grade IS NOT NULL
GROUP BY s.student_id
HAVING AVG(e.grade) >= 85;
-- inserts Aziza 90.0, Dilnoza 88.0, Malika 89.0, Otabek 95.5

No result table (2 statements executed, 1 ms)

Upsert — insert or update

python11 lines
-- SQLite / PostgreSQL
INSERT INTO courses (course_id, title, price)
VALUES (1, 'Python Basics', 1300000)
ON CONFLICT (course_id) DO UPDATE SET price = excluded.price;

-- Ignore the row instead of failing
INSERT OR IGNORE INTO courses (course_id, title) VALUES (1, 'Python Basics');

-- MySQL
INSERT INTO courses (course_id, title, price) VALUES (1, 'Python Basics', 1300000)
ON DUPLICATE KEY UPDATE price = VALUES(price);

Upsert is what every data-loading script needs: run it twice and the result should be the same as running it once. That property is called idempotency, and it is what makes a pipeline safe to re-run after a failure.

UPDATE

python15 lines
-- Change one row
UPDATE students SET city = 'Tashkent' WHERE student_id = 7;

-- Several columns at once
UPDATE enrollments
SET grade = 85, status = 'completed'
WHERE enrollment_id = 3;

-- Compute the new value from the old one
UPDATE courses SET price = price * 1.10 WHERE category = 'Data';

-- Conditional update with CASE
UPDATE enrollments
SET status = CASE WHEN grade >= 60 THEN 'completed' ELSE 'failed' END
WHERE grade IS NOT NULL AND status = 'active';

No result table (4 statements executed, 0 ms)

⚠️ The safety ritual — teach this as a rule, not a suggestion

Before every UPDATE or DELETE:

python5 lines
-- STEP 1: see exactly which rows you are about to change
SELECT * FROM students WHERE student_id = 7;

-- STEP 2: only now, swap SELECT * for the UPDATE, keeping the same WHERE
UPDATE students SET city = 'Tashkent' WHERE student_id = 7;
student_idfirst_namelast_namegendercitybirth_dateenrolled_date
7DostonAlimovMTashkent2004-02-082025-09-10

1 row

1 ms

An UPDATE without a WHERE changes every row in the table, and there is no undo outside a transaction. This is the most common way people damage a production database.

Updating from another table (dialects differ noticeably here):

python12 lines
-- SQLite / PostgreSQL: correlated subquery
UPDATE students
SET city = 'Tashkent'
WHERE student_id IN (
    SELECT student_id FROM enrollments WHERE course_id = 5
);

-- PostgreSQL also has UPDATE ... FROM
UPDATE students s
SET city = t.city
FROM temp_city_fixes t
WHERE s.student_id = t.student_id;

DELETE, TRUNCATE, DROP

DELETE TRUNCATE DROP
Removes Chosen rows All rows The whole table
Has WHERE Yes No No
Table survives Yes Yes No
Can roll back Yes (in a transaction) Usually no Usually no
Speed on a big table Slow (logs each row) Very fast Very fast
Fires triggers Yes No —
sql3 lines
DELETE FROM enrollments WHERE status = 'dropped';       -- removes 1 row
DELETE FROM enrollments;                                 -- removes ALL 20 rows
DROP TABLE IF EXISTS top_students;                       -- table gone entirely

No result table (3 statements executed, 0 ms)

⚠️ SQLite has no TRUNCATE

Use DELETE FROM table; — SQLite optimises exactly this case internally. To also reset auto-increment counters, delete the table's row from sqlite_sequence.

ALTER TABLE

ALTER TABLE changes a table's structure after it already holds data.

python12 lines
-- add a column (existing rows get NULL, or the DEFAULT)
ALTER TABLE students ADD COLUMN phone TEXT;
ALTER TABLE students ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1;

-- rename a column (SQLite 3.25+)
ALTER TABLE students RENAME COLUMN phone TO phone_number;

-- rename the table
ALTER TABLE students RENAME TO learners;

-- drop a column (SQLite 3.35+)
ALTER TABLE students DROP COLUMN phone_number;
⚠️ SQLite's `ALTER TABLE` is deliberately limited

SQLite supports only add column, rename column, rename table and drop column. You cannot change a column's type, add a constraint, or add a foreign key to an existing table. PostgreSQL and MySQL support all of those.

The standard workaround is the four-step table rebuild — worth showing once, because students will meet it:

python6 lines
BEGIN TRANSACTION;
CREATE TABLE students_new ( ... new definition ... );
INSERT INTO students_new SELECT ... FROM students;
DROP TABLE students;
ALTER TABLE students_new RENAME TO students;
COMMIT;
⚠️ `ADD COLUMN ... NOT NULL` needs a DEFAULT

Adding a NOT NULL column to a table that already has rows fails, because those rows would need a value and have none. Supply a DEFAULT, or add the column as nullable, UPDATE it, then tighten it (where the database allows tightening).

Transactions — COMMIT and ROLLBACK

A transaction groups several statements into one all-or-nothing unit. Either every statement takes effect, or none does.

python10 lines
BEGIN TRANSACTION;

UPDATE students SET city = 'Samarkand' WHERE student_id = 1;
DELETE FROM enrollments WHERE enrollment_id = 20;

-- Check the result before making it permanent
SELECT * FROM students WHERE student_id = 1;

COMMIT;      -- make it permanent
-- ROLLBACK;   — or undo everything since BEGIN
💡 The safety net students should always use

Before any risky UPDATE or DELETE: BEGIN TRANSACTION; → run it → SELECT to check → COMMIT if correct, ROLLBACK if not. This turns an irreversible mistake into a five-second recovery. It is the single most valuable habit in this chapter.

The classic reason transactions exist is a bank transfer:

python4 lines
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

If the database crashes between the two statements, the transaction guarantees that money is not simply destroyed. Without it, 100 leaves account 1 and never arrives anywhere.

ACID — the four guaranteesAtomicity — all statements succeed, or none do Consistency — constraints hold before and after Isolation — concurrent transactions do not see each other's half-done work Durability — once committed, it survives a crash

Normalization (1NF, 2NF, 3NF)

Normalization is the process of organising tables so that each fact is stored exactly once. Start with a badly designed table and fix it step by step — that is the only way this topic ever makes sense.

matn8 lines
-- The bad table: everything in one place
enrollments_bad
+----+-------------+---------------------+------------------+-------+---------------+
| id | student     | courses             | teacher          | grade | teacher_phone |
+----+-------------+---------------------+------------------+-------+---------------+
| 1  | Aziza K.    | Python, SQL, ML     | Sardor Yoqubov   | 92    | +998901112233 |
| 2  | Bekzod T.   | Python, Statistics  | Sardor Yoqubov   | 75    | +998901112233 |
+----+-------------+---------------------+------------------+-------+---------------+

Violation: courses holds a comma-separated list. You cannot filter, join or count it without string surgery, and WHERE courses = 'SQL' finds nothing.

Fix: one row per student-course pair.

matn4 lines
| id | student   | course     | teacher        | grade | teacher_phone |
| 1  | Aziza K.  | Python     | Sardor Yoqubov | 92    | +998901112233 |
| 2  | Aziza K.  | SQL        | Rustam Halilov | 88    | +998903334455 |
| 3  | Aziza K.  | ML         | Nodira Alimova | NULL  | +998905556677 |

2NF — no partial dependency on part of a composite key

Violation: with a composite key of (student, course), the student's own attributes — city, birth date — depend on the student alone, not on the pair. So they get repeated on every row that student appears in.

Fix: move student attributes into a students table and reference it by student_id.

3NF — no dependency on a non-key column

Violation: teacher_phone does not depend on the enrollment at all; it depends on teacher, which is not the key. Sardor's phone number is stored once per lesson he teaches — and when he changes it, every one of those rows must be updated, or the database contradicts itself.

Fix: a teachers table holding the phone once, referenced by teacher_id.

Apply all three and you get exactly the schema from Chapter 1. That is the point of the exercise — the four tables were not arbitrary.

Form Rule Problem it prevents
1NF One value per cell; no repeating groups Unsearchable lists inside cells
2NF 1NF + no partial dependency on part of the key Repeated attributes of one entity
3NF 2NF + no dependency between non-key columns Update anomalies (the phone number)
💡 Denormalization is not a sin

Normalization is for systems that write data (OLTP) — it protects correctness. Analytics systems (OLAP, data warehouses) deliberately denormalize: they duplicate data into wide tables so reports need fewer joins and run faster. That is the star schema. Both are correct; they optimise for different things. Say this to students explicitly, or they will think every wide table they meet is a mistake.

Views

A view is a saved query that behaves like a table. It stores no data — it re-runs the query every time it is used.

python18 lines
CREATE VIEW enrollment_details AS
SELECT
    e.enrollment_id,
    s.first_name || ' ' || s.last_name AS student,
    c.title                            AS course,
    COALESCE(t.full_name, '(none)')    AS teacher,
    e.grade,
    e.status,
    e.enrolled_at
FROM enrollments e
JOIN      students s ON e.student_id = s.student_id
JOIN      courses  c ON e.course_id  = c.course_id
LEFT JOIN teachers t ON e.teacher_id = t.teacher_id;

-- now query it like any table
SELECT student, course, grade FROM enrollment_details WHERE grade >= 90;

DROP VIEW IF EXISTS enrollment_details;
💡 Why views matter for a teacher
  • Hide complexity. Students can answer real questions against enrollment_details before they can write a four-table join themselves.
  • One definition, many users. "Active student" is defined once; every report agrees.
  • Security. Grant access to a view that excludes salary or phone number instead of the underlying table.
  • Stability. The view's shape can stay the same while the tables underneath are refactored.
⚠️ A view is not a cache

Every query against a view re-executes the whole underlying query. A view over a slow six-table join is still slow — it just looks tidy. When you need the results stored, that is a materialized view (PostgreSQL, Oracle) or a plain table you refresh on a schedule. SQLite has neither; use CREATE TABLE AS.

Practice set

python52 lines
-- 1. Create an assignments table with sensible constraints
CREATE TABLE IF NOT EXISTS assignments (
    assignment_id INTEGER PRIMARY KEY,
    course_id     INTEGER NOT NULL REFERENCES courses(course_id),
    title         TEXT NOT NULL,
    max_points    INTEGER NOT NULL DEFAULT 100 CHECK (max_points > 0),
    due_date      DATE
);

-- 2. Insert three assignments in one statement
INSERT INTO assignments (assignment_id, course_id, title, due_date) VALUES
    (1, 2, 'JOIN exercises',      '2026-03-01'),
    (2, 2, 'Window functions',    '2026-03-15'),
    (3, 1, 'Python loops',        '2026-03-10');

-- 3. Snapshot a summary table from a query
CREATE TABLE course_stats AS
SELECT c.course_id, c.title, COUNT(e.enrollment_id) AS enrollments,
       ROUND(AVG(e.grade),1) AS avg_grade
FROM courses c LEFT JOIN enrollments e USING (course_id)
GROUP BY c.course_id, c.title;

-- 4. Safe update: look first, then change
SELECT * FROM enrollments WHERE enrollment_id = 3;
BEGIN TRANSACTION;
UPDATE enrollments SET grade = 85, status = 'completed' WHERE enrollment_id = 3;
SELECT * FROM enrollments WHERE enrollment_id = 3;
ROLLBACK;   -- or COMMIT if it looks right

-- 5. Raise Data-category prices by 10%
BEGIN TRANSACTION;
UPDATE courses SET price = ROUND(price * 1.10) WHERE category = 'Data';
SELECT title, price FROM courses WHERE category = 'Data';
ROLLBACK;

-- 6. Set status from grade using CASE
UPDATE enrollments
SET status = CASE WHEN grade >= 60 THEN 'completed' ELSE 'failed' END
WHERE grade IS NOT NULL;

-- 7. Create the reporting view
CREATE VIEW IF NOT EXISTS enrollment_details AS
SELECT e.enrollment_id, s.first_name || ' ' || s.last_name AS student,
       c.title AS course, e.grade, e.status
FROM enrollments e
JOIN students s USING (student_id)
JOIN courses  c USING (course_id);

-- 8. Clean up everything this exercise created
DROP VIEW  IF EXISTS enrollment_details;
DROP TABLE IF EXISTS course_stats;
DROP TABLE IF EXISTS assignments;

Interview questions

Q: DELETE vs TRUNCATE vs DROP?

DELETE removes selected rows, can have a WHERE, is transactional and fires triggers. TRUNCATE empties the table quickly with no WHERE and usually cannot be rolled back. DROP removes the table itself.

Q: What is a transaction, and what does ACID stand for?

A group of statements treated as one all-or-nothing unit. Atomicity, Consistency, Isolation, Durability.

Q: Why put constraints in the database rather than the application?

Application code can be bypassed by other apps, scripts or manual access. The database enforces the rule for every client, permanently.

Q: What is the difference between PRIMARY KEY and UNIQUE?

A primary key is unique and not NULL, and there is exactly one per table. A unique constraint permits a NULL (usually one) and a table may have several.

Q: What does ON DELETE CASCADE do, and when is it dangerous?

Deleting a parent row automatically deletes its children. Dangerous because one small delete can cascade through several tables with no warning; RESTRICT is a safer default.

Q: Explain 1NF, 2NF and 3NF in one sentence each.

1NF: one value per cell. 2NF: 1NF plus no column depending on only part of a composite key. 3NF: 2NF plus no non-key column depending on another non-key column.

Q: When would you deliberately denormalize?

In analytics/OLAP systems, where read performance matters more than write anomalies — wide tables and star schemas mean fewer joins per report.

Q: What is a view, and does it store data?

A named saved query. It stores no data and re-executes each time. A materialized view does store results and must be refreshed.

Q: How do you write an insert that is safe to re-run?

Use an upsert — INSERT ... ON CONFLICT DO UPDATE (SQLite/PostgreSQL) or ON DUPLICATE KEY UPDATE (MySQL) — so the operation is idempotent.

Summary

  • CREATE TABLE declares types and rules. CREATE TABLE AS SELECT snapshots a query but copies no constraints or indexes.
  • Constraints — PRIMARY KEY, NOT NULL, UNIQUE, CHECK, DEFAULT, FOREIGN KEY — are the database's last line of defence and cannot be bypassed.
  • SQLite needs PRAGMA foreign_keys = ON; every session or foreign keys are not enforced at all.
  • INSERT: name your columns; insert many rows per statement; use INSERT ... SELECT for bulk work and upsert for idempotent loads.
  • Always SELECT first with the same WHERE, then swap in the UPDATE/DELETE. Missing WHERE changes the entire table.
  • BEGIN TRANSACTION → change → check → COMMIT or ROLLBACK. This is the habit that turns a disaster into a five-second fix. ACID = Atomicity, Consistency, Isolation, Durability.
  • Normalization: 1NF one value per cell · 2NF no partial dependency · 3NF no non-key dependency. Applying all three to a messy table reproduces the Chapter 1 schema exactly.
  • Denormalization is correct for analytics — star schemas trade write-safety for read speed.
  • Views are saved queries that store nothing: they hide complexity, centralise definitions and restrict access — but they do not make a slow query fast.

Next: Chapter 11 — indexes, EXPLAIN and why one query takes 3 milliseconds and another takes 3 minutes.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents