SQL Syntax

Every SQL statement is built from the same small set of ingredients: keywords, identifiers, literals, operators, and punctuation, arranged in a predictable shape. ‘SQL syntax’ is simply the set of rules that says which keywords are required, which are optional, and in what order they must appear. Once you know this shape, you can read a query you’ve never seen before and understand what it does, and you can write new queries with confidence instead of guessing. This lesson walks through that shape from the ground up, including how the database actually processes what you write.

Overview: How SQL Syntax Works

SQL statements fall into a few broad families: DDL (Data Definition Language, e.g. CREATE, ALTER, DROP) which defines structure, DML (Data Manipulation Language, e.g. INSERT, UPDATE, DELETE) which changes data, DQL (Data Query Language, essentially SELECT) which reads data, and DCL/TCL statements that handle permissions and transactions. All of them share the same underlying grammar rules.

When you submit a statement, the database engine doesn’t just read it top to bottom and execute each line as it appears. It first parses your text into a syntax tree, checking that keywords, identifiers, and punctuation appear in a legal arrangement. It then binds that tree against the schema catalog, verifying that every table and column you referenced actually exists and that data types are compatible. Only after both of those checks succeed does the engine build an execution plan and run it, using indexes and statistics to decide the fastest way to get your result. This is why a query can look correct at a glance but still fail: a misplaced clause fails parsing, a misspelled column fails binding, and only a query that passes both stages ever reaches execution.

SQL is also mostly whitespace-insensitive and free-form. You can write an entire statement on one line or spread it across twenty, and the engine treats it identically, because whitespace and line breaks only separate tokens, they carry no meaning of their own. What does matter is the order of clauses, the correct use of quotes, and terminating each statement properly.

Syntax

The general shape of a SELECT statement, the one you will write most often, looks like this:

SELECT column1, column2, ...
FROM table_name
WHERE condition
GROUP BY column1, column2, ...
HAVING group_condition
ORDER BY column1 [ASC|DESC]
LIMIT number;

Each clause has a fixed job and a fixed position relative to the others:

Clause Required? Purpose
SELECT Yes Lists the columns or expressions to return
FROM No (but usually present) Names the table(s) or joined tables to read from
WHERE No Filters individual rows before grouping
GROUP BY No Collapses rows into groups for aggregation
HAVING No Filters groups, typically using aggregate functions
ORDER BY No Sorts the final result set
LIMIT No Caps the number of rows returned

Clauses you include must appear in this order in the text of the statement — you cannot write ORDER BY before WHERE, for example, even though logically the engine evaluates them in a different sequence internally (more on that below).

Statement Terminator

A semicolon (;) marks the end of a statement. Most tools let you omit it for a single statement typed interactively, but when a script contains multiple statements, the semicolon is what tells the engine where one ends and the next begins. Always terminate your statements with ; so scripts run reliably across tools and engines.

Case Sensitivity

SQL keywords (SELECT, FROM, WHERE, and so on) are not case-sensitive — select, Select, and SELECT all work identically. The strong convention, though, is to write keywords in UPPERCASE and identifiers (table and column names) in lowercase, purely for readability. Identifier case sensitivity varies by engine: SQLite and SQL Server treat unquoted identifiers as case-insensitive, while PostgreSQL folds unquoted identifiers to lowercase and MySQL’s behavior depends on the operating system. String data comparisons (WHERE name = 'bob' vs 'Bob') are case-sensitive by default in most engines, SQLite included, unless you use a case-insensitive collation or a function like LOWER().

Quoting Identifiers and String Literals

