Modules / Database / DML Lab

DML in SQL

Data Manipulation Language changes what is in your tables. Run INSERT, UPDATE and DELETE against a live table, watch every affected row light up, and use a transaction to undo the damage.

inserted updated deleted

DML: Changing What Is Inside the Table

Four verbs do almost all the work in day-to-day SQL. Two of them can ruin your afternoon if you forget a WHERE clause.

The idea in brief

Data Manipulation Language covers the statements that read and change rows: INSERT, UPDATE, DELETE and SELECT. Where DDL defines the container, DML fills and reshapes its contents.

The Four Statements

  • INSERT INTO ... VALUES — adds new rows. Every constraint declared in DDL is checked here; a duplicate primary key is rejected outright.
  • UPDATE ... SET ... WHERE — changes values in existing rows. The WHERE decides which.
  • DELETE FROM ... WHERE — removes rows. The structure stays.
  • SELECT — reads without changing anything. The only one of the four that is always safe.

The missing WHERE clause

UPDATE employees SET salary = 50000;    -- every employee. All of them.
DELETE FROM orders;                     -- every order.

Both are valid SQL, both run without complaint, and both are the reason database people flinch at typing UPDATE into a production console.

Three defences, in order of reliability:

  1. Write the SELECT first. Run SELECT * FROM employees WHERE dept_id = 3 and look at what comes back. Then change the word SELECT * to UPDATE ... SET, keeping the same WHERE.
  2. Wrap it in a transaction. BEGIN, run the statement, check the reported row count, then COMMIT or ROLLBACK. If it says "4,912 rows affected" and you expected 12, roll back.
  3. Turn off autocommit in your client, or enable safe-update mode — MySQL's sql_safe_updates refuses an UPDATE or DELETE without a key-based WHERE.

The row count is the signal that catches this. Get into the habit of predicting it before running and comparing afterwards.

Transactions Are the Safety Net

Unlike DDL, DML is transactional. Wrap your changes in a transaction and nothing is permanent until you say so:

BEGIN; UPDATE employees SET salary = salary * 1.1 WHERE dept = 'Engineering'; -- check the result ROLLBACK; -- or COMMIT;

Press BEGIN in this lab, delete some rows, then press ROLLBACK — they all come back. This is the single most valuable habit when running a statement you are not completely sure about.

Transactions also provide atomicity: if you debit one account and credit another, either both happen or neither does. A crash between the two statements cannot leave money missing.

The four statements that move data

DDL defines the containers; DML fills, changes and empties them.

-- INSERT: add rows
INSERT INTO employees (name, email, salary)
VALUES ('Alice', 'alice@example.com', 75000),
       ('Bob',   'bob@example.com',   62000);

-- UPDATE: change existing rows
UPDATE employees SET salary = salary * 1.05 WHERE dept_id = 3;

-- DELETE: remove rows
DELETE FROM employees WHERE hired_on < '2015-01-01';

-- SELECT: read rows (the one you run a thousand times a day)
SELECT name, salary FROM employees WHERE salary > 70000;

Two habits worth adopting immediately. Always list the columns in an INSERTINSERT INTO employees VALUES (...) breaks the moment someone adds a column. And insert many rows in one statement rather than one statement per row; the difference on a few thousand rows is often a hundredfold, because each statement carries its own round trip and transaction overhead.

Upserts: insert or update in one statement

"Insert this row, or update it if it already exists" is such a common need that every engine has a form of it, and doing it with a SELECT followed by an INSERT is a race condition waiting to happen.

-- PostgreSQL and SQLite
INSERT INTO inventory (sku, quantity)
VALUES ('ABC-1', 10)
ON CONFLICT (sku) DO UPDATE SET quantity = inventory.quantity + EXCLUDED.quantity;

-- MySQL
INSERT INTO inventory (sku, quantity) VALUES ('ABC-1', 10)
ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity);

-- Standard SQL / SQL Server / Oracle
MERGE INTO inventory AS t
USING (SELECT 'ABC-1' AS sku, 10 AS qty) AS s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET quantity = t.quantity + s.qty
WHEN NOT MATCHED THEN INSERT (sku, quantity) VALUES (s.sku, s.qty);

The conflict target must be backed by a unique constraint or index — the engine needs a defined way to detect the collision.

RETURNING (PostgreSQL, and now several others) is the natural companion: it hands back the affected rows, so you can get the generated id without a second query.

