Changing Data
Adding, changing and removing rows with INSERT, UPDATE and DELETE — and why the WHERE clause decides whether you change one row or all of them.
Data manipulation language
SQL statements fall into groups. The statements that read and change
the rows in tables form the
data manipulation language
(DML). The statements that create and change the tables themselves
(CREATE, ALTER, DROP) are the
data definition language, covered in
Designing Tables.
| Statement | Does | Typical use |
|---|---|---|
SELECT |
Reads rows | Every query on the previous pages |
INSERT |
Adds new rows | A new customer signs up, an order is placed |
UPDATE |
Changes values in existing rows | A price change, an order is shipped |
DELETE |
Removes rows | An order line is removed from the basket |
MERGE / upsert |
Inserts or updates, depending on whether the row exists | Synchronizing data from another system |
Each example on this page starts with a fresh copy of the
sample shop, so changes do
not carry over from one example to the next. Statements that change
data print nothing, so each example ends with a
SELECT that shows the effect.
INSERT: adding rows
The INSERT statement adds rows to a table. You name the table, list the columns you are providing, and give the values in the same order:
INSERT INTO customers (first_name, last_name, email, city, country, signup_date)
VALUES ('Jan', 'Smit', 'jan.smit@example.com', 'Leiden', 'NL', '2026-10-01');
SELECT customer_id, first_name, last_name, city
FROM customers
WHERE email = 'jan.smit@example.com';
customer_id first_name last_name city ----------- ---------- --------- ------ 9 Jan Smit Leiden
The statement does not mention customer_id. Because it
is declared INTEGER PRIMARY KEY, SQLite assigns one
more than the highest existing id, 9. Other systems mark such a
column explicitly:
GENERATED ALWAYS AS IDENTITY (standard SQL, PostgreSQL,
Oracle) or SERIAL (older PostgreSQL),
IDENTITY(1,1) (SQL Server),
AUTO_INCREMENT (MySQL). A column that is left out gets
its DEFAULT value, or NULL if it has none: inserting a
product without stock gives it a stock of 0, the
default declared in shop.sql.
Several rows at once
One INSERT can add several rows, separated by commas.
This is faster than separate statements and is supported by all
major databases (Oracle only since version 23ai; SQL Server allows
up to 1,000 rows in one INSERT … VALUES statement):
INSERT INTO products (name, category, price)
VALUES ('Mouse pad', 'Accessories', 8),
('DisplayPort cable 2 m', 'Cables', 14),
('Docking station', 'Accessories', 159);
SELECT product_id, name, price, stock
FROM products
WHERE product_id > 10
ORDER BY product_id;
product_id name price stock ---------- --------------------- ----- ----- 11 Mouse pad 8 0 12 DisplayPort cable 2 m 14 0 13 Docking station 159 0
All three products got a stock of 0 from the column's
DEFAULT.
Always list the columns
shop.sql itself uses the short form
INSERT INTO customers VALUES (...), without a column
list. That is fine for a one-off setup script, but in application
code it is fragile: the values are matched to the columns by
position, so the statement breaks (or, worse, puts values in the
wrong columns) as soon as someone adds or reorders a column. With an
explicit column list the statement keeps working, and the reader can
see which value goes where.
INSERT … SELECT
Instead of VALUES, an INSERT can take the
rows from a query. This copies data between tables in one statement,
without the data ever leaving the database. A mailing list of all
Dutch customers:
CREATE TABLE newsletter (
email TEXT PRIMARY KEY,
first_name TEXT NOT NULL
);
INSERT INTO newsletter (email, first_name)
SELECT email, first_name
FROM customers
WHERE country = 'NL';
SELECT email, first_name FROM newsletter ORDER BY email;
email first_name ------------------------- ---------- kwame.mensah@example.com Kwame mei.chen@example.com Mei priya.patel@example.com Priya sanne.devries@example.com Sanne tom.jansen@example.com Tom
When a change breaks a rule
The tables of the shop declare rules, called constraints: email addresses must be unique, prices must be positive, an order must belong to an existing customer (see Designing Tables). The database checks them on every change and refuses a change that would break one. The error message names the rule. A second customer with Mei's email address:
INSERT INTO customers (first_name, last_name, email, country, signup_date)
VALUES ('Mei', 'Chen', 'mei.chen@example.com', 'NL', '2026-10-01');
Error: UNIQUE constraint failed: customers.email
A product with a price of zero:
INSERT INTO products (name, category, price)
VALUES ('Sticker pack', 'Accessories', 0);
Error: CHECK constraint failed: price > 0
An order for a customer that does not exist (there is no customer 99):
INSERT INTO orders (order_id, customer_id, order_date, status)
VALUES (111, 99, '2026-10-01', 'pending');
Error: FOREIGN KEY constraint failed
And a required column that was left out (signup_date is
NOT NULL and has no default):
INSERT INTO customers (first_name, last_name, email, country)
VALUES ('Jan', 'Smit', 'jan.smit@example.com', 'NL');
Error: NOT NULL constraint failed: customers.signup_date
In each case the statement is undone as a whole and the table is
unchanged. That includes multi-row statements: if one row of a
500-row INSERT breaks a rule, none of the 500 rows are
inserted. These errors are not a nuisance; they are the database
protecting the data from a bug in the application. SQLite only
enforces foreign keys after
PRAGMA foreign_keys = ON (the sample database switches
it on); PostgreSQL, MySQL (InnoDB) and SQL Server enforce them by
default.
UPDATE: changing rows
The UPDATE statement changes
values in existing rows. SET says which columns get
which new values; WHERE says which rows. The keyboard
gets cheaper:
UPDATE products
SET price = 79
WHERE product_id = 4;
SELECT product_id, name, price FROM products WHERE product_id = 4;
product_id name price ---------- ------------------- ----- 4 Mechanical keyboard 79
Filter on the primary key when you mean one specific row; a name can be misspelled or shared by two rows. The new value can be computed from the old one. When two keyboards are sold, decrease the stock rather than overwriting it with a number calculated elsewhere — the database does the arithmetic on the current value, which is safer when several sales happen at the same moment:
UPDATE products
SET stock = stock - 2
WHERE product_id = 4;
SELECT product_id, name, stock FROM products WHERE product_id = 4;
product_id name stock ---------- ------------------- ----- 4 Mechanical keyboard 10
Several columns change in one statement by separating them with
commas (SET price = 79, stock = 20), and the
WHERE can match many rows on purpose:
UPDATE orders SET status = 'delivered' WHERE status =
'shipped'. Constraints apply to updates too. Selling more monitors than are
in stock (7):
UPDATE products
SET stock = stock - 10
WHERE product_id = 5;
Error: CHECK constraint failed: stock >= 0
The danger of a missing WHERE
WHERE is optional in UPDATE and
DELETE. Without it, the statement applies to
every row in the table, and the database does not
ask whether you are sure:
UPDATE products
SET price = 10; -- meant: WHERE product_id = 2
SELECT product_id, name, price FROM products ORDER BY product_id LIMIT 4;
product_id name price ---------- ------------------- ----- 1 USB-C cable 2 m 10 2 HDMI cable 3 m 10 3 Wireless mouse 10 4 Mechanical keyboard 10
A routine for safe changes
-
Preview with SELECT. Write the
WHEREfirst as aSELECTwith the same condition, and check the rows and their count. -
Start a transaction.
BEGINbefore the change; nothing is permanent untilCOMMIT. -
Check the number of changed rows. Every database
tool reports it ("10 rows affected"); in SQLite,
changes()returns it. If it is not the number the preview showed, roll back. -
Commit or roll back.
COMMITmakes the change permanent;ROLLBACKundoes everything sinceBEGIN.
BEGIN;
UPDATE products
SET price = 10; -- oops: no WHERE
SELECT changes() AS rows_changed; -- expected 1, got 10
ROLLBACK;
SELECT product_id, name, price FROM products ORDER BY product_id LIMIT 4;
rows_changed ------------ 10 product_id name price ---------- ------------------- ----- 1 USB-C cable 2 m 9 2 HDMI cable 3 m 12 3 Wireless mouse 25 4 Mechanical keyboard 89
The rollback restored every price. Transactions, and what they
guarantee, are explained in
Transactions and ACID. Some
tools add a safety net of their own: the MySQL command-line client
has a --safe-updates option that refuses
UPDATE and DELETE without a key in the
WHERE, and many graphical tools warn before such
statements. And give applications a database account that can only
do what they need (least privilege): a reporting tool that can only SELECT cannot delete
anything by accident.
DELETE: removing rows
The DELETE statement removes
whole rows; it has no column list, because it never removes part of
a row (to clear one value, UPDATE it to NULL). Like
UPDATE, it applies to the rows that match
WHERE, and to all rows without one. Removing the HDMI
cable from order 109:
DELETE FROM order_items
WHERE order_id = 109 AND product_id = 2;
SELECT order_id, product_id, quantity FROM order_items WHERE order_id = 109;
order_id product_id quantity -------- ---------- -------- 109 3 1
order_items has a two-column primary key
(order_id, product_id), so both columns
are needed to identify one line.
WHERE order_id = 109 alone would remove the whole
order's lines.
DELETE and foreign keys
A row that other rows refer to cannot simply disappear: the orders would point to a customer that no longer exists. Sanne has two orders, so deleting her fails:
DELETE FROM customers WHERE customer_id = 1;
Error: FOREIGN KEY constraint failed
This is referential integrity at
work. To really remove her, you would delete the rows that depend on
her first (order lines, then orders, then the customer), or declare
the foreign key with ON DELETE CASCADE so the database
deletes the dependent rows itself. For business records such as
orders, neither is usually what you want, which is one reason for
soft deletes. A customer without orders,
such as Ana, can be deleted without any problem:
DELETE FROM customers WHERE customer_id = 8;
SELECT COUNT(*) AS customers FROM customers;
customers --------- 7
Upsert: insert or update
A common task is "add this row, or update it if it is already
there", for example when a delivery arrives and its products may or
may not be in the catalog yet. Checking first with a
SELECT and then choosing INSERT or
UPDATE takes two round trips and can go wrong when two
sessions do it at the same moment. An
upsert (update + insert) does it in
one atomic statement. SQLite (since 3.24) and PostgreSQL (since 9.5)
use ON CONFLICT:
INSERT INTO products (product_id, name, category, price, stock)
VALUES (3, 'Wireless mouse', 'Peripherals', 25, 20),
(11, 'Mouse pad', 'Accessories', 8, 60)
ON CONFLICT (product_id) DO UPDATE
SET stock = stock + excluded.stock;
SELECT product_id, name, stock FROM products WHERE product_id IN (3, 11) ORDER BY product_id;
product_id name stock ---------- -------------- ----- 3 Wireless mouse 65 11 Mouse pad 60
Product 3 already existed, so its stock went from 45 to 65; product
11 did not, so it was inserted. excluded refers to the
row that would have been inserted. The conflict target
(product_id) must be a primary key or have a unique
constraint. ON CONFLICT DO NOTHING simply skips rows
that already exist. The other big systems write it differently (not
run here):
-- MySQL / MariaDB
INSERT INTO products (product_id, name, category, price, stock)
VALUES (3, 'Wireless mouse', 'Peripherals', 25, 20)
ON DUPLICATE KEY UPDATE stock = stock + VALUES(stock);
-- MySQL 8.0.19+ prefers a row alias: ... VALUES (...) AS new
-- ON DUPLICATE KEY UPDATE stock = stock + new.stock;
-- SQL Server (PostgreSQL 15+ accepts the same; Oracle has MERGE with slightly different syntax)
MERGE INTO products AS target
USING (VALUES (3, 'Wireless mouse', 'Peripherals', 25, 20))
AS source (product_id, name, category, price, stock)
ON target.product_id = source.product_id
WHEN MATCHED THEN
UPDATE SET stock = target.stock + source.stock
WHEN NOT MATCHED THEN
INSERT (product_id, name, category, price, stock)
VALUES (source.product_id, source.name, source.category, source.price, source.stock);
MERGE is the SQL standard's statement for this and can
also delete; it is powerful but verbose. In SQL Server, two sessions
running the same MERGE at once can still collide unless
the statement holds the right locks (a HOLDLOCK hint is
the usual advice).
RETURNING: seeing what changed
After an INSERT the application often needs the
generated key, and after an UPDATE the new value. A
RETURNING clause makes a DML statement return the
affected rows, like a SELECT, without a second query.
SQLite supports it since version 3.35, PostgreSQL for a long time:
INSERT INTO customers (first_name, last_name, email, country, signup_date)
VALUES ('Jan', 'Smit', 'jan.smit@example.com', 'NL', '2026-10-01')
RETURNING customer_id, first_name;
UPDATE products
SET stock = stock - 2
WHERE product_id = 4
RETURNING product_id, name, stock;
customer_id first_name ----------- ---------- 9 Jan product_id name stock ---------- ------------------- ----- 4 Mechanical keyboard 10
SQL Server has the same idea as an OUTPUT clause (INSERT … OUTPUT inserted.customer_id VALUES …). MySQL has no RETURNING; applications read the
generated key through the driver (LAST_INSERT_ID()).
MariaDB supports RETURNING for INSERT and
DELETE. From code, drivers also expose the last
inserted id and the number of affected rows; see
Working with Databases from Code.
Removing all rows, and not removing at all
TRUNCATE versus DELETE
To empty a whole table, PostgreSQL, MySQL, SQL Server and Oracle
offer TRUNCATE TABLE name. It removes all rows at once
instead of one by one, which makes it much faster on large tables
and leaves less in the transaction log. It is also blunter:
DELETE FROM t |
TRUNCATE TABLE t |
|
|---|---|---|
| Which rows | Those matching WHERE (all without one) |
Always all |
| Speed on big tables | Slower: row by row, every row logged | Fast: releases the data pages |
Row-level DELETE triggers |
Fire | Do not fire |
| Auto-increment counter | Continues |
Reset in MySQL and SQL Server; in PostgreSQL only with
RESTART IDENTITY
|
| Referenced by a foreign key | Checked per row |
Refused (PostgreSQL and Oracle offer CASCADE)
|
| Rollback | Possible inside a transaction | Possible in PostgreSQL and SQL Server; MySQL and Oracle commit it immediately |
Oracle, SQL Server and MySQL classify TRUNCATE as data
definition language rather than DML.
SQLite has no TRUNCATE; a DELETE FROM t without WHERE is
internally optimized to empty the table quickly, much like a
truncate.
Soft deletes
Many applications never physically delete business records. Instead
they mark them as deleted with a column such as
deleted_at, a soft delete, and filter
those rows out in normal queries:
ALTER TABLE customers ADD COLUMN deleted_at TEXT;
UPDATE customers
SET deleted_at = '2026-10-01'
WHERE customer_id = 1;
SELECT COUNT(*) AS active_customers
FROM customers
WHERE deleted_at IS NULL;
active_customers ---------------- 7
Sanne's orders stay valid (the foreign key still points to an
existing row), the history is kept for reporting and auditing, and
an accidental "delete" can be undone. The costs: every query must
remember WHERE deleted_at IS NULL (a
view of active
customers helps), unique constraints still see the hidden rows, and
the table keeps growing. And a soft delete is not erasure: when a
customer exercises the right to erasure under the General Data
Protection Regulation (GDPR), personal data must really be removed
or anonymized (see
retention and deletion).