Cheat Sheet

SQL Quick Reference

The clauses in the order the database runs them, the patterns you reach for daily, and the Oracle vs PostgreSQL differences that bite when you switch.

11
Groups
Oracle 23
Checked
PG 17
Checked
Index ยท Spring ยท SQL ยท Git ยท Docker/kubectl ยท Kafka/Rabbit CLI ยท Redis ยท Big-O
01
Chapter 1 - Order of evaluation

Written Order vs Logical Order

You write SELECT first, but the database logically processes it near the end. This one table explains why you cannot use a SELECT alias in WHERE, why HAVING exists, and why window functions cannot appear in WHERE.

StepClauseWhat happensConsequence
1FROM / JOINBuild the row source: tables, joins, subqueriesJoin conditions (ON) filter before WHERE; for outer joins this matters
2WHEREFilter individual rowsCannot use aggregates or select-list aliases here
3GROUP BYForm groupsEvery non-aggregated select column must be in the group (standard SQL)
4HAVINGFilter groupsUse for conditions on aggregates; use WHERE for row conditions (faster)
5Window functionsComputed over the rows left after step 4Cannot be used in WHERE or GROUP BY; wrap in a subquery or CTE to filter on them
6SELECTEvaluate the select list and aliasesAliases exist only from here on
7DISTINCTRemove duplicate rowsApplies to the whole select list, not one column
8ORDER BYSortMay use select-list aliases; PostgreSQL also accepts them in GROUP BY, Oracle does not
9LIMIT / FETCH FIRSTCut the sorted resultWithout ORDER BY the rows you get are arbitrary
02
Chapter 2 - Query building blocks

Select, Filter, Sort, Page

The everyday clauses and the operators that trip people up.

ItemMeaningGotcha
SELECT DISTINCT a, bUnique combinations of the listed columnsDISTINCT hides a bad join; fix the join instead
WHERE a BETWEEN 1 AND 5Inclusive on both endsOn timestamps, BETWEEN day1 AND day2 misses most of day2; use >= and <
WHERE a IN (1, 2, 3)Match any listed valueOracle limits an IN list to 1000 expressions
WHERE a LIKE 'ab%'% any characters, _ one characterA leading % prevents a normal index range scan; LIKE is case-sensitive in Oracle and PostgreSQL
WHERE EXISTS (SELECT 1 ...)True if the subquery returns any rowStops at the first match; safe with NULLs, unlike NOT IN
CASE WHEN c THEN x ELSE y ENDConditional expressionNo ELSE gives NULL; branches must have compatible types
ORDER BY 2 DESCSort by the second select columnPosition numbers break when the select list changes
ORDER BY x NULLS LASTControl NULL placementDefault in both Oracle and PostgreSQL: NULLs sort as largest (last for ASC, first for DESC)
FETCH FIRST n ROWS ONLYStandard row limit (Oracle 12c+, PostgreSQL)WITH TIES also returns rows tied with the last one and needs ORDER BY
OFFSET m ROWS FETCH NEXT n ROWS ONLYSkip m, then take nDeep offsets scan and discard rows; use keyset paging (WHERE id > :last ORDER BY id) for big tables
LIMIT n OFFSET mPostgreSQL/MySQL spelling of the sameNot valid in Oracle
SELECT ... FOR UPDATE SKIP LOCKEDLock rows, skip rows locked by othersGood for job queues; both Oracle and PostgreSQL support SKIP LOCKED
03
Chapter 3 - Joins

Combining Tables

Think of a join as: take every pair of rows, keep pairs where ON is true. Outer joins add unmatched rows back with NULLs.