String literals always use single quotes: 'Engineering'. Double quotes, square brackets, or backticks are used for identifiers (table/column names), and only when needed, such as when a name contains a space or matches a reserved word. SQLite accepts double-quoted identifiers (\"order\"), MySQL traditionally uses backticks (`order`), and SQL Server uses square brackets ([order]). Mixing these up — using double quotes where a string literal is meant — is one of the most common beginner mistakes, covered below.

Comments

Use -- for a single-line comment and /* ... */ for a block comment spanning multiple lines. Comments are ignored entirely by the parser and are useful for explaining why a query is written a particular way.

Examples

Example 1: Basic SELECT / WHERE / ORDER BY

CREATE TABLE students (id INTEGER, name TEXT, grade INTEGER, score REAL);

INSERT INTO students (id, name, grade, score) VALUES
  (1, 'Ava', 10, 92.5),
  (2, 'Ben', 11, 78.0),
  (3, 'Cara', 10, 88.0),
  (4, 'Dan', 12, 95.0);

SELECT name, score
FROM students
WHERE grade = 10
ORDER BY score DESC;

Result:

name score
Ava 92.5
Cara 88.0

The engine first finds every row in students where grade = 10 (Ava and Cara), then sorts just those rows by score from highest to lowest. Ben and Dan are excluded before sorting ever happens, because WHERE is applied before ORDER BY.

Example 2: Aggregation with GROUP BY and HAVING

CREATE TABLE orders2 (order_id INTEGER, customer TEXT, amount REAL);

INSERT INTO orders2 (order_id, customer, amount) VALUES
  (1, 'Alice', 150),
  (2, 'Bob', 40),
  (3, 'Alice', 50),
  (4, 'Carol', 300),
  (5, 'Bob', 30);

SELECT customer, SUM(amount) AS total, COUNT(*) AS num_orders
FROM orders2
GROUP BY customer
HAVING SUM(amount) > 100
ORDER BY total DESC;

Result:

customer total num_orders
Carol 300 1
Alice 200 2

Rows are collapsed into one group per customer (Alice: 150+50=200, Bob: 40+30=70, Carol: 300), then HAVING SUM(amount) > 100 drops Bob’s group entirely, and finally the surviving groups are sorted by their total, largest first. Notice HAVING filters on the aggregate SUM(amount), something WHERE cannot do because WHERE runs before grouping exists.

Example 3: Full clause order with LIMIT

CREATE TABLE sales (id INTEGER, region TEXT, product TEXT, amount REAL);

INSERT INTO sales (id, region, product, amount) VALUES
  (1, 'East', 'Widget', 100),
  (2, 'East', 'Gadget', 150),
  (3, 'West', 'Widget', 200),
  (4, 'West', 'Gadget', 50),
  (5, 'East', 'Widget', 120),
  (6, 'West', 'Widget', 80);

SELECT region, SUM(amount) AS total_sales
FROM sales
WHERE amount > 60
GROUP BY region
HAVING SUM(amount) > 150
ORDER BY total_sales DESC
LIMIT 1;

Result:

region total_sales
East 370

Row 4 (West, Gadget, 50) is dropped first by WHERE amount > 60. The rest are grouped by region: East totals 100+150+120=370, West totals 200+80=280. Both groups pass HAVING SUM(amount) > 150, but ORDER BY total_sales DESC puts East first, and LIMIT 1 keeps only that top row, so West never appears in the output even though its group also qualified.

How It Works Step by Step (Logical Query Processing Order)

This is the single most important thing to understand about SQL syntax: the order you write clauses is not the order the engine evaluates them. The logical processing order is fixed:

Step Clause What happens
1 FROM / JOIN Identify and combine the source table(s)
2 WHERE Filter individual rows
3 GROUP BY Collapse remaining rows into groups
4 HAVING Filter groups, usually by aggregate result
5 SELECT Compute the final column list and expressions
6 ORDER BY Sort the result rows
7 LIMIT Truncate to the requested number of rows

This explains several things that otherwise look like arbitrary rules. In Example 3, WHERE runs (step 2) before GROUP BY exists (step 3), which is why WHERE can never reference an aggregate like SUM(amount) — no groups exist yet. HAVING runs after grouping (step 4), so it can reference aggregates freely. A column alias defined in SELECT (step 5, like total_sales) generally cannot be used in a WHERE clause (step 2, which runs earlier), but it usually can be used in ORDER BY (step 6, which runs later) — SQLite and most engines allow this. Understanding this two-track model, written order versus logical order, is what lets you reason about why a query is legal or illegal instead of memorizing rules by rote.

Common Mistakes

Mistake 1: Writing clauses in the wrong order

SELECT name, salary
FROM employees
ORDER BY salary
WHERE department = 'Engineering';

Why it’s wrong: the written order of clauses is fixed by the grammar — WHERE must come before ORDER BY, not after. The parser rejects this before it ever gets near execution; it doesn’t matter that the intent is clear to a human reader.

CREATE TABLE staff (id INTEGER, name TEXT, department TEXT, salary REAL);

INSERT INTO staff (id, name, department, salary) VALUES
  (1, 'Nora', 'Engineering', 95000),
  (2, 'Omar', 'Sales', 62000),
  (3, 'Priya', 'Engineering', 88000);

SELECT name, salary
FROM staff
WHERE department = 'Engineering'
ORDER BY salary;

Result:

name salary
Priya 88000
Nora 95000

Moving WHERE ahead of ORDER BY fixes the syntax, and the result is the two Engineering employees sorted by salary, lowest first.

Mistake 2: Forgetting quotes around a string literal

SELECT *
FROM employees
WHERE department = Engineering;

Why it’s wrong: without single quotes, Engineering is not a string literal, it’s parsed as an identifier — the engine looks for a column named Engineering to compare against department. Since no such column exists, this fails with an error like ‘no such column: Engineering’. This is one of the most common beginner mistakes because the corrected version looks almost identical.

CREATE TABLE staff2 (id INTEGER, name TEXT, department TEXT);

INSERT INTO staff2 (id, name, department) VALUES
  (1, 'Nora', 'Engineering'),
  (2, 'Omar', 'Sales');

SELECT *
FROM staff2
WHERE department = 'Engineering';

Result:

id name department
1 Nora Engineering

Wrapping the value in single quotes tells the engine it’s comparing against literal text, and only Nora’s row, the one in Engineering, is returned.

A related trap worth knowing about: a column alias you define in SELECT (like AS total_sales) cannot be reused inside the WHERE clause of the same query, because WHERE is evaluated before SELECT in the logical order. Trying to filter with WHERE total_sales > 100 when total_sales is a SELECT-list alias will fail with an unknown-column error in most engines; use the underlying expression again, or a HAVING clause, instead.

Best Practices

  • Write SQL keywords in UPPERCASE and identifiers in lowercase so the two are visually distinct at a glance.
  • Always terminate statements with a semicolon, even when your tool doesn’t strictly require it — it prevents ambiguity in multi-statement scripts.
  • Use single quotes for string literals and reserve double quotes, brackets, or backticks strictly for identifiers that need escaping.
  • Put each major clause (SELECT, FROM, WHERE, GROUP BY, …) on its own line for readability, especially once a query has three or more clauses.
  • Avoid SELECT * in real applications; list the columns you actually need so the query is explicit and doesn’t break silently if the table gains new columns.
  • Remember the logical processing order (FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT) when a query behaves unexpectedly — most ‘why doesn’t this work’ questions trace back to this order.
  • Comment non-obvious business logic with -- so future readers (including you) understand intent, not just mechanics.
  • Use consistent, descriptive identifier names (snake_case is common) and avoid reserved words as table or column names to sidestep quoting headaches entirely.

Practice Exercises

Exercise 1: Using the students table from Example 1, write a query that returns the name and grade of every student with a score below 90, ordered by name alphabetically. (Hint: expect Ben and Cara in the result.)

Exercise 2: Using the orders2 table from Example 2, write a query that shows each customer’s average order amount, but only for customers with more than one order. (Hint: you’ll need GROUP BY, HAVING, and COUNT(*) or AVG().)

Exercise 3: Spot the syntax error in this statement and rewrite it correctly: SELECT region, amount FROM sales GROUP BY region WHERE amount > 100;. (Hint: think about the fixed written order of clauses covered in the Syntax section.)

Summary

  • Every SQL statement is built from keywords, identifiers, literals, and punctuation arranged in a fixed, required order.
  • The engine parses, then binds against the schema, then plans and executes — a query can fail at any of these stages for different reasons.
  • Keywords are case-insensitive; string literal comparisons are usually case-sensitive; identifier case rules vary by database engine.
  • String literals use single quotes; identifiers use double quotes, brackets, or backticks only when necessary.
  • The written order of clauses (SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT) differs from the logical processing order (FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT) — this explains most ‘why doesn’t my query work’ confusion.
  • Common mistakes are writing clauses out of order and forgetting to quote string literals, both of which produce clear engine errors once you know what to look for.
  • Always terminate statements with a semicolon and format queries consistently for readability and maintainability.