SQL Documentation
Structured reference for learning SQL from basics to advanced. Written as a developer-first language guide, not a course.
Introduction to SQL
SQL (Structured Query Language) is the standard language for interacting with relational databases. It allows you to create, read, update, and delete data โ as well as define schemas and control access.
SQL is not a programming language in the traditional sense โ it's a declarative query language. You describe what you want, not how to get it.
Why SQL matters
Nearly every web application, SaaS product, or data dashboard relies on a relational database. SQL is the universal language to talk to them โ and in 2026, it's also the backbone of AI data pipelines.
Popular SQL databases
| Database | Type | Common Use | 2026 Status |
|---|---|---|---|
PostgreSQL | Open source RDBMS | SaaS, complex apps | v17 โ Recommended |
MySQL | Open source RDBMS | Web apps, WordPress | v9.x โ Strong JSON |
SQLite | Embedded RDBMS | Mobile apps, local dev | Stable |
SQL Server | Microsoft RDBMS | Enterprise, .NET apps | 2025 โ AI built-in |
MariaDB | MySQL fork | Cloud databases | Active |
Data Types Reference
Choosing the right data type is critical for performance, storage, and correctness.
| Type | Use Case | Example | Notes |
|---|---|---|---|
INT / BIGINT | Whole numbers, IDs | 150 | Use BIGINT for large tables |
DECIMAL(p,s) | Money, precise decimals | DECIMAL(10,2) | Never use FLOAT for currency |
VARCHAR(n) | Short variable strings | 'Yahya.Dev' | Max n chars, efficient storage |
TEXT | Long text content | Article body, logs | No length limit |
BOOLEAN | True / False flags | TRUE, FALSE | Stored as 0/1 in MySQL |
TIMESTAMP | Date + time values | 2026-01-15 14:30:00 | Use UTC always |
DATE | Date only | 2026-01-15 | No time component |
UUID | Unique identifiers | uuid_generate_v4() | Better than INT for distributed |
JSON / JSONB | Semi-structured data | '{"role":"admin"}' | JSONB is indexed in PostgreSQL |
SERIAL | Auto-incrementing IDs | Primary key column | SERIAL = PG, AUTO_INCREMENT = MySQL |
In PostgreSQL 17 and MySQL 9.x, JSONB columns can be indexed and queried with the ->> operator โ a powerful alternative to adding new columns for semi-structured data.
Syntax Basics
SQL statements are human-readable. Keywords are conventionally uppercase, though SQL is case-insensitive for keywords.
The SELECT statement
The most fundamental SQL command โ retrieves data from one or more tables.
SELECT * FROM users;
SELECT id, name, email
FROM users;
Statement structure
* for all columns.SELECT id, name, email
FROM users
WHERE active = 1
ORDER BY created_at DESC
LIMIT 10;
Always prefer selecting specific columns over SELECT * in production โ it reduces data transfer and makes queries explicit and maintainable.
Filtering & Sorting
WHERE clause
Filters which rows are included in the result. Supports comparison and logical operators.
-- Equality
SELECT * FROM users WHERE role = 'admin';
-- Comparison
SELECT * FROM products WHERE price > 50;
-- Range
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;
-- Pattern match
SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- Multiple conditions
SELECT * FROM users WHERE active = 1 AND role = 'editor';
-- NULL check
SELECT * FROM users WHERE deleted_at IS NULL;
ORDER BY & LIMIT
-- Sort newest first
SELECT id, name, created_at
FROM users
ORDER BY created_at DESC;
-- Top 5 most expensive products
SELECT name, price FROM products ORDER BY price DESC LIMIT 5;
-- Paginate: page 2, 10 items per page
SELECT * FROM posts ORDER BY id LIMIT 10 OFFSET 10;
CRUD Operations
CRUD โ Create, Read, Update, Delete โ maps to INSERT, SELECT, UPDATE, DELETE.
INSERT
-- Single row
INSERT INTO users (name, email, role)
VALUES ('Yahya', 'yehiahwary515@gmail.com', 'admin');
-- Multiple rows
INSERT INTO products (name, price, stock)
VALUES ('Widget A', 29.99, 100), ('Widget B', 49.99, 50), ('Widget C', 9.99, 200);
UPDATE
Always include a WHERE clause with UPDATE. Without it, every row in the table will be modified.
UPDATE users
SET role = 'moderator'
WHERE id = 42;
UPDATE users
SET name = 'Yahya A.', updated_at = NOW()
WHERE id = 42;
DELETE
-- Hard delete
DELETE FROM users WHERE id = 42;
-- Soft delete (preferred in production)
UPDATE users SET deleted_at = NOW() WHERE id = 42;
-- Bulk delete expired sessions
DELETE FROM sessions WHERE expires_at < NOW();
Joins
JOINs combine rows from two or more tables based on a related column.
INNER JOIN
Returns only rows matching in both tables.
LEFT JOIN
All rows from left table + matched rows from right (NULL where no match).
RIGHT JOIN
All rows from right table + matched rows from left.
INNER JOIN
SELECT
o.id AS order_id,
u.name AS customer,
u.email,
o.total,
o.created_at
FROM orders AS o
INNER JOIN users AS u ON o.user_id = u.id;
LEFT JOIN
SELECT
u.id,
u.name,
COUNT(o.id) AS order_count
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY order_count DESC;
Use table aliases (u, o) to keep JOIN queries readable. 90% of performance issues come from JOINs without indexes on the join columns.
Advanced SQL
GROUP BY & Aggregate Functions
Aggregate functions compute a single value from a set of rows. GROUP BY divides rows into groups before applying them.
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(total) AS revenue,
AVG(total) AS avg_order,
MAX(total) AS biggest_order
FROM orders
GROUP BY user_id
ORDER BY revenue DESC;
HAVING
HAVING filters after grouping โ unlike WHERE which filters before aggregation.
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY order_count DESC;
Subqueries
-- Users who placed at least one completed order
SELECT name, email FROM users
WHERE id IN (
SELECT DISTINCT user_id FROM orders WHERE status = 'completed'
);
-- Products above average price
SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);
Indexing
CREATE INDEX idx_users_email ON users (email);
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
DROP INDEX idx_users_email;
Index columns used in WHERE, JOIN ON, and ORDER BY. Don't over-index โ each index slows INSERT/UPDATE.
Pro Level SQL
These are the features that separate a junior from a senior developer. Master these and you can handle any real-world database problem.
CTEs โ Common Table Expressions
A CTE (WITH clause) lets you name a temporary result set and reference it like a table. It makes complex queries readable, reusable, and maintainable โ logic flows top-to-bottom like reading a story.
Use a CTE when you need to reference the same subquery more than once, or when the subquery is complex enough to deserve a descriptive name.
WITH active_users AS (
SELECT id, name, email
FROM users
WHERE active = TRUE
AND deleted_at IS NULL
)
SELECT *
FROM active_users
WHERE email LIKE '%@company.com'
ORDER BY name;
WITH
order_totals AS (
SELECT user_id, COUNT(*) AS order_count, SUM(total) AS lifetime_value
FROM orders
WHERE status = 'completed'
GROUP BY user_id
),
high_value AS (
SELECT user_id FROM order_totals
WHERE lifetime_value > 1000 AND order_count >= 3
)
SELECT u.name, u.email, ot.order_count, ot.lifetime_value
FROM users AS u
INNER JOIN order_totals AS ot ON u.id = ot.user_id
INNER JOIN high_value AS hv ON u.id = hv.user_id
ORDER BY ot.lifetime_value DESC;
Window Functions
Window functions perform calculations across rows related to the current row โ without collapsing them like GROUP BY does. They're the most powerful analytical feature in SQL.
SELECT
employee_name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank
FROM employees
ORDER BY department, salary DESC;
SELECT
month,
revenue,
SUM(revenue) OVER (ORDER BY month) AS running_total,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100
, 1) AS growth_pct
FROM monthly_revenue
ORDER BY month;
-- Top 3 best-selling products per category
WITH ranked AS (
SELECT
category, product_name, total_sold,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_sold DESC) AS rn
FROM products
)
SELECT category, product_name, total_sold
FROM ranked
WHERE rn <= 3
ORDER BY category, total_sold DESC;
Transactions
A transaction groups multiple SQL statements into a single atomic unit. Either all succeed, or none take effect โ the foundation of data integrity.
Transactions guarantee Atomicity (all or nothing), Consistency (valid state before & after), Isolation (concurrent transactions don't interfere), and Durability (committed data survives crashes).
BEGIN
Start the transaction โ changes won't be saved yet.
Run your statements
INSERT, UPDATE, DELETE โ execute but can still be reversed.
COMMIT or ROLLBACK
COMMIT saves permanently. ROLLBACK undoes everything back to BEGIN.
BEGIN;
UPDATE accounts
SET balance = balance - 500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 500
WHERE id = 2;
COMMIT;
-- Both updates happen together, or neither does
BEGIN;
INSERT INTO orders (user_id, total, status)
VALUES (42, 199.99, 'pending');
SAVEPOINT after_order;
UPDATE inventory SET stock = stock - 1
WHERE product_id = 7 AND stock > 0;
-- If something fails, rollback to savepoint (keeps the order insert)
-- ROLLBACK TO SAVEPOINT after_order;
INSERT INTO audit_log (action, user_id) VALUES ('order_created', 42);
COMMIT;
Always wrap financial operations (payments, balance transfers, inventory deductions) in a transaction. A crash mid-operation without one leaves your data in a corrupted, half-updated state.
Real-World Examples
Production-style queries you'd write in real applications.
Users Table
Schema + common queries
A typical users table with JSONB metadata for flexibility.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(20) DEFAULT 'user',
active BOOLEAN DEFAULT TRUE,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP NULL
);
SELECT * FROM users WHERE active = TRUE ORDER BY created_at DESC;
SELECT COUNT(*) FROM users WHERE role = 'admin';
-- Query JSONB column (PostgreSQL)
SELECT * FROM users WHERE metadata->>'plan' = 'pro';
E-Commerce Order Report
Multi-table query
Pull a full order report across orders, order_items, products, and users.
SELECT
o.id AS order_id,
u.name AS customer,
u.email,
p.name AS product,
oi.quantity,
oi.unit_price,
(oi.quantity * oi.unit_price) AS line_total,
o.status,
o.created_at
FROM orders AS o
INNER JOIN users AS u ON o.user_id = u.id
INNER JOIN order_items AS oi ON oi.order_id = o.id
INNER JOIN products AS p ON oi.product_id = p.id
WHERE o.status = 'completed'
AND o.created_at >= NOW() - INTERVAL '30 days'
ORDER BY o.created_at DESC;
SaaS MRR Analytics
Subscription metrics
MRR and churn rate for a SaaS platform.
-- MRR by plan tier
SELECT
p.name AS plan,
COUNT(s.id) AS subscribers,
SUM(p.monthly_price) AS mrr,
ROUND(AVG(p.monthly_price), 2) AS avg_revenue
FROM subscriptions AS s
INNER JOIN plans AS p ON s.plan_id = p.id
WHERE s.status = 'active' AND s.canceled_at IS NULL
GROUP BY p.name, p.monthly_price
ORDER BY mrr DESC;
-- Churn rate last 30 days
SELECT
COUNT(*) AS churned,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM subscriptions), 2) AS churn_pct
FROM subscriptions
WHERE canceled_at >= NOW() - INTERVAL '30 days';
Dashboard Analytics
CTE + Window Functions combined
Monthly revenue trend with running total and growth % โ the kind of query you'd write for a dashboard in a system like YS-Matrix.
WITH monthly AS (
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
WHERE status = 'completed'
AND created_at >= NOW() - INTERVAL '12 months'
GROUP BY DATE_TRUNC('month', created_at)
)
SELECT
TO_CHAR(month, 'Mon YYYY') AS period,
orders,
revenue,
SUM(revenue) OVER (ORDER BY month) AS cumulative_revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100
, 1) AS growth_pct
FROM monthly
ORDER BY month;