DML - Insert, Update, Delete
Definition
DML (Data Manipulation Language) statements change the data stored inside tables, as opposed to DDL, which changes the table structure itself (see 03-DDL-Tables).
INSERT
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
-- Multiple rows in one statement (far faster than separate INSERTs)
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com');
-- Insert from a query (copy/transform data from another table)
INSERT INTO active_users (name, email)
SELECT name, email FROM users WHERE is_active = true;RETURNING - Get Back What Was Inserted
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')
RETURNING id, created_at;
RETURNINGavoids a separateSELECTExtremely useful in application code: insert a row and immediately get its generated
idback in a single round trip, instead of inserting then querying separately.
UPDATE
UPDATE users SET age = 26 WHERE id = 1;
UPDATE users SET age = 26, status = 'active' WHERE id = 1; -- multiple columns
UPDATE products SET price = price * 1.10; -- no WHERE = updates EVERY row!
UPDATE users SET age = age + 1 WHERE age < 18; -- expression referencing current valueUPDATE ... FROM (Using Another Table)
UPDATE orders o
SET status = 'shipped'
FROM shipments s
WHERE o.id = s.order_id AND s.shipped_date = CURRENT_DATE;Always double-check your
WHEREclause before runningUPDATEAn
UPDATEwithout aWHEREclause modifies every single row in the table. Inpsql, run aSELECTwith the sameWHEREclause first to confirm exactly which rows will be affected.
DELETE
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE created_at < now() - INTERVAL '1 year';
DELETE FROM users; -- no WHERE = deletes EVERY row! (see TRUNCATE for a faster equivalent)DELETE ... USING (Using Another Table)
DELETE FROM orders o
USING users u
WHERE o.user_id = u.id AND u.is_banned = true;DELETE ... RETURNING
DELETE FROM sessions WHERE expires_at < now() RETURNING id;UPSERT - INSERT ... ON CONFLICT
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) -- must reference a UNIQUE or PRIMARY KEY constraint
DO UPDATE SET name = EXCLUDED.name; -- EXCLUDED refers to the row that FAILED to insert
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email) DO NOTHING; -- silently skip if it already existsgraph TD A["INSERT attempted"] --> B{"Conflict on unique constraint?"} B -->|No| C["Row inserted normally"] B -->|Yes| D{"ON CONFLICT clause"} D -->|"DO NOTHING"| E["Insert silently skipped"] D -->|"DO UPDATE"| F["Existing row updated instead"]
ON CONFLICTis Postgres's answer to "insert or update"Common pattern: syncing external data where a row might already exist.
EXCLUDEDis a special pseudo-table referring to the values that were proposed for insertion but conflicted.
MERGE (Postgres 15+)
MERGE INTO accounts a
USING transactions t ON a.id = t.account_id
WHEN MATCHED THEN
UPDATE SET balance = a.balance + t.amount
WHEN NOT MATCHED THEN
INSERT (id, balance) VALUES (t.account_id, t.amount);
MERGEvsON CONFLICT
MERGEis the SQL-standard statement for combined insert/update/delete logic based on a join condition, more flexible thanON CONFLICT(which is keyed specifically to a unique constraint violation) but requires Postgres 15 or later.
COPY - Bulk Load/Export (Fastest Method for Large Data)
COPY users (name, email) FROM '/path/to/users.csv' WITH (FORMAT csv, HEADER true);
COPY users TO '/path/to/export.csv' WITH (FORMAT csv, HEADER true);# psql's client-side variant, for loading from your local machine over a remote connection
\copy users FROM 'users.csv' WITH (FORMAT csv, HEADER true)
COPYis dramatically faster than many individualINSERTstatementsFor loading thousands+ of rows,
COPY(or\copyfrompsql) bypasses per-row statement overhead and is the standard tool for bulk data import/export.