🔄 Lesson 6: CRUD Operations
CRUD — Create, Read, Update, Delete — is the heartbeat of every database application. Whether it's a shopping cart, a social media feed, or a banking system, everything boils down to these four operations. In this lesson, you'll master them all.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Create data with
INSERT INTO(single row, multiple rows, and with defaults) - Read data with
SELECT(all columns, specific columns, and aliases) - Update data with
UPDATE ... SET ... WHERE - Delete data with
DELETE FROM ... WHERE - Understand the critical role of
WHEREin preventing accidental mass changes - Use
REPLACEandINSERT ... ON DUPLICATE KEY UPDATEfor upsert patterns
Estimated Time: 50 minutes
Prerequisites: Data types and constraints (Lesson 5)
📑 In This Lesson
What Is CRUD?
CRUD is an acronym for the four basic operations you can perform on data:
INSERT INTO"] --> R["🔵 Read
SELECT"] R --> U["🟡 Update
UPDATE ... SET"] U --> D["🔴 Delete
DELETE FROM"]
| CRUD Operation | SQL Statement | What It Does | Real-World Example |
|---|---|---|---|
| Create | INSERT INTO |
Adds new rows to a table | A new customer signs up |
| Read | SELECT |
Retrieves data from a table | Viewing your order history |
| Update | UPDATE |
Modifies existing rows | Changing your email address |
| Delete | DELETE |
Removes rows from a table | Cancelling an account |
Every web application, mobile app, and API you've ever used is performing these four operations behind the scenes. Learning them well is the foundation for everything else in SQL.
Setting Up Our Data
We'll work with the bookstore database from Lesson 4. If you need to rebuild it, run this first:
CREATE DATABASE IF NOT EXISTS bookstore
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE bookstore;
-- Drop existing tables so we start fresh
DROP TABLE IF EXISTS books;
DROP TABLE IF EXISTS customers;
DROP TABLE IF EXISTS categories;
CREATE TABLE books (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
author VARCHAR(100) NOT NULL,
price DECIMAL(8, 2) NOT NULL,
pages INT,
published DATE,
in_stock BOOLEAN DEFAULT TRUE
);
CREATE TABLE customers (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
city VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255)
);
📖 Follow Along!
Open your MySQL CLI (mysql -u root -p) and run each command as you read. This lesson is hands-on — you'll learn best by doing.
CREATE — INSERT INTO
The INSERT INTO statement adds new rows to a table. There are several ways to use it.
Insert a Single Row
The most common form — specify the columns and their values:
INSERT INTO books (title, author, price, pages, published)
VALUES ('Dune', 'Frank Herbert', 12.99, 412, '1965-08-01');
Output:
Query OK, 1 row affected (0.01 sec)
Notice what we didn't include:
id— handled byAUTO_INCREMENT(MySQL assigns 1 automatically)in_stock— handled byDEFAULT TRUE
Verify It Worked
SELECT * FROM books;
Output:
+----+------+----------------+-------+-------+------------+----------+
| id | title| author | price | pages | published | in_stock |
+----+------+----------------+-------+-------+------------+----------+
| 1 | Dune | Frank Herbert | 12.99 | 412 | 1965-08-01 | 1 |
+----+------+----------------+-------+-------+------------+----------+
Insert Multiple Rows at Once
You can insert several rows in a single statement — much faster than individual inserts:
INSERT INTO books (title, author, price, pages, published) VALUES
('1984', 'George Orwell', 9.99, 328, '1949-06-08'),
('To Kill a Mockingbird', 'Harper Lee', 11.99, 281, '1960-07-11'),
('The Great Gatsby', 'F. Scott Fitzgerald', 8.99, 180, '1925-04-10'),
('Pride and Prejudice', 'Jane Austen', 7.99, 432, '1813-01-28'),
('The Hobbit', 'J.R.R. Tolkien', 10.99, 310, '1937-09-21');
Output:
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0
✅ Performance Tip
Multi-row INSERT is significantly faster than individual inserts because MySQL processes them in a single transaction. When loading data, always batch your inserts.
Insert with All Columns (No Column List)
If you provide values for every column in order, you can skip the column list:
-- Works, but not recommended:
INSERT INTO books
VALUES (NULL, 'Fahrenheit 451', 'Ray Bradbury', 10.49, 158, '1953-10-19', TRUE);
The NULL for id tells MySQL to use AUTO_INCREMENT.
⚠️ Always Name Your Columns
Skipping the column list is fragile — if someone adds or reorders columns, your insert breaks. Always explicitly list the columns for clarity and safety:
-- Always prefer this:
INSERT INTO books (title, author, price, pages, published)
VALUES ('Fahrenheit 451', 'Ray Bradbury', 10.49, 158, '1953-10-19');
Now Let's Add Customers and Categories
-- Add some customers
INSERT INTO customers (first_name, last_name, email, city) VALUES
('Alice', 'Johnson', 'alice@email.com', 'New York'),
('Bob', 'Smith', 'bob@email.com', 'Chicago'),
('Carol', 'Williams', 'carol@email.com', 'Los Angeles'),
('Dave', 'Brown', 'dave@email.com', 'Houston'),
('Eve', 'Davis', 'eve@email.com', NULL);
-- Add some categories
INSERT INTO categories (name, description) VALUES
('Fiction', 'Novels, short stories, and other imaginative works'),
('Science Fiction', 'Stories exploring futuristic or scientific concepts'),
('Classic', 'Timeless literary works recognized across generations'),
('Fantasy', 'Stories set in imaginary worlds with magical elements'),
('Dystopian', 'Stories about oppressive societies and dark futures');
Notice that Eve has NULL for city — we don't know where she lives, and that's OK because city allows NULL.
READ — SELECT
SELECT is the most-used SQL statement by far — it retrieves data from tables. You'll write more SELECT queries than any other type of SQL.
SELECT * — All Columns
The asterisk (*) means "give me everything":
SELECT * FROM books;
Output:
+----+-----------------------+---------------------+-------+-------+------------+----------+
| id | title | author | price | pages | published | in_stock |
+----+-----------------------+---------------------+-------+-------+------------+----------+
| 1 | Dune | Frank Herbert | 12.99 | 412 | 1965-08-01 | 1 |
| 2 | 1984 | George Orwell | 9.99 | 328 | 1949-06-08 | 1 |
| 3 | To Kill a Mockingbird | Harper Lee | 11.99 | 281 | 1960-07-11 | 1 |
| 4 | The Great Gatsby | F. Scott Fitzgerald | 8.99 | 180 | 1925-04-10 | 1 |
| 5 | Pride and Prejudice | Jane Austen | 7.99 | 432 | 1813-01-28 | 1 |
| 6 | The Hobbit | J.R.R. Tolkien | 10.99 | 310 | 1937-09-21 | 1 |
| 7 | Fahrenheit 451 | Ray Bradbury | 10.49 | 158 | 1953-10-19 | 1 |
+----+-----------------------+---------------------+-------+-------+------------+----------+
💡 When to Use SELECT *
SELECT * is great for quick exploration and debugging, but in production code you should always name the columns you need. Fetching unnecessary columns wastes bandwidth and memory, especially with TEXT or BLOB columns.
SELECT Specific Columns
Name only the columns you need:
SELECT title, author, price FROM books;
Output:
+-----------------------+---------------------+-------+
| title | author | price |
+-----------------------+---------------------+-------+
| Dune | Frank Herbert | 12.99 |
| 1984 | George Orwell | 9.99 |
| To Kill a Mockingbird | Harper Lee | 11.99 |
| The Great Gatsby | F. Scott Fitzgerald | 8.99 |
| Pride and Prejudice | Jane Austen | 7.99 |
| The Hobbit | J.R.R. Tolkien | 10.99 |
| Fahrenheit 451 | Ray Bradbury | 10.49 |
+-----------------------+---------------------+-------+
Column Aliases with AS
Rename columns in the output using AS:
SELECT
title AS book_title,
author AS written_by,
price AS cost_usd
FROM books;
Output:
+-----------------------+---------------------+----------+
| book_title | written_by | cost_usd |
+-----------------------+---------------------+----------+
| Dune | Frank Herbert | 12.99 |
| 1984 | George Orwell | 9.99 |
| ... | ... | ... |
+-----------------------+---------------------+----------+
Aliases don't change the actual column names — they only change the output labels. They're useful for making results more readable, especially in reports and joins (covered later).
SELECT with Expressions
You can do calculations and transformations right in the SELECT:
-- Calculate price with 10% tax
SELECT
title,
price,
price * 1.10 AS price_with_tax
FROM books;
-- Combine first and last name
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
email
FROM customers;
CONCAT Output:
+-----------------+-------------------+
| full_name | email |
+-----------------+-------------------+
| Alice Johnson | alice@email.com |
| Bob Smith | bob@email.com |
| Carol Williams | carol@email.com |
| Dave Brown | dave@email.com |
| Eve Davis | eve@email.com |
+-----------------+-------------------+
SELECT DISTINCT — Remove Duplicates
DISTINCT returns only unique values:
-- All unique cities (removes duplicate values)
SELECT DISTINCT city FROM customers;
Output:
+-------------+
| city |
+-------------+
| New York |
| Chicago |
| Los Angeles |
| Houston |
| NULL |
+-------------+
Note that NULL appears in the results — Eve's unknown city. DISTINCT treats all NULLs as one group.
Quick WHERE Preview
You can filter results with WHERE — this is so important it gets its own full lesson (Lesson 7). Here's a quick taste:
-- Books under $10
SELECT title, price FROM books WHERE price < 10;
-- A specific customer
SELECT * FROM customers WHERE email = 'bob@email.com';
-- Books with more than 300 pages
SELECT title, pages FROM books WHERE pages > 300;
UPDATE — Modifying Data
UPDATE changes the values of existing rows. It always has three parts: which table, which columns to change, and which rows to change.
Update a Single Row
-- Change the price of "Dune"
UPDATE books
SET price = 14.99
WHERE id = 1;
Output:
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Verify:
SELECT title, price FROM books WHERE id = 1;
Output:
+-------+-------+
| title | price |
+-------+-------+
| Dune | 14.99 |
+-------+-------+
Update Multiple Columns
Separate column assignments with commas:
-- Update Bob's city and email
UPDATE customers
SET city = 'San Francisco',
email = 'bob.smith@newemail.com'
WHERE id = 2;
Update Multiple Rows
A WHERE clause that matches multiple rows will update all of them:
-- Give a 10% discount on all books over $10
UPDATE books
SET price = price * 0.90
WHERE price > 10;
Output:
Query OK, 4 rows affected (0.01 sec)
Rows matched: 4 Changed: 4 Warnings: 0
Check the "Rows matched" and "Changed" counts — they tell you exactly how many rows were affected.
Mark a Book as Out of Stock
UPDATE books
SET in_stock = FALSE
WHERE title = 'The Great Gatsby';
-- Verify
SELECT title, in_stock FROM books WHERE title = 'The Great Gatsby';
Output:
+------------------+----------+
| title | in_stock |
+------------------+----------+
| The Great Gatsby | 0 |
+------------------+----------+
⚠️ The Golden Rule: Never UPDATE Without WHERE
If you forget the WHERE clause, every row in the table gets updated:
-- ⛔ DANGER — this changes EVERY book's price!
UPDATE books SET price = 0.00;
This is one of the most common and costly mistakes in SQL. Always write your WHERE clause first, or run a SELECT with the same WHERE to preview which rows will be affected before you UPDATE.
✅ Safe Update Pattern
Before running an UPDATE, preview the affected rows with SELECT:
-- Step 1: Preview — which rows will change?
SELECT id, title, price FROM books WHERE price > 10;
-- Step 2: Looks right? Now update.
UPDATE books SET price = price * 0.90 WHERE price > 10;
This "SELECT first, UPDATE second" pattern saves careers.
DELETE — Removing Data
DELETE removes rows from a table. Like UPDATE, the WHERE clause is critical.
Delete a Single Row
-- Delete the book "Fahrenheit 451"
DELETE FROM books WHERE id = 7;
Output:
Query OK, 1 row affected (0.01 sec)
-- Verify it's gone
SELECT * FROM books WHERE id = 7;
Output:
Empty set (0.00 sec)
💡 AUTO_INCREMENT After DELETE
When you delete row 7, the ID number 7 is not reused. The next insert will get ID 8. Gaps in IDs are normal and expected — don't try to "fill" them.
Delete Multiple Rows
-- Delete all books under $9
DELETE FROM books WHERE price < 9;
Delete All Rows
Omitting WHERE deletes every row in the table:
-- ⛔ DANGER — deletes ALL customers!
DELETE FROM customers;
⚠️ The Same Golden Rule: Never DELETE Without WHERE
Without WHERE, DELETE removes every single row from the table. The table structure remains, but all data is gone. Use the same "SELECT first" pattern:
-- Step 1: Who are we about to delete?
SELECT * FROM customers WHERE city IS NULL;
-- Step 2: OK, delete those.
DELETE FROM customers WHERE city IS NULL;
DELETE vs. TRUNCATE vs. DROP
Three ways to remove data, each with different effects:
| Command | What It Removes | Table Structure? | WHERE Clause? | Resets AUTO_INCREMENT? |
|---|---|---|---|---|
DELETE FROM t WHERE ... |
Matching rows | Kept | Yes | No |
DELETE FROM t |
All rows | Kept | No (all rows) | No |
TRUNCATE TABLE t |
All rows | Kept | Not available | Yes |
DROP TABLE t |
All rows + table | Removed | Not available | N/A |
Upsert Patterns
Sometimes you want to insert a row if it doesn't exist, or update it if it does. This is called an upsert (update + insert). MySQL provides two ways to do this.
INSERT ... ON DUPLICATE KEY UPDATE
If the insert would violate a UNIQUE or PRIMARY KEY constraint, it updates the existing row instead:
-- Try to insert a category that might already exist
INSERT INTO categories (name, description)
VALUES ('Fiction', 'Updated description for fiction')
ON DUPLICATE KEY UPDATE
description = VALUES(description);
Since "Fiction" already exists (and name is UNIQUE), this updates its description instead of throwing an error.
💡 When to Use Upserts
Upserts are common in these scenarios:
- Syncing data from an external source (import scripts)
- Updating settings or preferences (insert if first time, update if exists)
- Counters and statistics (increment if exists, create if new)
REPLACE INTO
REPLACE is more aggressive — if the row exists, it deletes it first and then inserts the new one:
REPLACE INTO categories (name, description)
VALUES ('Fiction', 'Novels and imaginative prose');
⚠️ REPLACE Deletes and Re-inserts
REPLACE works by deleting the conflicting row and inserting a new one. This means:
- The
AUTO_INCREMENTID changes (you get a new ID) - Any foreign key references to the old row may break
- Columns not specified in the REPLACE get their default values, not the old values
In most cases, INSERT ... ON DUPLICATE KEY UPDATE is the safer choice.
Exercises
📖 Reset Before Starting
If your data has gotten messy from experimenting, rebuild it by running the setup SQL from the "Setting Up Our Data" section at the top of this lesson.
🏋️ Exercise 1: Insert Data
Add the following books to the books table:
- "Brave New World" by Aldous Huxley, $9.49, 311 pages, published 1932-01-01
- "The Catcher in the Rye" by J.D. Salinger, $8.99, 234 pages, published 1951-07-16
- "Lord of the Rings" by J.R.R. Tolkien, $15.99, 1178 pages, published 1954-07-29
Use a single INSERT statement. Then verify with SELECT * FROM books;
✅ Solution
INSERT INTO books (title, author, price, pages, published) VALUES
('Brave New World', 'Aldous Huxley', 9.49, 311, '1932-01-01'),
('The Catcher in the Rye', 'J.D. Salinger', 8.99, 234, '1951-07-16'),
('Lord of the Rings', 'J.R.R. Tolkien', 15.99, 1178, '1954-07-29');
SELECT * FROM books;
🏋️ Exercise 2: SELECT Practice
Write queries to answer each question:
- Show only the
titleandauthorof all books - Show each book's title and price with 8% tax added (alias the calculated column as
price_with_tax) - Show all unique cities from the customers table
- Show each customer's full name (first + last) and email
✅ Solution
-- 1. Title and author only
SELECT title, author FROM books;
-- 2. Price with 8% tax
SELECT title, price, price * 1.08 AS price_with_tax FROM books;
-- 3. Unique cities
SELECT DISTINCT city FROM customers;
-- 4. Full name and email
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
email
FROM customers;
🏋️ Exercise 3: UPDATE Practice
Perform the following updates:
- Change Alice's city to "Brooklyn"
- Increase the price of all books with more than 400 pages by $2.00
- Mark "1984" as out of stock
- Update Eve's city to "Denver" and her email to "eve.davis@email.com"
Remember: Preview each update with a SELECT first!
✅ Solution
-- 1. Change Alice's city
SELECT * FROM customers WHERE first_name = 'Alice'; -- Preview
UPDATE customers SET city = 'Brooklyn' WHERE id = 1;
-- 2. Increase price for long books
SELECT title, pages, price FROM books WHERE pages > 400; -- Preview
UPDATE books SET price = price + 2.00 WHERE pages > 400;
-- 3. Mark 1984 as out of stock
UPDATE books SET in_stock = FALSE WHERE title = '1984';
-- 4. Update Eve
UPDATE customers
SET city = 'Denver', email = 'eve.davis@email.com'
WHERE first_name = 'Eve' AND last_name = 'Davis';
🏋️ Exercise 4: DELETE Practice
Perform the following deletions:
- Delete the customer with email "dave@email.com"
- Delete all books priced under $9.00
- Verify each deletion with a SELECT
✅ Solution
-- 1. Delete Dave
SELECT * FROM customers WHERE email = 'dave@email.com'; -- Preview
DELETE FROM customers WHERE email = 'dave@email.com';
SELECT * FROM customers; -- Verify
-- 2. Delete cheap books
SELECT title, price FROM books WHERE price < 9.00; -- Preview
DELETE FROM books WHERE price < 9.00;
SELECT * FROM books; -- Verify
🎯 Quick Quiz
Question 1: What happens if you run UPDATE books SET price = 0; (no WHERE)?
Question 2: Which is the best way to insert multiple rows?
Question 3: What does SELECT DISTINCT city FROM customers; do?
Question 4: What's the safe pattern before running an UPDATE or DELETE?
Question 5: What happens to the AUTO_INCREMENT value when you delete a row?
Summary
🎉 Key Takeaways
- INSERT INTO adds new rows — always name your columns explicitly
- Multi-row
INSERT(one statement, multipleVALUESsets) is faster than individual inserts - SELECT retrieves data — use
*for exploration, named columns for production AScreates column aliases;DISTINCTremoves duplicates;CONCATcombines strings- UPDATE ... SET ... WHERE modifies existing rows — never omit WHERE
- DELETE FROM ... WHERE removes rows — never omit WHERE
- The "SELECT first" pattern: preview affected rows before UPDATE or DELETE
INSERT ... ON DUPLICATE KEY UPDATEprovides safe upsert behaviorREPLACEdeletes-then-inserts — usually less safe thanON DUPLICATE KEY UPDATE- Deleted
AUTO_INCREMENTIDs are never reused — gaps are expected
CRUD Quick Reference
| Operation | Syntax |
|---|---|
| Insert one row | INSERT INTO t (cols) VALUES (vals); |
| Insert multiple rows | INSERT INTO t (cols) VALUES (v1), (v2), (v3); |
| Select all | SELECT * FROM t; |
| Select specific columns | SELECT col1, col2 FROM t; |
| Select with alias | SELECT col AS alias FROM t; |
| Select unique values | SELECT DISTINCT col FROM t; |
| Update rows | UPDATE t SET col = val WHERE condition; |
| Delete rows | DELETE FROM t WHERE condition; |
| Upsert | INSERT INTO t (cols) VALUES (vals) ON DUPLICATE KEY UPDATE col = val; |
📚 Additional Resources
- MySQL Docs — INSERT
- MySQL Docs — SELECT
- MySQL Docs — UPDATE
- MySQL Docs — DELETE
- MySQL Docs — INSERT ... ON DUPLICATE KEY UPDATE
🚀 What's Next?
You can now create, read, update, and delete data — the four pillars of database work. But so far, SELECT has been returning everything. In the next lesson, you'll learn to filter your results precisely with WHERE clauses, comparison operators, and pattern matching — the tools that let you ask your database exactly the right question.
🎉 Congratulations!
You've completed Module 2! You can now create databases, design tables with proper types and constraints, and perform all four CRUD operations. That's a solid SQL foundation — time to build on it!