Get Started Free

SELECT Basics

Read rows and columns from one or more tables.

Basic query
SELECT id, name, price
FROM   products
WHERE  price > 100
ORDER BY price DESC
LIMIT  20;
Logical order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
Aliases & expressions
SELECT p.name        AS product,
       p.price * 1.2 AS price_incl_tax
FROM   products AS p;
DISTINCT
SELECT DISTINCT country FROM customers;
Removes duplicate rows from the result.

Filtering

Restrict rows with WHERE predicates.

Operators
WHERE age >= 18
  AND status <> 'banned'
  AND country IN ('US','CA','UK')
  AND price BETWEEN 10 AND 50
Pattern & null
WHERE email LIKE '%@gmail.com'
  AND name ILIKE 'jo%'       -- case-insensitive (PostgreSQL)
  AND deleted_at IS NULL
% = any run of chars, _ = single char.
EXISTS / IN subquery
SELECT * FROM orders o
WHERE EXISTS (
  SELECT 1 FROM refunds r WHERE r.order_id = o.id
);

Joins

Combine rows from multiple tables on a related key.

INNER JOIN
SELECT o.id, c.name
FROM   orders o
JOIN   customers c ON c.id = o.customer_id;
Only rows that match in both tables.
LEFT / RIGHT / FULL
FROM orders o
LEFT  JOIN customers c ON c.id = o.customer_id
-- RIGHT JOIN keeps all right rows
-- FULL  JOIN keeps all rows from both
LEFT keeps all left rows; unmatched right columns are NULL.
Self & cross join
-- managers
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;

-- every combination
FROM sizes CROSS JOIN colors;

Grouping & Aggregates

Summarize many rows into grouped results.

GROUP BY + HAVING
SELECT region, COUNT(*) AS n, SUM(amount) AS total
FROM   orders
GROUP BY region
HAVING SUM(amount) > 10000
ORDER BY total DESC;
WHERE filters rows; HAVING filters groups (after aggregation).
Aggregate functions
COUNT(*)          COUNT(DISTINCT x)
SUM(x)  AVG(x)
MIN(x)  MAX(x)
STRING_AGG(name, ', ')   -- concat
Conditional aggregate
SELECT
  COUNT(*) FILTER (WHERE status='paid')  AS paid,
  SUM(CASE WHEN refunded THEN amount ELSE 0 END) AS refunded
FROM orders;
FILTER is PostgreSQL; CASE works everywhere.

Sorting & Paging

Order and slice result sets.

ORDER BY & LIMIT
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;
MySQL/PostgreSQL/SQLite use LIMIT/OFFSET.
Standard FETCH
SELECT * FROM posts
ORDER BY created_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
ANSI syntax, SQL Server, Oracle, PostgreSQL.
Keyset pagination
SELECT * FROM posts
WHERE created_at < $last_seen
ORDER BY created_at DESC
LIMIT 20;
Faster than OFFSET for deep pages.

Subqueries & CTEs

Name intermediate results for readability and reuse.

