PostgreSQL INSERT, UPDATE, DELETE and upsert
These commands change data. INSERT adds rows (one, many, or the result of a query). UPDATE changes columns in rows matching a WHERE. DELETE removes rows matching a WHERE. An UPDATE or DELETE without WHERE affects every row; run the matching SELECT first if you’re unsure.
PostgreSQL extras that you’ll use constantly:
RETURNINGgives back the inserted/updated/deleted rows, such as a new id, without a second query.INSERT ... ON CONFLICT DO NOTHING / DO UPDATEis an “upsert”: insert, or update if the key already exists.EXCLUDEDrefers to the row you tried to insert.UPDATE ... FROMandDELETE ... USINGlet you use other tables in the condition.TRUNCATEempties a table instantly.
Tip: wrap experiments in BEGIN; ... ROLLBACK; to see the effect and then undo it.
Syntax
INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y') RETURNING id; UPDATE t SET a = a + 1 WHERE condition; DELETE FROM t WHERE condition; INSERT ... ON CONFLICT (key) DO UPDATE SET col = EXCLUDED.col;
Examples
Edit the code and run it. Output is printed the way psql prints it.
17.1INSERT with RETURNING
INSERT INTO customers (name, email, city, country, tier)
VALUES ('Tara Menon', 'tara@example.in', 'Kochi', 'India', 'pro'),
('Ben Carter', 'ben@example.com', 'Austin', 'USA', DEFAULT)
RETURNING id, name, tier, signup_date;Two rows in one statement. DEFAULT uses the column default (free). RETURNING shows the generated ids and defaults. Run it twice and the UNIQUE email rule stops you.
17.2INSERT ... SELECT
DROP TABLE IF EXISTS vip_customers;
CREATE TABLE vip_customers (customer_id integer PRIMARY KEY, name text, added_on date DEFAULT current_date);
INSERT INTO vip_customers (customer_id, name)
SELECT id, name FROM customers WHERE tier = 'enterprise';
SELECT * FROM vip_customers;Instead of VALUES, the rows come from a query. The column count and types must match.
17.3UPDATE safely
BEGIN;
UPDATE products
SET price = round(price * 1.10, 2),
stock = stock + 10
WHERE category = 'Office'
RETURNING id, name, price, stock;
ROLLBACK; -- undo, so the shop data stays the sameTwo columns change in one pass; the right-hand side sees the old values. RETURNING shows the new values, and ROLLBACK throws the change away.
17.4UPDATE ... FROM another table
BEGIN;
UPDATE customers c
SET tier = 'pro'
FROM (
SELECT o.customer_id
FROM orders o JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.customer_id
HAVING sum(oi.quantity * oi.unit_price) > 150
) big
WHERE c.id = big.customer_id AND c.tier = 'free'
RETURNING c.name, c.tier;
ROLLBACK;Upgrades free customers who spent more than 150. The FROM clause adds another table (here a derived table) to match against.
17.5DELETE and DELETE ... USING
BEGIN;
DELETE FROM orders WHERE status = 'cancelled' RETURNING id, customer_id;
SELECT count(*) AS items_left_for_cancelled FROM order_items WHERE order_id IN (6, 14);
ROLLBACK;
BEGIN;
DELETE FROM order_items oi
USING products p
WHERE p.id = oi.product_id AND p.category = 'Audio'
RETURNING oi.order_id, p.name;
ROLLBACK;Deleting the cancelled orders also removed their items, because order_items.order_id is defined with ON DELETE CASCADE. The second statement deletes items by joining to products with USING.
17.6UPSERT with ON CONFLICT
DROP TABLE IF EXISTS page_views;
CREATE TABLE page_views (page text PRIMARY KEY, views integer NOT NULL);
INSERT INTO page_views VALUES ('/home', 1)
ON CONFLICT (page) DO UPDATE SET views = page_views.views + EXCLUDED.views;
INSERT INTO page_views VALUES ('/home', 1), ('/pricing', 1)
ON CONFLICT (page) DO UPDATE SET views = page_views.views + EXCLUDED.views;
INSERT INTO page_views VALUES ('/pricing', 99)
ON CONFLICT DO NOTHING;
SELECT * FROM page_views ORDER BY page;The first insert creates /home. The second finds /home already there and adds to it instead, while inserting /pricing. DO NOTHING silently skips duplicates. This pattern replaces “check then insert or update” and is safe under concurrency.
Exercises
Write a query and check it. Any query that returns the right rows passes.
Exercise 17.1
Gaming Mouse in category Electronics priced 49.99 with stock 40.Hint
INSERT INTO products (name, category, price, stock) VALUES (...)
Solution
INSERT INTO products (name, category, price, stock) VALUES ('Gaming Mouse', 'Electronics', 49.99, 40);Exercise 17.2
Office category by 10%, rounded to 2 decimals.Hint
UPDATE products SET price = round(price * 1.1, 2) WHERE ...
Solution
UPDATE products SET price = round(price * 1.1, 2) WHERE category = 'Office';Exercise 17.3
pending order as cancelled if it has no sales rep (employee_id is NULL).Hint
Two conditions in WHERE.
Solution
UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND employee_id IS NULL;