Skip to content
SQL

Learn

INSERT, UPDATE, DELETE and DDL

Modifying data and schema: multi-row inserts, UPDATE with a join, upserts, DELETE vs TRUNCATE vs DROP, and the constraint types with their referential actions.

4 min readUpdated 2 Sept 2026

#INSERT#UPDATE#DDL#Constraints

Read queries dominate SQL interviews, but writes come up reliably in two forms: the DELETE/TRUNCATE/DROP comparison, and "how would you write an upsert". Both are short questions with precise answers.

INSERT

sql
-- Always name the columns: positional inserts break when the table changes
INSERT INTO employees (first_name, last_name, salary)
VALUES ('Ana', 'Silva', 75000);

-- Multi-row: one statement, one round trip, one transaction
INSERT INTO employees (first_name, last_name, salary) VALUES
    ('Ben', 'Cruz', 68000),
    ('Cy',  'Diaz', 82000);

-- INSERT … SELECT: copy or archive without pulling data into the client
INSERT INTO employees_archive (id, first_name, salary)
SELECT id, first_name, salary FROM employees WHERE left_on IS NOT NULL;

-- Get the generated key back
INSERT INTO employees (first_name) VALUES ('Dee') RETURNING employee_id;  -- Postgres

Multi-row INSERT is dramatically faster than a loop of single inserts — one parse, one round trip, one transaction — and saying so is usually the follow-up answer.

Upsert

Insert, or update if it already exists. Every engine spells it differently:

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

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

-- Standard SQL / SQL Server / Oracle
MERGE INTO inventory AS t
USING (SELECT 'A1' 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);

An upsert needs a UNIQUE or PRIMARY KEY constraint to detect the conflict against — that is the detail to mention. Doing it as SELECT then INSERT in application code is the wrong answer: two concurrent requests both see "not there" and both insert.

UPDATE

sql
UPDATE employees SET salary = salary * 1.10 WHERE department = 'Engineering';

-- UPDATE from another table — the syntax differs most here
UPDATE employees e                                     -- PostgreSQL
SET department_name = d.name
FROM departments d
WHERE d.id = e.department_id;

UPDATE employees e                                     -- MySQL
JOIN departments d ON d.id = e.department_id
SET e.department_name = d.name;

An `UPDATE` with no `WHERE` updates every row. The habit that prevents it: write the

SELECT first, confirm the row set, then convert it to an UPDATE. Inside an explicit transaction if the database allows one, so a wrong count can be rolled back.

DELETE vs TRUNCATE vs DROP

The comparison question, asked almost every time:

DELETETRUNCATEDROP
TypeDMLDDLDDL
RemovesSelected rowsAll rowsRows and the table
WHEREYesNoNo
SpeedSlow — row by row, loggedFast — deallocates pagesFast
RollbackYesUsually not (yes in PostgreSQL)Usually not
Fires triggersYesNoNo
Resets identityNoUsually yesN/A

The short version: DELETE for some rows, TRUNCATE to empty a table quickly, DROP to remove the table itself. TRUNCATE skipping triggers and per-row logging is exactly what makes it fast — and what makes it unsafe when triggers carry business logic.

DDL and constraints

sql
CREATE TABLE employees (
    employee_id   SERIAL PRIMARY KEY,
    email         VARCHAR(255) NOT NULL UNIQUE,
    salary        DECIMAL(10, 2) CHECK (salary > 0),
    department_id INT REFERENCES departments(id) ON DELETE SET NULL,
    created_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

ALTER TABLE employees ADD COLUMN phone VARCHAR(20);
ALTER TABLE employees ADD CONSTRAINT chk_salary CHECK (salary <= 1000000);
CREATE INDEX idx_employees_dept ON employees (department_id);
ConstraintGuarantees
PRIMARY KEYUnique and NOT NULL; one per table
UNIQUENo duplicates; multiple NULLs usually allowed
FOREIGN KEYThe referenced row exists
CHECKA predicate holds — but passes on NULL, which is UNKNOWN
NOT NULLA value is present

Both NULL caveats matter and both get asked; see NULL handling.

Referential actions on a foreign key decide what happens when the parent goes:

ON DELETE RESTRICT / NO ACTION   -- refuse (the default)
ON DELETE CASCADE                -- delete the children too
ON DELETE SET NULL               -- orphan them, column must be nullable

CASCADE is convenient and dangerous — one delete can silently remove a large subtree.

Use DECIMAL, never FLOAT, for money: binary floating point cannot represent 0.10 exactly, so sums drift. That is a favourite follow-up.

Transactions

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;    -- or ROLLBACK

ACID in one line each: Atomicity — all or nothing; Consistency — constraints hold before and after; Isolation — concurrent transactions do not see each other's in-progress work; Durability — a committed change survives a crash. Being able to recite that, plus the isolation levels (read uncommitted, read committed, repeatable read, serializable) and the anomalies each prevents, covers the usual follow-ups.

Note that DDL is often implicitly committed — in MySQL and Oracle, an ALTER TABLE inside a transaction commits everything before it. PostgreSQL is the notable engine with transactional DDL.

Mistakes that cost the interview

  • `UPDATE` or `DELETE` with no `WHERE`.
  • Positional `INSERT` without a column list.
  • `SELECT` then `INSERT` as an upsert — a race condition.
  • Expecting `TRUNCATE` to fire triggers or be rollback-safe.
  • `FLOAT` for currency.
  • `CASCADE` without saying what it will remove.

Practise these on the SQL sheet.

Frequently asked

What is the difference between DELETE, TRUNCATE and DROP?

DELETE is DML: it removes selected rows, accepts a WHERE clause, fires triggers, is logged per row and can be rolled back. TRUNCATE is DDL: it empties the whole table by deallocating pages, so it is much faster but takes no WHERE, fires no triggers and is usually not transactional. DROP removes the table definition itself along with its data.

How do you write an upsert in SQL?

PostgreSQL and SQLite use INSERT … ON CONFLICT (key) DO UPDATE, MySQL uses INSERT … ON DUPLICATE KEY UPDATE, and standard SQL, SQL Server and Oracle use MERGE. All of them need a UNIQUE or PRIMARY KEY constraint to detect the conflict. Checking with a SELECT and then inserting is not equivalent — two concurrent sessions can both pass the check.

Does a CHECK constraint reject NULL values?

No. A CHECK constraint fails only when it evaluates to FALSE, and a comparison against NULL evaluates to UNKNOWN — so CHECK (salary > 0) accepts a NULL salary. Add NOT NULL separately if a value is required. Similarly, a UNIQUE constraint permits multiple NULLs in most engines, SQL Server being the exception.

Related