LEARN TO CODE | SQL | 04 CHANGING DATA AND BUILDING PROJECTS
04.1 INSERT, UPDATE and DELETE
Add records, change selected values and remove intended records while checking keys, conditions and related data.
01 | QUERIES CAN CHANGE STORED FACTS
SELECT builds a result without changing the source. INSERT, UPDATE and DELETE deliberately change data. Each has a different purpose: add a record, edit selected fields or remove a record.
We will practise only on a fresh in-memory database. Predict the intended rows first, then inspect them after the operation. This is a practical workflow, not a substitute for appropriate access controls in a real application.
Use this section’s fresh library practice database. Members stores borrowers, Books stores physical copies , and Loans stores borrowing events. Two copies may share a title but have different IDs and barcodes.
Open the Section 04 runner in VS Code
Create
sql_changes.py
, paste this complete runner and click Run Python File. Change the SQL in changes for the operation and query for the inspection SELECT. Keep setup_sql unchanged. Every run starts fresh, so each example below is independent.
This runner uses SQLite through Python, with explicit SQL transaction control. Outside an explicit BEGIN, individual completed write statements commit automatically. For an atomic multi-statement change, use BEGIN and COMMIT, with error handling that rolls back. The transaction lesson explains this distinction.
import sqlite3
# Explicit SQL transaction control; fresh data on every run.
connection = sqlite3.connect(":memory:", isolation_level=None)
connection.execute("PRAGMA foreign_keys = ON")
setup_sql = """
CREATE TABLE IF NOT EXISTS Members (
member_id INTEGER PRIMARY KEY,
member_name TEXT NOT NULL CHECK (length(trim(member_name)) > 0)
);
CREATE TABLE IF NOT EXISTS Books (
book_id INTEGER PRIMARY KEY,
barcode TEXT NOT NULL UNIQUE CHECK (length(trim(barcode)) > 0),
title TEXT NOT NULL CHECK (length(trim(title)) > 0)
);
CREATE TABLE IF NOT EXISTS Loans (
loan_id INTEGER PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES Members(member_id),
book_id INTEGER NOT NULL REFERENCES Books(book_id),
borrowed_on TEXT NOT NULL,
due_on TEXT NOT NULL,
returned_on TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS one_active_loan_per_book
ON Loans(book_id) WHERE returned_on IS NULL;
INSERT INTO Members VALUES (1, 'Maya'), (2, 'Leo'), (3, 'Amina');
INSERT INTO Books VALUES (10, 'LIB-010', 'The Code Book'),
(11, 'LIB-011', 'The Code Book'), (12, 'LIB-012', 'Python Adventures');
INSERT INTO Loans VALUES
(100, 1, 10, '2026-10-01', '2026-10-15', NULL),
(101, 2, 11, '2026-09-01', '2026-09-15', '2026-09-14');
"""
changes = """
-- Put this exercise's INSERT, UPDATE, DELETE or transaction here.
"""
query = """
SELECT * FROM Loans ORDER BY loan_id;
"""
try:
connection.executescript(setup_sql)
connection.executescript(changes)
cursor = connection.execute(query)
print(" | ".join(column[0] for column in cursor.description))
for row in cursor.fetchall():
print(row)
except sqlite3.Error as error:
if connection.in_transaction:
connection.execute("ROLLBACK")
print("Database error:", error)
finally:
connection.close()
Use executescript only for fixed teaching scripts like these, not text assembled from user input. Do not call executescript in the middle of a manually controlled transaction; the complete application later uses execute for its transaction statements.
02 | READ THE STARTING STATE
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo |
| 3 | Amina |
| book_id | barcode | title |
|---|---|---|
| 10 | LIB-010 | The Code Book |
| 11 | LIB-011 | The Code Book |
| 12 | LIB-012 | Python Adventures |
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | NULL |
| 101 | 2 | 11 | 2026-09-01 | 2026-09-15 | 2026-09-14 |
Loan 100 is active because returned_on is NULL. Loan 101 is historical because its return date is recorded. Book 12 has never been borrowed in this starting dataset.
03 | INSERT A NEW RECORD
INSERT INTO Members (member_id, member_name)
VALUES (4, 'Noah');
| member_id | member_name |
|---|---|
| 1 | Maya |
| 2 | Leo |
| 3 | Amina |
| 4 | Noah |
The column list defines how values map into fields. Text uses single quotes. An omitted INTEGER PRIMARY KEY can be assigned by SQLite, but explicit IDs make these small teaching examples easy to follow.
Use clear column lists rather than depending on all-column positional order. A schema change can make an unexplained INSERT VALUES list difficult to maintain.
04 | ADD RELATED DATA IN A VALID ORDER
INSERT INTO Loans (loan_id, member_id, book_id, borrowed_on, due_on)
VALUES (102, 3, 12, '2026-10-10', '2026-10-24');
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | NULL |
| 101 | 2 | 11 | 2026-09-01 | 2026-09-15 | 2026-09-14 |
| 102 | 3 | 12 | 2026-10-10 | 2026-10-24 | NULL |
The referenced member and physical copy already exist. returned_on is omitted and becomes NULL, indicating an active loan.
With foreign keys enabled, an unknown member ID fails rather than creating a dangling reference. The unique partial index in this schema also prevents a second active loan for the same copy.
05 | UPDATE A SPECIFIC ROW
UPDATE Members
SET member_name = 'Maya Chen'
WHERE member_id = 1;
| member_id | member_name |
|---|---|
| 1 | Maya Chen |
| 2 | Leo |
| 3 | Amina |
UPDATE names the table; SET names the new field values; WHERE selects rows. The identifier remains unchanged, so existing loans still reference the same member.
Use stable IDs when targeting a person. Names can repeat. An UPDATE that matches no records can succeed with zero affected rows; that is not proof the intended change happened.
06 | CHANGE MORE THAN ONE FIELD
UPDATE Loans
SET due_on = '2026-10-22', returned_on = NULL
WHERE loan_id = 100;
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-22 | NULL |
Separate SET assignments with commas, not AND. This example explicitly retains the active status while changing the due date.
The SQL schema stores dates as text here. It does not by itself validate a real calendar date or every date relationship; the project application adds date validation.
07 | PREVIEW THE SAME CONDITION
SELECT * FROM Members WHERE member_id = 1;
UPDATE Members
SET member_name = 'Maya Chen'
WHERE member_id = 1;
Previewing helps catch a mistaken condition. In a multi-user system, data could change between separate steps; a transaction and appropriate database rules may be needed for the complete operation.
Without WHERE, UPDATE targets every row. Sometimes a whole-table change is intended, but it must be deliberate. Do not infer that SET applies to only the first visible record.
08 | DELETE REMOVES ROWS
DELETE FROM Books
WHERE book_id = 12;
| book_id | barcode | title |
|---|---|---|
| 10 | LIB-010 | The Code Book |
| 11 | LIB-011 | The Code Book |
This removes the unreferenced physical copy, not merely its title value. The Books table and its columns remain.
DELETE without WHERE targets all rows, subject to constraints. DROP TABLE removes the table itself. Recording a book’s return should normally update the loan, not delete the history.
09 | REFERENCES CAN BLOCK DELETION
Try DELETE FROM Books WHERE book_id = 10 against a fresh setup. It fails because loan 100 references that copy. The default immediate foreign-key behaviour in our schema protects the reference.
Historical loans also reference books. Returning a book does not make its parent row unreferenced. Decide how to archive or retire records rather than remove constraints to force deletion.
TRY IT YOURSELF
Record loan 100 as returned on 2026-10-12, retaining its history.
Reveal a possible solution
UPDATE Loans SET returned_on = '2026-10-12'
WHERE loan_id = 100 AND returned_on IS NULL;
| loan_id | member_id | book_id | borrowed_on | due_on | returned_on |
|---|---|---|---|---|---|
| 100 | 1 | 10 | 2026-10-01 | 2026-10-15 | 2026-10-12 |
The loan remains but is no longer active. A repeated operation with the same condition would affect zero rows.
10 | PRACTICE AND CHECK
TRY IT YOURSELF
Insert an unused book copy with ID 13, barcode LIB-013 and title Database Detectives, then inspect Books.
Reveal a possible solution
INSERT INTO Books (book_id, barcode, title)
VALUES (13, 'LIB-013', 'Database Detectives');
| book_id | barcode | title |
|---|---|---|
| 10 | LIB-010 | The Code Book |
| 11 | LIB-011 | The Code Book |
| 12 | LIB-012 | Python Adventures |
| 13 | LIB-013 | Database Detectives |
A unique barcode identifies a physical copy even if another copy shares the title.
Explain why the target condition matters, what is preserved and which database rules could reject the change. Next, we will group related changes into transactions.
KEEP LEARNING
Previous page 04.2 Transactions, COMMIT and ROLLBACK
References: Python SQLite parameters and transactions · SQLite transactions · Partial indexes