Data Manipulation
INSERT, UPDATE, and DELETE operations.
Modifying Data in Databases
So far we've only read data with SELECT. Now we'll learn how to create, modify, and delete data. These operations, INSERT, UPDATE, and DELETE, are collectively known as DML (Data Manipulation Language). They're how you actually change what's stored in your database. Be careful: unlike SELECT, these operations permanently modify your data.
Starting Table: employees
We'll modify this table throughout the lesson:
CREATE TABLE employees (
id SERIAL PRIMARY KEY, -- database assigns 1, 2, 3, ...
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
department VARCHAR(50),
salary INTEGER
);
INSERT INTO employees (first_name, last_name, department, salary) VALUES
('Alice', 'Johnson', 'Engineering', 95000),
('Bob', 'Smith', 'Marketing', 65000),
('Charlie', 'Brown', 'Engineering', 105000);
SELECT * FROM employees ORDER BY id;Expected Output:
┌────┬────────────┬───────────┬─────────────┬────────┐ │ id │ first_name │ last_name │ department │ salary │ ├────┼────────────┼───────────┼─────────────┼────────┤ │ 1 │ Alice │ Johnson │ Engineering │ 95000 │ │ 2 │ Bob │ Smith │ Marketing │ 65000 │ │ 3 │ Charlie │ Brown │ Engineering │ 105000 │ └────┴────────────┴───────────┴─────────────┴────────┘
INSERT: Adding New Data
INSERT adds new rows to a table. You specify which columns to fill and what values to use.
Insert One Row
Add a single employee with all required fields.
INSERT INTO employees (first_name, last_name, department, salary)
VALUES ('Diana', 'Martinez', 'Sales', 72000);
SELECT * FROM employees ORDER BY id;Expected Output:
┌────┬────────────┬───────────┬─────────────┬────────┐ │ id │ first_name │ last_name │ department │ salary │ ├────┼────────────┼───────────┼─────────────┼────────┤ │ 1 │ Alice │ Johnson │ Engineering │ 95000 │ │ 2 │ Bob │ Smith │ Marketing │ 65000 │ │ 3 │ Charlie │ Brown │ Engineering │ 105000 │ │ 4 │ Diana │ Martinez │ Sales │ 72000 │ └────┴────────────┴───────────┴─────────────┴────────┘
Diana got id 4 without us naming it: the SERIAL column pulled the next value from its sequence.
Insert Partial Data
You don't need to specify every column, omitted columns get NULL or their default values.
INSERT INTO employees (first_name, last_name)
VALUES ('Eve', 'Davis');
SELECT * FROM employees WHERE first_name = 'Eve';Expected Output:
┌────┬────────────┬───────────┬────────────┬────────┐ │ id │ first_name │ last_name │ department │ salary │ ├────┼────────────┼───────────┼────────────┼────────┤ │ 5 │ Eve │ Davis │ │ │ └────┴────────────┴───────────┴────────────┴────────┘
department and salary are NULL. Note how psql prints NULL: as an empty cell, not the word "NULL". An empty string would look exactly the same, which is one reason IS NULL exists.
Insert Multiple Rows
Insert several rows in a single statement (more efficient).
INSERT INTO employees (first_name, last_name, department, salary)
VALUES
('Frank', 'Wilson', 'Marketing', 70000),
('Grace', 'Lee', 'Sales', 78000),
('Henry', 'Taylor', 'Engineering', 92000);
SELECT * FROM employees ORDER BY id;Expected Output:
┌────┬────────────┬───────────┬─────────────┬────────┐ │ id │ first_name │ last_name │ department │ salary │ ├────┼────────────┼───────────┼─────────────┼────────┤ │ 1 │ Alice │ Johnson │ Engineering │ 95000 │ │ 2 │ Bob │ Smith │ Marketing │ 65000 │ │ 3 │ Charlie │ Brown │ Engineering │ 105000 │ │ 4 │ Diana │ Martinez │ Sales │ 72000 │ │ 5 │ Eve │ Davis │ │ │ │ 6 │ Frank │ Wilson │ Marketing │ 70000 │ │ 7 │ Grace │ Lee │ Sales │ 78000 │ │ 8 │ Henry │ Taylor │ Engineering │ 92000 │ └────┴────────────┴───────────┴─────────────┴────────┘
3 rows in one statement, one round trip to the server instead of three.
Insert from Another Table
Copy data from one table to another using SELECT.
-- Assumes an employees_archive table already exists with matching columns: -- CREATE TABLE employees_archive (id INTEGER, first_name VARCHAR(50), last_name VARCHAR(50)); INSERT INTO employees_archive (id, first_name, last_name) SELECT id, first_name, last_name FROM employees WHERE department = 'Sales';
Copies all Sales employees to the archive table
UPDATE: Modifying Existing Data
UPDATE changes values in existing rows. Always use a WHERE clause unless you want to update every row.
Update One Row
Give Alice a raise.
UPDATE employees SET salary = 100000 WHERE id = 1;
Before / After:
Before: │ 1 │ Alice │ Johnson │ Engineering │ 95000 │ After: │ 1 │ Alice │ Johnson │ Engineering │ 100000 │ ← Updated
Update Multiple Columns
Bob gets promoted and a raise.
UPDATE employees
SET
department = 'Engineering',
salary = 85000
WHERE id = 2;Before / After:
Before: │ 2 │ Bob │ Smith │ Marketing │ 65000 │ After: │ 2 │ Bob │ Smith │ Engineering │ 85000 │ ← Both changed
Update Multiple Rows
Give all Marketing employees a 10% raise.
UPDATE employees SET salary = salary * 1.10 WHERE department = 'Marketing';
Result: All Marketing employees get 10% increase
Before: Frank: $70,000 After: Frank: $77,000 (70000 * 1.10)
Conditional Update
Update only if certain conditions are met.
UPDATE employees SET salary = salary + 5000 WHERE department = 'Engineering' AND salary < 90000;
Only Engineering employees earning less than $90k get the bonus
-- DISASTER: Sets everyone's salary to 50000 UPDATE employees SET salary = 50000; -- SAFE: Only updates specific employee UPDATE employees SET salary = 50000 WHERE id = 2;
DELETE: Removing Data
DELETE removes rows from a table permanently. There's no undo button.
Delete One Row
Remove a specific employee by ID.
DELETE FROM employees WHERE id = 5;
Before / After:
Before: │ 5 │ Eve │ Davis │ NULL │ NULL │ ← Will be deleted (8 employees) After: Row removed, 7 employees remain
Delete Multiple Rows
Remove all employees from a specific department.
DELETE FROM employees WHERE department = 'Sales';
Result: Removes Diana and Grace (both in Sales)
Deleted: │ 4 │ Diana │ Martinez │ Sales │ 72000 │ ✗ │ 7 │ Grace │ Lee │ Sales │ 78000 │ ✗
Delete with Complex Condition
Remove employees meeting multiple criteria.
DELETE FROM employees WHERE salary < 70000 AND department = 'Marketing';
Only removes Marketing employees earning less than $70k
Delete All Rows (Dangerous!)
Remove everything from the table (structure remains).
DELETE FROM employees;
Warning: Deletes all rows! Table is now empty.
Before: 8 rows After: 0 rows (but table structure still exists)
-- Step 1: TEST with SELECT SELECT * FROM employees WHERE id = 5; -- Step 2: Verify it returns what you expect -- Step 3: THEN delete DELETE FROM employees WHERE id = 5;
TRUNCATE: Fast Delete All
TRUNCATE removes all rows instantly, faster than DELETE, because it discards whole data files instead of marking rows dead one at a time. It has no WHERE clause: it is all or nothing.
-- Removes every row, but leaves the sequence where it was TRUNCATE TABLE employees; -- Add RESTART IDENTITY to also reset the id counter back to 1 TRUNCATE TABLE employees RESTART IDENTITY; -- In PostgreSQL, TRUNCATE is transactional like any other statement: BEGIN; TRUNCATE TABLE employees; -- table now looks empty ROLLBACK; -- and every row is back
Immediately removes all rows. Note that plain TRUNCATE leaves sequences alone: you need RESTART IDENTITY to reset the id counter.
TRUNCATE vs DELETE
- TRUNCATE: Much faster on big tables, all rows or nothing, no WHERE, needs RESTART IDENTITY to reset counters, takes an exclusive lock on the table
- DELETE: Slower, can target rows with WHERE, fires row-level triggers, leaves dead rows for VACUUM to reclaim
- Both can be rolled back in PostgreSQL: wrap either one in BEGIN and ROLLBACK undoes it
RETURNING: Get Data Back
Some databases (PostgreSQL) let you see what was inserted, updated, or deleted.
INSERT with RETURNING
INSERT INTO employees (first_name, last_name, salary)
VALUES ('John', 'Doe', 80000)
RETURNING id, first_name, salary;Expected Output:
┌────┬────────────┬────────┐ │ id │ first_name │ salary │ ├────┼────────────┼────────┤ │ 9 │ John │ 80000 │ └────┴────────────┴────────┘ The id was generated by the sequence, so this is the only way to learn it without issuing a second query.
UPDATE with RETURNING
UPDATE employees SET salary = salary * 1.10 WHERE department = 'Engineering' RETURNING first_name, salary;
Expected Output:
┌────────────┬────────┐ │ first_name │ salary │ ├────────────┼────────┤ │ Alice │ 104500 │ │ Charlie │ 115500 │ │ Henry │ 101200 │ └────────────┴────────┘ Three rows changed and you see all three, without a follow-up SELECT. (95000 * 1.10 = 104500. salary is INTEGER, so the numeric result is rounded back to a whole number on the way in.)
Best Practices & Safety
✅ Always Use WHERE
Unless you genuinely want to affect every row, always include a WHERE clause in UPDATE and DELETE statements.
✅ Test with SELECT First
Before UPDATE or DELETE, run a SELECT with the same WHERE clause to see what will be affected.
✅ Use Transactions
Wrap dangerous operations in BEGIN/COMMIT so you can ROLLBACK if something goes wrong.
✅ Backup Before Bulk Changes
Before running large UPDATEs or DELETEs on production, always have a recent backup.
❌ Never Run in Production Without Testing
Test all data manipulation queries on development or staging databases first.
Using Transactions for Safety
Transactions let you test changes before making them permanent.
-- Start transaction BEGIN; -- Make changes UPDATE employees SET salary = salary * 1.20 WHERE department = 'Engineering'; -- Check if it looks correct SELECT * FROM employees WHERE department = 'Engineering'; -- If good: make it permanent COMMIT; -- If bad: undo everything ROLLBACK;
Changes aren't permanent until COMMIT. Use ROLLBACK to undo.
Key Takeaways
- INSERT adds new rows to a table
- UPDATE modifies existing rows - always use WHERE
- DELETE removes rows - always use WHERE
- TRUNCATE removes all rows quickly
- Test with SELECT first before UPDATE or DELETE
- Use transactions to safely test changes
- RETURNING shows affected rows (PostgreSQL)
- Data manipulation is permanent, there's no undo button, so always be cautious and test thoroughly