JoinReturnsUse whenGotcha
INNER JOINOnly matching pairsYou need rows that exist on both sidesSilently drops rows with no match
LEFT [OUTER] JOINAll left rows, right columns NULL if no matchKeep the main entity even without childrenA WHERE on a right-table column turns it into an inner join; put it in ON
RIGHT [OUTER] JOINMirror of left joinRarely; swap the table order insteadMixing left and right joins in one query is hard to read
FULL [OUTER] JOINAll rows from both sidesReconciliation: find rows missing on either sideFilter with WHERE l.id IS NULL OR r.id IS NULL
CROSS JOINEvery pair (Cartesian product)Generate combinations, calendarsForgetting a join condition produces one by accident: N x M rows
Self joinJoin a table to itself with two aliasesHierarchies, "compare to previous row"Alias both sides; consider a window function (LAG) instead
USING (col)Join on same-named column, output it onceShort joins on identical key namesSELECT * shows the joined column once; names must really be identical on both sides
NATURAL JOINJoins on all same-named columnsAlmost neverA new column with the same name changes the join silently
Anti join: NOT EXISTSRows with no match in another table"Customers without orders"Prefer it over NOT IN (NULL trap) and usually over LEFT JOIN ... IS NULL
Semi join: EXISTSRows that have at least one match, no duplicates"Customers with orders"A plain join would repeat the customer once per order
Join fan-outOne-to-many join multiplies parent rowsAggregating after joining two child tablesSUM over a fan-out double counts; aggregate each child first in a subquery
04
Chapter 4 - Aggregates

Grouping and Summaries

Aggregates collapse rows. NULLs are ignored by every aggregate except COUNT(*).

ItemMeaningGotcha
COUNT(*)Number of rowsCounts NULL rows too
COUNT(col)Number of non-NULL valuesDifferent from COUNT(*) when the column has NULLs
COUNT(DISTINCT col)Number of distinct non-NULL valuesCan be slow on big sets
SUM / AVGTotal and mean of non-NULL valuesAVG over no rows or all NULLs is NULL; SUM of no rows is NULL, not 0
MIN / MAXSmallest and largestWork on strings and dates too
GROUP BY a, bOne output row per distinct pairSelecting a column not in the group fails in Oracle and PostgreSQL (unless it is functionally dependent in PostgreSQL)
HAVING COUNT(*) > 1Keep groups meeting a conditionThe standard way to find duplicates
ROLLUP(a, b)Subtotals and a grand totalSubtotal rows show NULL in the rolled-up columns; use GROUPING() to tell them from real NULLs
CUBE(a, b)Subtotals for every combinationRow count grows as 2 to the power of the number of columns
GROUPING SETSChoose exactly which groupingsVerbose but exact
String aggregationPostgreSQL string_agg(x, ',' ORDER BY x); Oracle LISTAGG(x, ',') WITHIN GROUP (ORDER BY x)Oracle LISTAGG raises ORA-01489 on overflow by default; ON OVERFLOW TRUNCATE is available
FILTER (WHERE ...)PostgreSQL: aggregate over a subset, e.g. COUNT(*) FILTER (WHERE x > 0)Not in Oracle; use SUM(CASE WHEN ... THEN 1 ELSE 0 END)
05
Chapter 5 - Window functions

Analytic Functions

A window function adds a value to each row without collapsing rows: fn() OVER (PARTITION BY ... ORDER BY ... frame). Both Oracle (as "analytic functions") and PostgreSQL support the functions below.

ItemMeaningGotcha
ROW_NUMBER()1, 2, 3 ... within the partitionTies get different numbers in an arbitrary order; add a tie-breaker column to ORDER BY
RANK()Ties share a rank, then a gap (1, 1, 3)Gaps surprise people expecting consecutive numbers
DENSE_RANK()Ties share a rank, no gap (1, 1, 2)Good for "second highest salary"
NTILE(n)Split rows into n bucketsBucket sizes differ by one when not divisible
LAG(x, n, default) / LEADValue from n rows before or afterReturns NULL at the edges unless you give a default
FIRST_VALUE / LAST_VALUEFirst or last value in the frameDefault frame ends at the current row, so LAST_VALUE often returns the current row; extend the frame to UNBOUNDED FOLLOWING
SUM(x) OVER (ORDER BY d)Running totalWith ORDER BY and no frame, ties (peers) are summed together (RANGE default)
ROWS BETWEEN 2 PRECEDING AND CURRENT ROWMoving window by physical rowsROWS counts rows; RANGE counts by value and includes peers
PARTITION BYRestart the calculation per groupOmit it and the whole result is one partition
Filtering on a window resultWrap in a subquery or CTE and filter outsideWindow functions run after WHERE, so you cannot filter on them directly
06
Chapter 6 - Patterns

Everyday Patterns

