SQL Cheatsheet
Modifying Data
Use this SQL reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
INSERT
-- Single row, all columns (order matches table definition) INSERT INTO users VALUES (1, 'Alice', 'alice@example.com', NOW()); -- Single row, named columns (recommended — order-independent) INSERT INTO users (name, email, created_at) VALUES ('Alice', 'alice@example.com', NOW()); -- Multiple rows in one statement INSERT INTO products (name, price, category) VALUES ('Widget', 9.99, 'Gadgets'), ('Gizmo', 14.99, 'Gadgets'), ('Doohickey', 4.99, 'Parts'); -- Insert from SELECT INSERT INTO archive_orders (id, user_id, total, archived_at) SELECT id, user_id, total, NOW() FROM orders WHERE created_at < '2023-01-01'; -- Insert with default values INSERT INTO logs DEFAULT VALUES; INSERT INTO logs (message) VALUES ('event'); -- other columns use defaults -- INSERT RETURNING (PostgreSQL) — get values from the inserted row INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com') RETURNING id, created_at; -- OUTPUT (SQL Server equivalent) INSERT INTO users (name, email) OUTPUT INSERTED.id, INSERTED.created_at VALUES ('Bob', 'bob@example.com');
INSERT OR IGNORE / ON CONFLICT (Upsert)
-- PostgreSQL: ON CONFLICT DO NOTHING (ignore duplicate) INSERT INTO tags (name) VALUES ('sql'), ('database') ON CONFLICT (name) DO NOTHING; -- PostgreSQL: ON CONFLICT DO UPDATE (upsert) INSERT INTO users (id, email, updated_at) VALUES (42, 'newemail@example.com', NOW()) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, updated_at = EXCLUDED.updated_at; -- ON CONFLICT with WHERE (partial upsert) INSERT INTO products (sku, price) VALUES ('ABC', 19.99) ON CONFLICT (sku) DO UPDATE SET price = EXCLUDED.price WHERE products.price <> EXCLUDED.price; -- only update if price changed -- MySQL: INSERT ... ON DUPLICATE KEY UPDATE INSERT INTO users (id, email, updated_at) VALUES (42, 'newemail@example.com', NOW()) ON DUPLICATE KEY UPDATE email = VALUES(email), updated_at = VALUES(updated_at); -- MySQL: REPLACE INTO (delete + insert on conflict — careful with FKs) REPLACE INTO users (id, name, email) VALUES (42, 'Bob', 'bob@example.com'); -- SQLite: INSERT OR REPLACE / INSERT OR IGNORE INSERT OR IGNORE INTO tags (name) VALUES ('sql'); INSERT OR REPLACE INTO users (id, name) VALUES (42, 'Alice'); -- SQL Server: MERGE (upsert) MERGE users AS target USING (VALUES (42, 'bob@example.com')) AS source(id, email) ON target.id = source.id WHEN MATCHED THEN UPDATE SET target.email = source.email WHEN NOT MATCHED THEN INSERT (id, email) VALUES (source.id, source.email);
UPDATE
-- Update all rows (dangerous — almost always add a WHERE) UPDATE products SET stock = 0; -- Update specific rows UPDATE users SET status = 'inactive' WHERE last_login < NOW() - INTERVAL '1 year'; -- Update multiple columns UPDATE employees SET salary = salary * 1.10, title = 'Senior Engineer', updated_at = NOW() WHERE id = 123; -- Update with expression UPDATE orders SET total = subtotal + tax WHERE total IS NULL; -- Update using another table (correlated subquery) UPDATE employees e SET salary = ( SELECT AVG(salary) FROM employees WHERE department = e.department ) WHERE title = 'Intern'; -- Update with JOIN (PostgreSQL FROM clause) UPDATE orders o SET status = 'cancelled' FROM users u WHERE o.user_id = u.id AND u.status = 'banned'; -- Update with JOIN (MySQL) UPDATE orders o JOIN users u ON u.id = o.user_id SET o.status = 'cancelled' WHERE u.status = 'banned'; -- RETURNING (PostgreSQL) — get affected rows back UPDATE users SET status = 'active' WHERE email_verified = TRUE RETURNING id, email; -- Limit updates (MySQL) UPDATE products SET stock = 0 WHERE stock < 0 LIMIT 100;
DELETE
-- Delete all rows (use TRUNCATE for full table wipe) DELETE FROM session_tokens; -- Delete specific rows DELETE FROM users WHERE status = 'deleted' AND updated_at < NOW() - INTERVAL '30 days'; -- Delete with subquery DELETE FROM order_items WHERE order_id IN ( SELECT id FROM orders WHERE status = 'cancelled' ); -- Delete with JOIN (MySQL) DELETE oi FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.status = 'cancelled'; -- Delete with USING (PostgreSQL) DELETE FROM order_items oi USING orders o WHERE oi.order_id = o.id AND o.status = 'cancelled'; -- RETURNING (PostgreSQL) — return deleted rows DELETE FROM sessions WHERE expires_at < NOW() RETURNING id, user_id; -- Delete with CTE (PostgreSQL) WITH expired AS ( SELECT id FROM sessions WHERE expires_at < NOW() ) DELETE FROM sessions WHERE id IN (SELECT id FROM expired); -- Limit deletes (MySQL) DELETE FROM logs WHERE level = 'debug' ORDER BY created_at LIMIT 1000;
TRUNCATE
Removes all rows from a table — faster than DELETE (no row-by-row logging), but non-recoverable in some configurations and cannot be filtered.
-- Truncate a table TRUNCATE TABLE session_tokens; -- Truncate and reset identity/serial counter TRUNCATE TABLE orders RESTART IDENTITY; -- Truncate multiple tables TRUNCATE TABLE cart_items, carts; -- Cascade truncate (also truncate referencing tables) TRUNCATE TABLE users CASCADE; -- TRUNCATE vs DELETE: -- TRUNCATE: DDL in some DBs (not transactional in MySQL), resets sequences, -- ignores ON DELETE triggers, cannot filter rows. -- DELETE: DML, fully transactional, fires triggers, can use WHERE, RETURNING.
MERGE (Upsert — SQL Standard)
Combine INSERT, UPDATE, and DELETE in one statement based on a match condition.
-- SQL Standard MERGE (PostgreSQL 15+, SQL Server, Oracle) MERGE INTO target_table AS target USING source_table AS source ON target.id = source.id WHEN MATCHED AND source.deleted = TRUE THEN DELETE WHEN MATCHED THEN UPDATE SET target.name = source.name, target.updated_at = NOW() WHEN NOT MATCHED THEN INSERT (id, name, created_at) VALUES (source.id, source.name, NOW()); -- MERGE with a values list as source MERGE INTO products AS t USING (VALUES (101, 'Widget', 9.99)) AS s(id, name, price) ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.price = s.price WHEN NOT MATCHED THEN INSERT (id, name, price) VALUES (s.id, s.name, s.price);
RETURNING / OUTPUT
Get values from rows affected by DML.
-- PostgreSQL RETURNING INSERT INTO orders (user_id, total) VALUES (1, 99.99) RETURNING id, created_at; UPDATE users SET status = 'active' WHERE id = 42 RETURNING *; DELETE FROM sessions WHERE user_id = 42 RETURNING id; -- SQL Server OUTPUT (into a temp table or variable) INSERT INTO orders (user_id, total) OUTPUT INSERTED.id, INSERTED.created_at INTO @new_orders VALUES (1, 99.99); UPDATE users SET status = 'active' OUTPUT DELETED.status AS old_status, INSERTED.status AS new_status WHERE id = 42;
Bulk Operations and Performance
-- COPY (PostgreSQL) — fastest bulk import from file COPY users (id, name, email) FROM '/tmp/users.csv' CSV HEADER; COPY users TO '/tmp/users.csv' CSV HEADER; -- COPY from stdin (psql) COPY products FROM STDIN WITH (FORMAT csv, HEADER true); -- MySQL: LOAD DATA INFILE LOAD DATA INFILE '/tmp/products.csv' INTO TABLE products FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS; -- Batch inserts: combine multiple rows in one INSERT -- Much faster than one INSERT per row -- Disable indexes for bulk loads (MySQL) ALTER TABLE products DISABLE KEYS; -- ... bulk insert ... ALTER TABLE products ENABLE KEYS; -- Deferred constraint checking (PostgreSQL) SET CONSTRAINTS ALL DEFERRED; -- ... insert rows that temporarily violate FK constraints ... COMMIT; -- constraints checked at commit time
Set Operations
Combine result sets from multiple SELECT statements.
-- UNION: combine and deduplicate SELECT id FROM customers UNION SELECT id FROM leads; -- UNION ALL: combine without deduplication (faster) SELECT name, 'customer' AS type FROM customers UNION ALL SELECT name, 'lead' AS type FROM leads; -- INTERSECT: rows that appear in both SELECT user_id FROM premium_users INTERSECT SELECT user_id FROM active_users; -- EXCEPT / MINUS: rows in first but not second SELECT id FROM users EXCEPT SELECT user_id FROM orders; -- users who never ordered -- Rules: -- Same number of columns in all SELECT statements -- Compatible data types per column position -- Column names from first SELECT are used -- ORDER BY applies to the final result SELECT a, b FROM t1 UNION ALL SELECT c, d FROM t2 ORDER BY 1 DESC;