SQL Cheatsheet
Every SQL command and clause you need: from SELECT to window functions, joins, aggregation, DDL, and transactions, with syntax and real use cases.113 commands · 8 sections
SQL is the language of relational databases. This cheatsheet covers the commands you write daily: querying with SELECT, filtering and sorting, every JOIN type, aggregation and GROUP BY, window functions, modifying data with INSERT/UPDATE/DELETE, and schema management with DDL.
Each entry shows real syntax followed by the use case. Most examples work in PostgreSQL, MySQL, SQLite, and SQL Server with minor dialect differences.
Querying Data13
SELECT * FROM users;SELECT id, name, email FROM users;SELECT DISTINCT country FROM users;SELECT name AS full_name FROM users;SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;SELECT 10 + 5 AS result;SELECT COALESCE(email, phone) FROM users;SELECT NULLIF(a, b);SELECT COUNT(*) FROM users;SELECT COUNT(DISTINCT country) FROM users;SELECT * FROM users LIMIT 10;SELECT * FROM users LIMIT 10 OFFSET 20;SELECT * FROM users ORDER BY created_at DESC LIMIT 1;Filtering & Sorting12
SELECT * FROM users WHERE status = 'active';SELECT * FROM orders WHERE total > 100 AND status = 'paid';SELECT * FROM users WHERE plan = 'free' OR plan = 'pro';SELECT * FROM users WHERE plan IN ('free', 'pro', 'team');SELECT * FROM users WHERE email LIKE '%@gmail.com';SELECT * FROM users WHERE name ILIKE 'jane%';SELECT * FROM users WHERE email IS NULL;SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';SELECT * FROM orders WHERE total NOT BETWEEN 10 AND 100;SELECT * FROM users ORDER BY created_at DESC;SELECT * FROM users ORDER BY country ASC, name DESC;SELECT * FROM orders ORDER BY total DESC LIMIT 5;Joins12
SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id;SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id;SELECT * FROM customers LEFT JOIN orders ON orders.customer_id = customers.id;SELECT * FROM customers LEFT JOIN orders ON orders.customer_id = customers.id WHERE orders.id IS NULL;SELECT * FROM orders RIGHT JOIN customers ON orders.customer_id = customers.id;SELECT * FROM orders FULL OUTER JOIN customers ON orders.customer_id = customers.id;SELECT * FROM employees e JOIN employees m ON e.manager_id = m.id;SELECT * FROM a CROSS JOIN b;SELECT * FROM orders JOIN customers USING (customer_id);SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id AND customers.country = 'US';SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id JOIN products p ON o.product_id = p.id;SELECT * FROM orders JOIN LATERAL (SELECT MAX(price) AS max_price FROM order_items WHERE order_id = orders.id) x ON true;Aggregation12
SELECT country, COUNT(*) FROM users GROUP BY country;SELECT country, COUNT(*) FROM users GROUP BY country HAVING COUNT(*) > 100;SELECT department, AVG(salary) FROM employees GROUP BY department;SELECT department, SUM(salary) FROM employees GROUP BY department;SELECT department, MIN(salary), MAX(salary) FROM employees GROUP BY department;SELECT COUNT(*), SUM(total) FROM orders;SELECT year, month, SUM(revenue) FROM sales GROUP BY year, month ORDER BY year, month;SELECT GROUP_CONCAT(name) FROM users WHERE plan = 'pro';SELECT STRING_AGG(name, ', ' ORDER BY name) FROM users;SELECT ARRAY_AGG(name) FROM users;SELECT JSON_AGG(name) FROM users;SELECT department, COUNT(*) FILTER (WHERE salary > 100000) FROM employees GROUP BY department;Window Functions10
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) FROM employees;SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) FROM employees;SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) FROM employees;SELECT name, department, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) FROM employees;SELECT order_date, total, SUM(total) OVER (ORDER BY order_date) AS running_total FROM orders;SELECT month, revenue, AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma3 FROM monthly;SELECT name, salary, salary - LAG(salary) OVER (ORDER BY id) AS diff FROM employees;SELECT month, revenue, revenue - LEAD(revenue) OVER (ORDER BY month) AS next_diff FROM monthly;SELECT month, revenue, FIRST_VALUE(revenue) OVER (ORDER BY month) FROM monthly;SELECT order_id, customer_id, total, total / SUM(total) OVER (PARTITION BY customer_id) AS share FROM orders;Modifying Data16
INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com');INSERT INTO users (name, email) VALUES ('A', 'a@x.com'), ('B', 'b@x.com'), ('C', 'c@x.com');INSERT INTO archive SELECT * FROM users WHERE created_at < '2020-01-01';INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON CONFLICT (email) DO NOTHING;INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON DUPLICATE KEY UPDATE name = VALUES(name);UPDATE users SET status = 'active' WHERE id = 42;UPDATE users SET status = 'inactive' WHERE last_login_at < NOW() - INTERVAL '1 year';UPDATE orders SET total = total * 1.1 WHERE status = 'pending';UPDATE users SET email = NULL WHERE email = '';DELETE FROM users WHERE id = 42;DELETE FROM users WHERE last_login_at < NOW() - INTERVAL '5 years';DELETE FROM users;TRUNCATE TABLE users;UPDATE users SET plan = 'pro' FROM (SELECT id FROM users WHERE email LIKE '%@company.com') AS corp WHERE users.id = corp.id;MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);Schema & Indexes19
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at TIMESTAMP DEFAULT NOW());ALTER TABLE users ADD COLUMN phone TEXT;ALTER TABLE users DROP COLUMN phone;ALTER TABLE users RENAME COLUMN phone TO mobile;ALTER TABLE users ALTER COLUMN name SET NOT NULL;ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);ALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE;CREATE INDEX idx_users_email ON users (email);CREATE UNIQUE INDEX idx_users_email ON users (email);CREATE INDEX idx_orders_customer ON orders (customer_id, created_at);CREATE INDEX idx_orders_lower_email ON users (LOWER(email));CREATE INDEX idx_users_name ON users USING GIN (to_tsvector('english', name));DROP INDEX idx_users_email;DROP TABLE users;EXPLAIN SELECT * FROM users WHERE email = 'x@y.com';EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 5;CREATE VIEW active_users AS SELECT * FROM users WHERE status = 'active';CREATE MATERIALIZED VIEW monthly_revenue AS SELECT date_trunc('month', created_at) AS month, SUM(total) FROM orders GROUP BY 1;REFRESH MATERIALIZED VIEW monthly_revenue;Transactions & Advanced19
BEGIN; ... COMMIT;BEGIN; ... ROLLBACK;SELECT ... FOR UPDATE;SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;SELECT NOW();SELECT CURRENT_DATE;SELECT DATE_TRUNC('month', NOW());SELECT * FROM orders WHERE created_at >= NOW() - INTERVAL '7 days';SELECT DATEDIFF('day', created_at, NOW()) FROM users;WITH recent AS (SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days') SELECT customer_id, SUM(total) FROM recent GROUP BY customer_id;WITH RECURSIVE nums AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n < 10) SELECT n FROM nums;SELECT * FROM orders WHERE total = (SELECT MAX(total) FROM orders);SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.customer_id = users.id);SELECT * FROM users WHERE id IN (SELECT customer_id FROM orders WHERE total > 500);SELECT CASE WHEN total > 1000 THEN 'high' WHEN total > 100 THEN 'medium' ELSE 'low' END AS tier, COUNT(*) FROM orders GROUP BY 1;SELECT * FROM users UNION SELECT * FROM deleted_users;SELECT * FROM users UNION ALL SELECT * FROM deleted_users;SELECT (json_data->>'name') FROM users;SELECT * FROM users WHERE json_data @> '{"plan": "pro"}';SQL Cheatsheet
Every SQL command and clause you need: from SELECT to window functions, joins, aggregation, DDL, and transactions, with syntax and real use cases.
SQL is the language of relational databases. This cheatsheet covers the commands you write daily: querying with SELECT, filtering and sorting, every JOIN type, aggregation and GROUP BY, window functions, modifying data with INSERT/UPDATE/DELETE, and schema management with DDL.
Each entry shows real syntax followed by the use case. Most examples work in PostgreSQL, MySQL, SQLite, and SQL Server with minor dialect differences.
Querying Data
SELECT * FROM users;: Return every column and row from a table: the simplest read.SELECT id, name, email FROM users;: Return only specific columns: less data, faster queries.SELECT DISTINCT country FROM users;: Return unique values: which countries have users?SELECT name AS full_name FROM users;: Rename a column in the result: cleaner output.SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;: Combine columns into one (PostgreSQL/MySQL: || or CONCAT).SELECT 10 + 5 AS result;: Run calculations without a table: test expressions.SELECT COALESCE(email, phone) FROM users;: Return the first non-NULL value: fallback columns.SELECT NULLIF(a, b);: Return NULL when two values are equal: avoid division by zero.SELECT COUNT(*) FROM users;: Count all rows: how many users exist?SELECT COUNT(DISTINCT country) FROM users;: Count unique values: how many countries are represented?SELECT * FROM users LIMIT 10;: Return only the first 10 rows: preview a large table.SELECT * FROM users LIMIT 10 OFFSET 20;: Skip 20 rows, return 10: page 3 of results.SELECT * FROM users ORDER BY created_at DESC LIMIT 1;: The latest user: most recent row first.Filtering & Sorting
SELECT * FROM users WHERE status = 'active';: Filter rows with a condition.SELECT * FROM orders WHERE total > 100 AND status = 'paid';: Combine conditions with AND: both must be true.SELECT * FROM users WHERE plan = 'free' OR plan = 'pro';: Either condition true: plan is one of these.SELECT * FROM users WHERE plan IN ('free', 'pro', 'team');: Cleaner than multiple ORs: value in a list.SELECT * FROM users WHERE email LIKE '%@gmail.com';: Pattern match: % is a wildcard. Find Gmail users.SELECT * FROM users WHERE name ILIKE 'jane%';: Case-insensitive pattern match (PostgreSQL): find Jane, jane, JANE.SELECT * FROM users WHERE email IS NULL;: NULL comparison: = NULL never works, always use IS NULL.SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';: Range filter: inclusive on both ends.SELECT * FROM orders WHERE total NOT BETWEEN 10 AND 100;: Exclude a range: orders outside 10-100.SELECT * FROM users ORDER BY created_at DESC;: Sort newest first.SELECT * FROM users ORDER BY country ASC, name DESC;: Sort by multiple columns: country first, then name within country.SELECT * FROM orders ORDER BY total DESC LIMIT 5;: Top 5 orders by value: your best customers' biggest orders.Joins
SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id;: INNER JOIN: only rows matching in both tables.SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id;: Alias tables and pick columns: the everyday join.SELECT * FROM customers LEFT JOIN orders ON orders.customer_id = customers.id;: All customers, even those with zero orders: NULLs fill the missing side.SELECT * FROM customers LEFT JOIN orders ON orders.customer_id = customers.id WHERE orders.id IS NULL;: Customers who NEVER ordered: the anti-join pattern.SELECT * FROM orders RIGHT JOIN customers ON orders.customer_id = customers.id;: All customers (right table) with matching orders.SELECT * FROM orders FULL OUTER JOIN customers ON orders.customer_id = customers.id;: Everything from both tables: matched or not (not in MySQL).SELECT * FROM employees e JOIN employees m ON e.manager_id = m.id;: Self-join: employees with their managers, one table joined to itself.SELECT * FROM a CROSS JOIN b;: Cartesian product: every row of a with every row of b.SELECT * FROM orders JOIN customers USING (customer_id);: Join when both tables share the same column name: less boilerplate.SELECT * FROM orders JOIN customers ON orders.customer_id = customers.id AND customers.country = 'US';: Join with extra conditions: filter inside the join.SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id JOIN products p ON o.product_id = p.id;: Three-table join: orders with customer AND product details.SELECT * FROM orders JOIN LATERAL (SELECT MAX(price) AS max_price FROM order_items WHERE order_id = orders.id) x ON true;: Lateral join: reference earlier tables inside a subquery (PostgreSQL).Aggregation
SELECT country, COUNT(*) FROM users GROUP BY country;: Count users per country: the fundamental GROUP BY.SELECT country, COUNT(*) FROM users GROUP BY country HAVING COUNT(*) > 100;: Filter groups after aggregation: countries with 100+ users.SELECT department, AVG(salary) FROM employees GROUP BY department;: Average salary per department.SELECT department, SUM(salary) FROM employees GROUP BY department;: Total payroll per department.SELECT department, MIN(salary), MAX(salary) FROM employees GROUP BY department;: Salary range per department.SELECT COUNT(*), SUM(total) FROM orders;: Multiple aggregates in one pass: order count and revenue.SELECT year, month, SUM(revenue) FROM sales GROUP BY year, month ORDER BY year, month;: Group by multiple columns: monthly revenue over years.SELECT GROUP_CONCAT(name) FROM users WHERE plan = 'pro';: Concatenate values into one string (MySQL: GROUP_CONCAT, PostgreSQL: string_agg).SELECT STRING_AGG(name, ', ' ORDER BY name) FROM users;: Comma-joined list, sorted: tag lists and CSV building (PostgreSQL).SELECT ARRAY_AGG(name) FROM users;: Collect values into an array (PostgreSQL): pass to client code.SELECT JSON_AGG(name) FROM users;: Aggregate into a JSON array: build API payloads in SQL.SELECT department, COUNT(*) FILTER (WHERE salary > 100000) FROM employees GROUP BY department;: Conditional count per group (PostgreSQL): senior staff per department.Window Functions
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) FROM employees;: Rank employees by salary: ties share a rank with gaps.SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) FROM employees;: Rank without gaps: 1,2,2,3 instead of 1,2,2,4.SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) FROM employees;: Unique sequential number per row: no ties.SELECT name, department, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) FROM employees;: Rank within each department separately: per-group leaderboards.SELECT order_date, total, SUM(total) OVER (ORDER BY order_date) AS running_total FROM orders;: Cumulative running total over time.SELECT month, revenue, AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS ma3 FROM monthly;: 3-month moving average: smooth out seasonality.SELECT name, salary, salary - LAG(salary) OVER (ORDER BY id) AS diff FROM employees;: Compare each row to the previous: month-over-month changes.SELECT month, revenue, revenue - LEAD(revenue) OVER (ORDER BY month) AS next_diff FROM monthly;: Compare to the NEXT row: peek ahead.SELECT month, revenue, FIRST_VALUE(revenue) OVER (ORDER BY month) FROM monthly;: First value in the window: compare each month to January.SELECT order_id, customer_id, total, total / SUM(total) OVER (PARTITION BY customer_id) AS share FROM orders;: Share of each order within a customer's total: % of customer spend.Modifying Data
INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com');: Insert a single row.INSERT INTO users (name, email) VALUES ('A', 'a@x.com'), ('B', 'b@x.com'), ('C', 'c@x.com');: Insert multiple rows in one statement: much faster than separate inserts.INSERT INTO archive SELECT * FROM users WHERE created_at < '2020-01-01';: Insert from a query: copy old rows into an archive table.INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON CONFLICT (email) DO NOTHING;: Skip if the email already exists: idempotent imports (PostgreSQL).INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;: Upsert: update the existing row on conflict (PostgreSQL).INSERT INTO users (name, email) VALUES ('Jane', 'jane@x.com') ON DUPLICATE KEY UPDATE name = VALUES(name);: MySQL upsert: same idea, different syntax.UPDATE users SET status = 'active' WHERE id = 42;: Update one row's column.UPDATE users SET status = 'inactive' WHERE last_login_at < NOW() - INTERVAL '1 year';: Bulk update with a condition: flag stale accounts.UPDATE orders SET total = total * 1.1 WHERE status = 'pending';: Update using the existing value: apply a 10% surcharge.UPDATE users SET email = NULL WHERE email = '';: Normalize empty strings to NULL: clean up bad data.DELETE FROM users WHERE id = 42;: Delete one row.DELETE FROM users WHERE last_login_at < NOW() - INTERVAL '5 years';: Bulk delete: purge old records.DELETE FROM users;: Delete ALL rows: table stays, data goes. Use with extreme care.TRUNCATE TABLE users;: Delete all rows instantly, resetting storage: much faster than DELETE for full clears.UPDATE users SET plan = 'pro' FROM (SELECT id FROM users WHERE email LIKE '%@company.com') AS corp WHERE users.id = corp.id;: Update from a subquery (PostgreSQL): upgrade a specific set.MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);: Full upsert: insert new rows and update existing in one statement (SQL Server/Oracle).Schema & Indexes
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at TIMESTAMP DEFAULT NOW());: Create a table with constraints: the standard users table.ALTER TABLE users ADD COLUMN phone TEXT;: Add a column to an existing table.ALTER TABLE users DROP COLUMN phone;: Remove a column: and its data.ALTER TABLE users RENAME COLUMN phone TO mobile;: Rename a column.ALTER TABLE users ALTER COLUMN name SET NOT NULL;: Add a NOT NULL constraint: enforce required fields.ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);: Add a named unique constraint.ALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE;: Add a foreign key: cascade deletes customers' orders.CREATE INDEX idx_users_email ON users (email);: Speed up lookups by email: index the columns you filter on.CREATE UNIQUE INDEX idx_users_email ON users (email);: Index with a uniqueness guarantee: enforce dedup at the database level.CREATE INDEX idx_orders_customer ON orders (customer_id, created_at);: Composite index: filter by customer and sort by date efficiently.CREATE INDEX idx_orders_lower_email ON users (LOWER(email));: Functional index: fast case-insensitive lookups (PostgreSQL).CREATE INDEX idx_users_name ON users USING GIN (to_tsvector('english', name));: Full-text search index: fast text searching (PostgreSQL).DROP INDEX idx_users_email;: Remove an index.DROP TABLE users;: Delete a table permanently: no undo.EXPLAIN SELECT * FROM users WHERE email = 'x@y.com';: Show the query plan: is it using your index or scanning the whole table?EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 5;: Run the query AND show actual timings: find slow queries.CREATE VIEW active_users AS SELECT * FROM users WHERE status = 'active';: Save a query as a reusable view: treat it like a table.CREATE MATERIALIZED VIEW monthly_revenue AS SELECT date_trunc('month', created_at) AS month, SUM(total) FROM orders GROUP BY 1;: Precompute and store an aggregation (PostgreSQL): instant reads on big data.REFRESH MATERIALIZED VIEW monthly_revenue;: Rebuild a materialized view's data: schedule this after bulk loads.Transactions & Advanced
BEGIN; ... COMMIT;: Wrap multiple statements in one atomic transaction: all succeed or all roll back.BEGIN; ... ROLLBACK;: Abort a transaction: undo everything since BEGIN.SELECT ... FOR UPDATE;: Lock selected rows until the transaction ends: safe "transfer money" reads.SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;: Isolate a transaction from concurrent changes: snapshot consistency.SELECT NOW();: Current timestamp: when is "now" on the database server?SELECT CURRENT_DATE;: Today's date without time.SELECT DATE_TRUNC('month', NOW());: Truncate a timestamp to the month (PostgreSQL): group by month cleanly.SELECT * FROM orders WHERE created_at >= NOW() - INTERVAL '7 days';: Rows from the last 7 days: rolling windows.SELECT DATEDIFF('day', created_at, NOW()) FROM users;: Days since an event (PostgreSQL DATEDIFF-style: EXTRACT(EPOCH FROM ...)).WITH recent AS (SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days') SELECT customer_id, SUM(total) FROM recent GROUP BY customer_id;: CTE: name an intermediate result for readable queries.WITH RECURSIVE nums AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n < 10) SELECT n FROM nums;: Recursive CTE: generate sequences, trees, and hierarchies.SELECT * FROM orders WHERE total = (SELECT MAX(total) FROM orders);: Subquery: rows matching an aggregate condition.SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.customer_id = users.id);: EXISTS: users who have at least one order. Faster than IN on big tables.SELECT * FROM users WHERE id IN (SELECT customer_id FROM orders WHERE total > 500);: IN with a subquery: users with big orders.SELECT CASE WHEN total > 1000 THEN 'high' WHEN total > 100 THEN 'medium' ELSE 'low' END AS tier, COUNT(*) FROM orders GROUP BY 1;: CASE: bucket rows into categories, then aggregate.SELECT * FROM users UNION SELECT * FROM deleted_users;: Combine results, deduplicating rows.SELECT * FROM users UNION ALL SELECT * FROM deleted_users;: Combine results keeping duplicates: faster than UNION.SELECT (json_data->>'name') FROM users;: Extract a field from a JSON column (PostgreSQL): query JSON directly.SELECT * FROM users WHERE json_data @> '{"plan": "pro"}';: Match rows by JSON content (PostgreSQL GIN): JSON filtering.Frequently asked questions
What is the difference between INNER JOIN, LEFT JOIN and FULL OUTER JOIN?
INNER JOIN returns only rows that match in both tables. LEFT JOIN returns all left-table rows plus matches from the right (NULL where missing). FULL OUTER JOIN returns all rows from both sides, matching where possible.
How do I find duplicate rows in a table?
Group by the columns that should be unique and count them: SELECT col, COUNT(*) FROM table GROUP BY col HAVING COUNT(*) > 1. The HAVING clause filters after aggregation.
What is the difference between WHERE and HAVING?
WHERE filters rows before aggregation, so it cannot reference aggregate functions like COUNT or SUM. HAVING filters groups after aggregation. Use WHERE for row-level and HAVING for group-level conditions.
How do I paginate query results?
Use LIMIT <count> OFFSET <n> (PostgreSQL/MySQL/SQLite) or OFFSET ... FETCH NEXT <n> ROWS ONLY (SQL Server/Oracle). Order by a stable column first so pagination stays consistent.