Top N per group, de-duplicate, running total, hierarchy (standard SQL; checked on SQLite 3.45 for syntax and results, Oracle needs the alias changes in chapter 10)
-- Top 2 salaries per department (add a tie-breaker so the result is stable) SELECT dept, name, salary FROM (SELECT e.*, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC, id) AS rn FROM emp e) ranked WHERE rn <= 2; -- Keep the first row of each duplicate set SELECT * FROM ( SELECT e.*, ROW_NUMBER() OVER (PARTITION BY dept, salary ORDER BY id) AS rn FROM emp e ) d WHERE rn = 1; -- Running total SELECT id, salary, SUM(salary) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running FROM emp; -- Org chart walk (PostgreSQL and SQLite: WITH RECURSIVE; Oracle: WITH without RECURSIVE) WITH RECURSIVE t(id, name, lvl) AS ( SELECT id, name, 1 FROM emp WHERE mgr IS NULL UNION ALL SELECT e.id, e.name, t.lvl + 1 FROM emp e JOIN t ON e.mgr = t.id ) SELECT * FROM t;
07
Chapter 7 - Subqueries, CTEs, set operators

Composing Queries

A CTE (WITH name AS (...)) names a subquery so the main query reads top to bottom.

ItemMeaningGotcha
Scalar subqueryReturns one value, used like a columnMore than one row is a runtime error
IN (subquery)Match any value returnedFine for IN; NOT IN is dangerous with NULLs (see chapter 8)
Correlated subqueryReferences the outer row; re-evaluated per row logicallyOptimisers often turn it into a join; rewrite as a join or window function if slow
Derived tableSubquery in FROMOracle: no AS before the alias; PostgreSQL requires an alias
WITH x AS (...)Common table expression (CTE)Readability tool; whether it is inlined or materialised depends on the database and version
WITH RECURSIVEAnchor query + recursive member joined to the CTEOracle omits the RECURSIVE keyword; add a termination condition or it loops (cycle clauses exist)
UNIONCombine results, remove duplicatesSorts or hashes to deduplicate; use UNION ALL when you know there are no duplicates
UNION ALLCombine results, keep duplicatesFastest; columns must match in count and type group
INTERSECTRows in both resultsDuplicates removed; NULLs are treated as equal here
EXCEPT / MINUSRows in the first but not the secondPostgreSQL: EXCEPT. Oracle: MINUS, and the Oracle 23 docs list EXCEPT as a synonym
ALL variantsINTERSECT ALL, EXCEPT ALL keep duplicates by countPostgreSQL has them; Oracle 23 docs list them too, older Oracle does not
Set operator precedenceEvaluated left to right in Oracle; use parenthesesIn standard SQL INTERSECT binds tighter than UNION; parenthesise to be safe
08
Chapter 8 - NULL logic

Three-Valued Logic

SQL has TRUE, FALSE and UNKNOWN. WHERE keeps only TRUE rows, so UNKNOWN rows vanish.

ExpressionResultWhy / fix
NULL = NULLUNKNOWN (not TRUE)Use IS NULL and IS NOT NULL
NULL <> 1UNKNOWNRows with NULL are excluded by WHERE x <> 1; add OR x IS NULL if you want them
1 NOT IN (2, NULL)UNKNOWN: returns no rowsUse NOT EXISTS; checked: returns zero rows
TRUE OR UNKNOWNTRUEShort-circuit rule
FALSE AND UNKNOWNFALSEShort-circuit rule
NOT UNKNOWNUNKNOWNNegating does not turn it into TRUE
NULL + 10NULLArithmetic with NULL is NULL
COUNT(*) vs COUNT(col)5 vs 4 when one value is NULLChecked on a sample table
COALESCE(a, b, c)First non-NULL argumentANSI standard, works in Oracle and PostgreSQL; all arguments must share a type group
NULLIF(a, b)NULL if a equals b, else aClassic divide-by-zero guard: x / NULLIF(y, 0)
GROUP BY / DISTINCTTreat all NULLs as one groupUnlike =, grouping considers NULLs equal
Unique constraintPostgreSQL allows many NULLs in a unique columnOracle allows many NULLs in a single-column unique key; in composite keys, two rows with all-NULL are considered duplicates (Oracle treats nulls as equal in compound keys)
Oracle empty string'' is NULL in OracleThe Oracle docs say a zero-length character value is treated as NULL; PostgreSQL keeps '' and NULL distinct
|| with NULLOracle: NULL is ignored; PostgreSQL: result is NULLIn PostgreSQL use concat() (skips NULLs) or COALESCE
09
Chapter 9 - Dates and times

