SQL Fundamentals
SQL Standards and Dialects
Section titled “SQL Standards and Dialects”SQL is defined by ANSI/ISO standards (SQL-86, SQL-89, SQL-92, SQL:1999, SQL:2003, SQL:2006, SQL:2008, SQL:2011, SQL:2016, SQL:2019, SQL:2023). No database implements the full standard. PostgreSQL has the broadest standards compliance among open-source databases. MySQL diverges Significantly. SQLite implements a large subset but omits many features (e.g., RIGHT JOIN, FULL OUTER JOIN were added in 3.39.0, 2022).
When this document specifies behaviour, it defaults to PostgreSQL syntax unless otherwise noted.
Data Definition Language (DDL)
Section titled “Data Definition Language (DDL)”DDL defines and modifies the database schema. These statements are transactional in PostgreSQL and SQLite but often auto-commit in MySQL.
CREATE TABLE
Section titled “CREATE TABLE”CREATE TABLE employees ( emp_id SERIAL PRIMARY KEY, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, hire_date DATE NOT NULL DEFAULT CURRENT_DATE, salary NUMERIC(10,2) NOT NULL CHECK (salary > 0), department_id INTEGER REFERENCES departments(dept_id) ON DELETE SET NULL, CONSTRAINT uq_email UNIQUE (email), CONSTRAINT chk_salary_range CHECK (salary >= 30000 AND salary <= 1000000));Key elements:
SERIAL(PostgreSQL) /AUTO_INCREMENT(MySQL) /INTEGER PRIMARY KEY(SQLite) for auto-generating keysNOT NULL— the column must have a valueUNIQUE— no two rows can have the same value in this columnCHECK— an arbitrary boolean expression evaluated on insert/updateDEFAULT— value used when no explicit value is providedREFERENCES— foreign key constraint with referential action
Column Data Types
Section titled “Column Data Types”| Type Category | PostgreSQL Types | Notes |
|---|---|---|
| Integers | SMALLINT``INTEGER``BIGINT | INTEGER is 4 bytes, BIGINT is 8 bytes |
| Fixed precision | NUMERIC(p,s)``DECIMAL(p,s) | Exact arithmetic; NUMERIC(10,2) holds up to 99,999,999.99 |
| Floating point | REAL``DOUBLE PRECISION | Inexact; avoid for financial data |
| Variable string | VARCHAR(n)``TEXT | VARCHAR with length is a constraint, not a storage optimisation in PostgreSQL |
| Fixed string | CHAR(n) | Padded with spaces; rarely useful |
| Boolean | BOOLEAN | TRUE``FALSE``NULL |
| Date/Time | DATE``TIME``TIMESTAMP``TIMESTAMPTZ | TIMESTAMPTZ stores UTC; always prefer it over TIMESTAMP |
| Binary | BYTEA | Variable-length binary data |
| JSON | JSON``JSONB | JSONB is stored in decomposed binary form; faster to query |
| UUID | UUID | Requires the uuid-ossp or pgcrypto extension |
| Array | INTEGER[]``TEXT[] | PostgreSQL-specific extension |
| Network | INET``CIDR``MACADDR | PostgreSQL-specific; enforces valid IP/MAC formats |
AVG(salary) excludes rows where salary IS NULL from both the sum and the count. If you need to Treat NULL as zero, use AVG(COALESCE(salary, 0))But understand that this changes the semantics: NULL means “unknown,” not “zero.”
GROUP BY
Section titled “GROUP BY”SELECT department_id, COUNT(*) AS headcount, AVG(salary) AS avg_salaryFROM employeesGROUP BY department_idORDER BY avg_salary DESC;Every column in the SELECT list must either appear in the GROUP BY clause or be wrapped in an Aggregate function. PostgreSQL is strict about this; MySQL (with ONLY_FULL_GROUP_BY disabled) Allows ambiguous queries.
HAVING
Section titled “HAVING”Filters groups after aggregation. WHERE filters rows before aggregation.
SELECT department_id, AVG(salary) AS avg_salaryFROM employeesWHERE hire_date >= '2022-01-01' -- filter individual rows firstGROUP BY department_idHAVING AVG(salary) > 100000 -- then filter aggregated groupsORDER BY avg_salary DESC;GROUPING SETS, CUBE, ROLLUP
Section titled “GROUPING SETS, CUBE, ROLLUP”PostgreSQL extensions for multi-level grouping:
-- GROUPING SETS: specify multiple groupings in one querySELECT department_id, job_title, COUNT(*), AVG(salary)FROM employeesGROUP BY GROUPING SETS ( (department_id, job_title), (department_id), ());
-- ROLLUP: hierarchical subtotalsSELECT region, country, SUM(revenue)FROM salesGROUP BY ROLLUP (region, country);-- Produces: (region, country), (region), ()
-- CUBE: all possible combinationsSELECT region, channel, product_line, SUM(revenue)FROM salesGROUP BY CUBE (region, channel, product_line);-- Produces: all 8 combinations of 3 columnsWindow Functions
Section titled “Window Functions”Window functions perform a calculation across a set of table rows related to the current row. Unlike Aggregate functions with GROUP BYThey do not collapse rows — every input row produces an output Row.
Syntax
Section titled “Syntax”function_name(args) OVER ( [PARTITION BY partition_expression] [ORDER BY sort_expression [ASC|DESC] [NULLS {FIRST|LAST}]] [frame_clause: ROWS|RANGE BETWEEN start AND end])ROW_NUMBER, RANK, DENSE_RANK
Section titled “ROW_NUMBER, RANK, DENSE_RANK”SELECT emp_id, department_id, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num, RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rank, DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rankFROM employees;| salary | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 150000 | 1 | 1 | 1 |
| 150000 | 2 | 1 | 1 |
| 140000 | 3 | 3 | 2 |
| 140000 | 4 | 3 | 2 |
| 130000 | 5 | 5 | 3 |
ROW_NUMBER: assigns a unique sequential integer to each row within the partitionRANK: ties get the same rank; next rank skips (1, 1, 3, 3, 5)DENSE_RANK: ties get the same rank; next rank does not skip (1, 1, 2, 2, 3)
LAG and LEAD
Section titled “LAG and LEAD”Access values from preceding or following rows:
SELECT order_date, revenue, LAG(revenue, 1) OVER (ORDER BY order_date) AS prev_day_revenue, LEAD(revenue, 1) OVER (ORDER BY order_date) AS next_day_revenue, revenue - LAG(revenue, 1) OVER (ORDER BY order_date) AS day_over_day_changeFROM daily_sales;Aggregate Window Functions
Section titled “Aggregate Window Functions”-- Running total:SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total;
-- Moving average (7-day):SELECT order_date, amount, AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7day;
-- Percentage of total:SELECT department_id, salary, salary / SUM(salary) OVER (PARTITION BY department_id) AS pct_of_dept_salaryFROM employees;
-- First and last value in partition:SELECT emp_id, department_id, hire_date, FIRST_VALUE(hire_date) OVER (PARTITION BY department_id ORDER BY hire_date) AS earliest_hire, LAST_VALUE(hire_date) OVER ( PARTITION BY department_id ORDER BY hire_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS latest_hireFROM employees;