WITH (CTE)
WITH top_customers AS (
  SELECT customer_id, SUM(amount) AS spent
  FROM orders GROUP BY customer_id
  HAVING SUM(amount) > 1000
)
SELECT c.name, t.spent
FROM top_customers t
JOIN customers c ON c.id = t.customer_id;
Derived table
SELECT region, AVG(total) FROM (
  SELECT region, SUM(amount) AS total
  FROM orders GROUP BY region, month
) s
GROUP BY region;
Recursive CTE
WITH RECURSIVE tree AS (
  SELECT id, parent_id FROM cat WHERE id = 1
  UNION ALL
  SELECT c.id, c.parent_id
  FROM cat c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
Walk hierarchies / graphs.

Window Functions

Compute across a set of rows without collapsing them.

Ranking
SELECT name, region, amount,
  ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn,
  RANK()       OVER (PARTITION BY region ORDER BY amount DESC) AS rnk
FROM sales;
Filter rn = 1 to get top row per group.
Running total & lag
SELECT day, amount,
  SUM(amount) OVER (ORDER BY day) AS running,
  LAG(amount) OVER (ORDER BY day) AS prev_day
FROM daily;

Modifying Data

Insert, update, delete and upsert rows.

INSERT
INSERT INTO users (name, email)
VALUES ('Ada', 'ada@x.com'),
       ('Bo',  'bo@x.com');
UPDATE / DELETE
UPDATE orders SET status = 'shipped'
WHERE id = 42;

DELETE FROM carts
WHERE updated_at < NOW() - INTERVAL '30 days';
Always include a WHERE, no WHERE means every row.
Upsert
INSERT INTO counters (key, n) VALUES ('hits', 1)
ON CONFLICT (key) DO UPDATE
  SET n = counters.n + 1;
PostgreSQL/SQLite. MySQL: ON DUPLICATE KEY UPDATE.

Tables & Indexes (DDL)

Define and change schema objects.

CREATE TABLE
CREATE TABLE products (
  id      SERIAL PRIMARY KEY,
  name    VARCHAR(120) NOT NULL,
  price   NUMERIC(10,2) DEFAULT 0,
  created TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE
ALTER TABLE products ADD COLUMN sku TEXT;
ALTER TABLE products
  ADD CONSTRAINT uq_sku UNIQUE (sku);
Indexes & keys
CREATE INDEX idx_orders_cust ON orders (customer_id);
CREATE UNIQUE INDEX uq_email ON users (email);

FOREIGN KEY (customer_id) REFERENCES customers(id)
Index columns you filter or join on frequently.

Handy Functions

Frequently reached-for scalar helpers.

Null & conditional
COALESCE(nickname, name, 'anon')
NULLIF(a, b)         -- NULL when a = b
CASE WHEN x > 0 THEN 'pos' ELSE 'neg' END
String
UPPER(s)  LOWER(s)  TRIM(s)  LENGTH(s)
CONCAT(a, ' ', b)
SUBSTRING(s FROM 1 FOR 3)
REPLACE(s, 'a', 'b')
Date & time
NOW()   CURRENT_DATE
DATE_TRUNC('month', ts)          -- PostgreSQL
EXTRACT(YEAR FROM ts)
ts + INTERVAL '7 days'
Function names vary a little by database engine.

More information about SQL

SQL (Structured Query Language) is the standard language for working with relational databases, PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite and more. It's one of the most valuable and durable skills in software: nearly every application, analytics stack, and data pipeline touches a relational database, and SQL is how you read from and write to it.

What is SQL?

SQL is a declarative language, you describe what data you want and the database's query planner figures out how to get it. A single SELECT can filter, join, group, and sort across millions of rows.

The main categories of SQL

Why it matters

Well-written SQL, with the right joins, indexes, and window functions, lets the database do work your application would otherwise do slowly and at scale. It's portable knowledge: the core language is the same across engines, with small dialect differences.

Frequently asked questions

What is the difference between WHERE and HAVING?

WHERE filters individual rows before grouping; HAVING filters groups after GROUP BY has aggregated them.

What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN returns only rows that match in both tables. LEFT JOIN returns all rows from the left table, filling unmatched right-side columns with NULL.

Is SQL the same across all databases?

The core (SELECT/JOIN/GROUP BY/DML/DDL) is standardized and portable. Functions, pagination syntax, and some features differ per engine, this cheat sheet notes the common dialect differences.

VisuaLeaf

Run this SQL against your database in VisuaLeaf

VisuaLeaf connects to PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite and more, with a visual query builder, schema diagrams, charts, and a query profiler, plus native MongoDB support.

MongoDB & SQL in one client
Visual query & aggregation builders
Live visual schema diagrams
Charts, dashboards & query profiler
Connection manager for local, cloud & teams
Free Community Edition