Date Handling: Oracle vs PostgreSQL

Dates are where portability ends. Learn these pairs.

TaskOraclePostgreSQLGotcha
Current date and timeSYSDATE (DATE with time), SYSTIMESTAMP, CURRENT_TIMESTAMPnow(), CURRENT_TIMESTAMP, CURRENT_DATEOracle DATE always has a time part; PostgreSQL date does not
Drop the time partTRUNC(d)date_trunc('day', ts) or cast ts::dateUsing TRUNC(col) in a WHERE stops index use; compare with a range
Add daysd + 7d + 7 (date) or ts + INTERVAL '7 days'In Oracle the number is days, fractions are hours
Add monthsADD_MONTHS(d, 1)d + INTERVAL '1 month'End-of-month behaviour differs; test Jan 31 + 1 month
Differenced2 - d1 gives days (NUMBER)date - date gives integer days; timestamp - timestamp gives an intervalDifferent types, so reporting code needs changes
Months betweenMONTHS_BETWEEN(d2, d1)age(d2, d1) or EXTRACT on an intervalResults are not the same kind of number
Last day of monthLAST_DAY(d)(date_trunc('month', d) + INTERVAL '1 month - 1 day')::dateDifferent idioms
Format as textTO_CHAR(d, 'YYYY-MM-DD HH24:MI:SS')to_char(d, 'YYYY-MM-DD HH24:MI:SS')Format models are similar but not identical; check minutes (MI) vs months (MM)
Parse textTO_DATE(s, fmt), TO_TIMESTAMPto_date, to_timestamp, or castWithout a format, Oracle uses NLS_DATE_FORMAT of the session
Extract a partEXTRACT(YEAR FROM d)EXTRACT(YEAR FROM d)Same standard syntax in both
Date literalDATE '2026-10-31'DATE '2026-10-31'Prefer literals over relying on session formats
Time zoneTIMESTAMP WITH TIME ZONE, AT TIME ZONEtimestamptz, AT TIME ZONEPostgreSQL timestamptz stores UTC and converts on display; store UTC and convert at the edge
Range filter (any DB)WHERE d >= DATE '2026-10-01' AND d < DATE '2026-11-01'sameHalf-open ranges avoid the BETWEEN end-of-day trap
10
Chapter 10 - Oracle vs PostgreSQL

Dialect Differences You Will Hit

Checked against the Oracle Database SQL Language Reference (23 series) and the PostgreSQL documentation (current) on 2026-10-31. Where the Oracle 23 release changed behaviour, the row says so.

