Preparing your learning space...
100% through CRUD Operations tutorials
CRUD stands for Create, Read, Update, and Delete — the four fundamental operations for persistent data storage. In MySQL, these map directly to INSERT, SELECT, UPDATE, and DELETE statements. This tutorial covers all of them, plus MySQL-specific variants like REPLACE, INSERT IGNORE, and ON DUPLICATE KEY UPDATE that give you finer control over how data is written and read.
Purpose: Add a new row (record) to a table.
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);
If you provide values for all columns in the exact table order, you can omit the column list:
INSERT INTO table_name
VALUES (value1, value2, ...);
Note: Omitting the column list is fragile — the order depends on the table definition. Always prefer listing columns explicitly.
INSERT INTO users (name, email, age)
VALUES ('Alice', 'alice@example.com', 30);
Before:
| id | name | age | |
|---|---|---|---|
| (empty) |
After:
| id | name | age | |
|---|---|---|---|
| 1 | Alice | alice@example.com | 30 |
Purpose: Insert several rows in a single statement — faster and more efficient than separate INSERT calls.
INSERT INTO table_name (column1, column2, ...)
VALUES
(row1_val1, row1_val2, ...),
(row2_val1, row2_val2, ...),
(row3_val1, row3_val2, ...);
INSERT INTO users (name, email, age)
VALUES
('Bob', 'bob@example.com', 25),
('Charlie', 'charlie@example.com', 28),
('Diana', 'diana@example.com', 35);
Result (3 new rows):
| id | name | age | |
|---|---|---|---|
| 2 | Bob | bob@example.com | 25 |
| 3 | Charlie | charlie@example.com | 28 |
| 4 | Diana | diana@example.com | 35 |
Note: MySQL has a
max_allowed_packetlimit. For very large bulk inserts (hundreds of thousands of rows), batch in chunks of 500–1000 rows per statement.
Purpose: Insert rows but ignore any that would cause a duplicate-key error (or other warning-level errors). Rows that don't conflict are inserted normally; conflicting rows are silently skipped.
INSERT IGNORE INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);
Assume email has a UNIQUE constraint and 'alice@example.com' already exists:
INSERT IGNORE INTO users (name, email, age)
VALUES ('Eve', 'alice@example.com', 22);
Without IGNORE: ERROR 1062 (23000): Duplicate entry 'alice@example.com'
With IGNORE: No error — the row is silently skipped. MySQL reports a warning instead.
SHOW WARNINGS;
-- | Warning | 1062 | Duplicate entry 'alice@example.com' for key 'email'
Note:
INSERT IGNOREalso converts other non-fatal errors (e.g., data truncation, division by zero) into warnings. Use it carefully — you might hide real data issues.
Purpose: If a row with the same primary key or unique index already exists, delete it and insert a new row. If no conflict exists, it inserts normally.
This is a MySQL-specific extension (also available in SQLite).
REPLACE INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);
PRIMARY KEY or UNIQUE index), MySQL deletes the old row, then inserts the new one.id values (especially AUTO_INCREMENT) increment even on a replace.REPLACE INTO users (id, name, email, age)
VALUES (1, 'Alicia', 'alicia@example.com', 31);
Before:
| id | name | age | |
|---|---|---|---|
| 1 | Alice | alice@example.com | 30 |
After:
| id | name | age | |
|---|---|---|---|
| 1 | Alicia | alicia@example.com | 31 |
⚠️ Warning:
REPLACEis a DELETE + INSERT, not an UPDATE. If your table has foreign key constraints withON DELETE CASCADE, replacing a row can cascade-delete related rows in other tables. TheAUTO_INCREMENTcounter also increments.
Purpose: Insert a row. If a duplicate-key conflict occurs, update the existing row instead of failing (or deleting it).
This is the most flexible upsert mechanism and is MySQL-specific.
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...)
ON DUPLICATE KEY UPDATE
column1 = new_value1,
column2 = new_value2,
...;
You can reference both the values you tried to insert and the existing row values.
INSERT INTO users (id, name, email, age)
VALUES (1, 'Alice', 'alice@newdomain.com', 31)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
age = VALUES(age);
If id = 1 already exists, update the email and age columns.
INSERT INTO page_views (page_slug, view_count)
VALUES ('/home', 1)
ON DUPLICATE KEY UPDATE
view_count = view_count + 1;
VALUES() (MySQL 8.0+)VALUES(column_name) refers to the value you tried to insert for that column. In MySQL 8.0.20+, prefer the AS alias syntax:
INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@b.com', 30) AS new
ON DUPLICATE KEY UPDATE
name = new.name,
email = new.email,
age = new.age;
Note: This is the recommended approach over
REPLACEwhen you want to preserve the existing row'sAUTO_INCREMENTid, foreign key relationships, or only update specific columns. It performs anUPDATE, not aDELETE + INSERT.
Purpose: Retrieve (query) data from one or more tables.
SELECT column1, column2, ...
FROM table_name
WHERE condition;
* selects all columns (use sparingly — prefer explicit columns).WHERE filters rows (optional — omitting it returns all rows).-- All columns, all rows
SELECT * FROM users;
-- Specific columns with a condition
SELECT name, email FROM users WHERE age > 25;
-- Order results
SELECT name, age FROM users ORDER BY age DESC;
-- Limit results
SELECT name, email FROM users LIMIT 5;
-- Combine filters
SELECT name, email, age FROM users
WHERE age >= 18 AND age <= 65
ORDER BY name ASC
LIMIT 10;
Output:
| name | age | |
|---|---|---|
| Bob | bob@example.com | 25 |
| Charlie | charlie@example.com | 28 |
Purpose: Return only unique (non-duplicate) values for the selected column(s).
SELECT DISTINCT column1, column2, ...
FROM table_name;
SELECT DISTINCT age FROM users;
If users has: (25, 30, 25, 35, 30) → Result: (25, 30, 35)
SELECT DISTINCT city, state FROM customers;
Uniqueness is checked across all selected columns. ('Springfield', 'IL') and ('Springfield', 'MA') are both kept.
Note:
SELECT DISTINCTscans all rows to find unique values. On large tables without an index, it can be slow.DISTINCTapplies to the entire row combination — not per-column.
Purpose: Change the values of one or more columns in existing rows.
UPDATE table_name
SET column1 = new_value1,
column2 = new_value2, ...
WHERE condition;
⚠️ CRITICAL: Always use a
WHEREclause unless you intend to update every row. AnUPDATEwithoutWHEREupdates the whole table!
UPDATE users
SET age = 31
WHERE name = 'Alice';
Before:
| id | name | age |
|---|---|---|
| 1 | Alice | 30 |
After:
| id | name | age |
|---|---|---|
| 1 | Alice | 31 |
UPDATE users
SET
email = 'alice@newdomain.com',
age = 32,
updated_at = NOW()
WHERE id = 1;
SET age = age + 1 increments by 1.SET salary = (SELECT AVG(salary) FROM employees)WHERE but whose values don't actually change are reported as "affected" (though ROW_COUNT() may differ with certain client settings).Purpose: Delete existing rows from a table.
DELETE FROM table_name
WHERE condition;
⚠️ CRITICAL: Always use a
WHEREclause unless you intend to empty the table.DELETE FROM users(noWHERE) removes every row but keeps the table structure.
DELETE FROM users
WHERE id = 5;
Before:
| id | name |
|---|---|
| 5 | Eve |
After:
| id | name |
|---|---|
| (row 5 removed) |
TRUNCATE TABLE users; -- DDL: removes + recreates table structure
-- vs
DELETE FROM users; -- DML: deletes row by row, fires triggers, slower
Note:
TRUNCATEcannot be rolled back in most transactions and resetsAUTO_INCREMENTcounters.DELETEcan be rolled back and preserves the counter.
OPTIMIZE TABLE if needed).LIMIT to delete in batches on large tables (though MySQL's DELETE ... LIMIT may not play well with replication).| Practice | Why |
|---|---|
| Always list columns in INSERT | Protects against schema changes and makes intent clear. |
| Always use WHERE with UPDATE/DELETE | Prevents accidental mass modification or data loss. |
| SELECT * sparingly | Explicit columns are self-documenting and more efficient. |
| Use transactions for multi-step writes | Wrap related INSERT/UPDATE/DELETE in START TRANSACTION … COMMIT so you can roll back on error. |
| Prefer ON DUPLICATE KEY UPDATE over REPLACE | Avoids the DELETE+INSERT overhead and preserves FK relationships and AUTO_INCREMENT. |
| Batch inserts in chunks | 500–1000 rows per statement balances speed and packet size. |
| Test with SELECT before running UPDATE/DELETE | SELECT * FROM table WHERE condition first — if it returns the wrong rows, fix the condition before modifying. |
| Use LIMIT with UPDATE/DELETE in production loops | Avoids long table locks. Loop: UPDATE ... WHERE ... LIMIT 1000 until ROW_COUNT() = 0. |
Save your progress and earn XP for completing tutorials.
Keep learning
Technology
MySQL
Lesson group
CRUD Operations
Progress
100% complete