INSERT INTO employees (name) VALUES ('Carol') RETURNING id, hired_on;

Changing rows, and the clause you must not forget

INSERT, UPDATE and DELETE are the three statements that change data, and two of them take a WHERE clause that is optional in the grammar and mandatory in practice. Everything here runs against a copy you can reset.

query.sqlSQLite
Result

Guided tour

  1. Insert a row and watch it appear highlighted in green, with the table count rising by one.
  2. Update salaries for one department. Only matching rows turn amber — the filter decided the blast radius.
  3. Switch the filter to "all rows" and run a DELETE. Every row is struck through. This is what a forgotten WHERE looks like.
  4. Now do it inside a transaction: BEGIN, DELETE, then ROLLBACK. The table returns exactly as it was.
  5. Try inserting a duplicate id. The primary key constraint from DDL rejects it — the database defends its own rules.

Worth remembering

DML is where data actually changes, and the WHERE clause is what stands between a targeted fix and a table-wide accident. Preview with SELECT, wrap risky work in a transaction, and let the constraints you declared in DDL catch what you miss.

Updating from another table

Changing rows based on data elsewhere is routine, and the syntax is one of the least portable corners of SQL.

-- PostgreSQL
UPDATE employees e
SET    dept_name = d.name
FROM   departments d
WHERE  d.id = e.dept_id;

-- MySQL
UPDATE employees e
JOIN   departments d ON d.id = e.dept_id
SET    e.dept_name = d.name;

-- Portable, if slower
UPDATE employees e
SET    dept_name = (SELECT name FROM departments d WHERE d.id = e.dept_id);

The portable version has a trap: rows with no matching department get NULL rather than being left alone. Add WHERE EXISTS (...) if that is not what you want.

Deleting and updating at scale

A DELETE touching millions of rows holds locks, generates enormous undo or WAL volume, and can block everything else for minutes. Batch it instead:

-- repeat until zero rows are affected
DELETE FROM events
WHERE  id IN (SELECT id FROM events WHERE created_at < '2025-01-01' LIMIT 10000);

Each batch is a short transaction, other queries get a chance to run between them, and an interruption leaves the job resumable rather than rolled back.

For deleting most of a very large table, an even better pattern is to copy the rows you are keeping into a new table, then swap the names. And for data with a natural time boundary, partitioning turns "delete last year" into "drop a partition", which is instant.

Questions people ask

Does UPDATE rewrite the whole row? In PostgreSQL, yes — an update writes a new row version and marks the old one dead, which is why heavily updated tables need vacuuming. In MySQL's InnoDB, only the changed columns and the undo record.

Why did my INSERT succeed but the data is gone? Autocommit was off and the transaction was never committed, or a rollback happened later in the session.

Can I INSERT from a SELECT? Yes — INSERT INTO archive SELECT * FROM orders WHERE completed is the standard way to copy rows between tables.

How do I get the id of a row I just inserted? RETURNING id where supported, otherwise the driver's last-insert-id function. Never re-query by the values you inserted; that is racy.

Is TRUNCATE a DML statement? It is usually classified as DDL, which is why it auto-commits in some engines and does not fire triggers. Treat it as a structural operation.

What happens if an INSERT violates a constraint? The statement fails and, inside a transaction, the whole transaction is typically marked as aborted — in PostgreSQL you must roll back or use a savepoint before continuing.

Recap in one screen

  • INSERT, UPDATE, DELETE and SELECT are the statements that move data.
  • Always name your columns in INSERT, and insert in batches rather than row by row.
  • A missing WHERE is the classic disaster — write the SELECT first, and use a transaction.
  • Upserts belong in one statement (ON CONFLICT / ON DUPLICATE KEY / MERGE), never in read-then-write application code.
  • Delete and update large volumes in batches, and prefer partition drops where the data allows.

Predict, then reveal

About to run: Now do it inside a transaction. Before it does — what happens to the readout?

Committing to an answer first is the point — the reveal runs the experiment on the visualisation above and reads the real value back, so nothing here is scripted.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. Without scrolling back — what is the one-line takeaway from this module?

  2. What does this module say about “The idea in brief”?

  3. What does this module say about “The Four Statements”?

Cheat sheet

DML in SQL

Data Manipulation Language changes what is in your tables. Run INSERT, UPDATE and DELETE against a live table, watch every affected row light up, and use a transaction to undo the damage.

DATABASE · vizlearn.in/database/dml_in_sql.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.