TopicOraclePostgreSQLNote
Limit rowsFETCH FIRST n ROWS ONLY (12c+); older code uses WHERE ROWNUM <= nLIMIT n or FETCH FIRST n ROWS ONLYROWNUM is assigned before ORDER BY, so WHERE ROWNUM <= 5 ORDER BY x is not the top 5 by x; sort in a subquery first
Row number in rowsROWNUM pseudocolumnNone; use ROW_NUMBER() OVER ()ROWNUM > 1 never returns rows
No-table selectSELECT 1 FROM DUAL; from Release 23 the FROM clause may be omittedSELECT 1Keep FROM DUAL for code that must run on older Oracle
Null replacementNVL(a, b), COALESCECOALESCE(a, b)NVL is Oracle-only and takes exactly two arguments; prefer COALESCE for portability
UpsertMERGE INTO t USING s ON (...) WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...INSERT ... ON CONFLICT (...) DO UPDATE; MERGE since PostgreSQL 15Oracle MERGE allows DELETE WHERE inside WHEN MATCHED; PostgreSQL 17 added RETURNING and WHEN NOT MATCHED BY SOURCE to MERGE
Sequencesseq.NEXTVAL, seq.CURRVALnextval('seq'), currval('seq')Both can leave gaps; identity columns (GENERATED ... AS IDENTITY) exist in both
Set differenceMINUS (and EXCEPT as a synonym in the 23 docs)EXCEPTUse MINUS on older Oracle
Table aliasFROM emp e (no AS for tables)FROM emp AS e or emp eAS before a table alias is an error in Oracle
String concatenation||, NULL treated as empty|| (NULL propagates), concat()See chapter 8
Empty stringIs NULLIs a value, distinct from NULLPort WHERE col = '' with care
String aggregationLISTAGGstring_aggDifferent overflow behaviour
Top row per groupROW_NUMBER() subquerySELECT DISTINCT ON (g) ... ORDER BY g, x DESC or ROW_NUMBER()DISTINCT ON is PostgreSQL only
Return generated valuesRETURNING col INTO :var (PL/SQL, JDBC)RETURNING col returns a result setDifferent client handling
Boolean typeBOOLEAN data type is documented in the 23 release; older releases use NUMBER(1) or CHAR(1) flagsbooleanCheck your Oracle release before relying on it in tables
Pagination in queriesOFFSET n ROWS FETCH NEXT m ROWS ONLYsame or LIMIT m OFFSET nStandard form works in both
Comparison of stringsDepends on NLS_COMP / NLS_SORTDepends on collation of the column/databaseCase-insensitive search needs LOWER() or a configured collation
Identifier caseUnquoted names are folded to upper caseUnquoted names are folded to lower caseQuoted identifiers keep case and then must always be quoted
Procedural languagePL/SQLPL/pgSQLSimilar ideas, different syntax
Recursive CTEWITH t(cols) AS (...) (no RECURSIVE)WITH RECURSIVE t(cols) AS (...)Keyword differs
11
Chapter 11 - Common mistakes

SQL Mistakes

โš  The ones that cost real time
  • **NOT IN with a subquery that can return NULL.** Returns nothing. Use NOT EXISTS.
  • **WHERE on the right table of a LEFT JOIN.** Silently makes it an inner join; put the filter in ON.
  • **BETWEEN on timestamps.** Misses the rest of the last day; use >= start AND < next_start.
  • Aggregating after a join fan-out. Sums double count; aggregate the child tables first.
  • **ROWNUM with ORDER BY in the same query (Oracle).** Rows are numbered before sorting; wrap the sorted query in a subquery.
  • **LAST_VALUE with the default frame.** It returns the current row; extend the frame.
  • **ORDER BY missing with LIMIT or FETCH FIRST.** Different rows on different runs.
  • **UNION where UNION ALL is meant.** Costs a sort and silently removes legitimate duplicates.
  • **Function on an indexed column in WHERE** (TRUNC(d), UPPER(name)). The plain index is not used; compare ranges or create a function-based index.
  • **COUNT(col) when you meant COUNT(*).** NULL values are not counted.
12
Chapter 12 - Go deeper

Long-form pages and what was checked

TopicPage
Filtering, sorting, limitingFiltering with WHERE, Sorting and Limiting
Aggregation and joinsAggregation, Joining Tables
Subqueries and CTEsSubqueries and CTEs
Window functionsWindow Functions
Indexes and performanceIndexes and Performance
Writing data (MERGE, upsert)Writing Data
Oracle PL/SQLPL/SQL
SQL course indexSQL Tutorial
Version note

Checked on 2026-10-31: Oracle ROWNUM ordering, set operators (EXCEPT as a synonym of MINUS, ALL variants), the optional FROM clause from Release 23, empty string as NULL, || with NULL, LISTAGG overflow default and the MERGE ... DELETE WHERE clause against the Oracle Database 23 SQL Language Reference; PostgreSQL default NULL ordering, LIMIT/FETCH FIRST ... WITH TIES, DISTINCT ON and concat()/|| behaviour against the current PostgreSQL manual; MERGE in PostgreSQL 15 and its RETURNING and WHEN NOT MATCHED BY SOURCE additions in PostgreSQL 17 against the 15.0 and 17.0 release notes. The code block was run on SQLite 3.45 for syntax and results only. Not verified against a live Oracle or PostgreSQL server: the date-function rows (standard vendor behaviour, listed from experience; test on your version), the IN list limit of 1000, and the Oracle 12c start for FETCH FIRST. The Oracle BOOLEAN type was confirmed in the Release 23 data types list.