A query that errors is not random malice — it is the engine pointing at the exact spot it got lost. The mistake most beginners make is to delete the failing line and retype it differently, hoping. That scrambles the clue. The fix is to slow down, read the message, and classify the failure before changing anything.
SQL faults come in three families, and each needs a different response. A syntax error means the statement is malformed and the engine refuses to run it. A semantic error parses fine but names something that does not exist. A logic error runs and returns rows — just the wrong ones, silently. Learning to sort a failure into one of these three is the single skill that turns hours of flailing into minutes of deliberate repair.
flowchart TD
Q["Query fails or looks wrong"] --> C{"classify first"}
C -->|"will not parse"| S["Syntax<br/>malformed statement"]
C -->|"parses, then errors"| SE["Semantic<br/>bad name or type"]
C -->|"runs, wrong rows"| L["Logic<br/>correct SQL, wrong intent"]
style S fill:#b45309,color:#fff
style SE fill:#b45309,color:#fff
style L fill:#b45309,color:#fff
Syntax errors: the engine refuses to run
A syntax error stops the query before a single row is touched. The message usually names the token where the parser gave up, and the line and column are right next to it. The most common causes are a missing quote around text, an unbalanced parenthesis, a forgotten comma between columns, or a keyword used where a value was expected.
WHERE city = 'Oslo is the classic — the opening quote has no partner, so the engine reads the rest of the line as one runaway string and reports an unterminated literal. The fix is mechanical: close the quote. Resist the urge to rewrite the whole clause; the message has already told you where the break is, so go straight there and repair only that.
SELECT name FROM departments WHERE city = 'Oslo';
Semantic errors: valid grammar, missing target
A semantic error is sneakier. The statement is grammatically correct SQL, so the parser accepts it — then the engine tries to resolve a name and fails. no such column: departmetn_id is the signature: a typo in a column name that parses as an identifier but matches nothing. no such table, ambiguous column name, and type mismatches live in the same family.
The message is precise: it tells you exactly which name it could not resolve. The mistake is to assume the table is wrong; most of the time the name is simply misspelled. Copy the offending identifier from the error, check it against the schema character by character, and the typo usually jumps out. A quick SELECT * FROM table LIMIT 1 confirms the real column names before you retry.
SELECT id, name, department_id FROM employees WHERE salary > 900000;
flowchart LR
F["Failing query"] --> R["Reduce: fewer columns, fewer rows"]
R --> T{"still fails?"}
T -->|"yes"| R
T -->|"no"| K["the removed piece held the bug"]
style K fill:#0e7490,color:#fff
style R fill:#3776ab,color:#fff
Logic errors: the silent ones
A logic error is the most dangerous kind, because nothing complains. The query runs, returns rows, and looks plausible — only an expert eye, or a sanity check against known data, reveals it is wrong. A reversed comparison (a < where you meant >), a join on the wrong key, or a forgotten WHERE all produce confident, incorrect results.
The defense is verification. Before trusting an answer, ask whether its magnitude is plausible: should seven employees really earn above average, or does that number feel off? Add a COUNT(*) variant of the query to check the shape of the result, and compare a few specific rows you can reason about by hand. Logic errors survive every automated check, so they demand a human willing to doubt a result that ran cleanly.
SELECT COUNT(*) AS above_average_count FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);
flowchart TD
A["Query returns rows"] --> V{"verify the magnitude"}
V -->|"plausible"| T["trust, then spot-check one row by hand"]
V -->|"feels off"| D["add COUNT(*) and re-derive by hand"]
D --> E["find the wrong comparison or join"]
style E fill:#0e7490,color:#fff
This query has a syntax error — the city value's opening quote is never closed, so SQLite reads the rest of the line as a runaway string. Close the quote so the query returns the departments located in Oslo.
SELECT name FROM departments WHERE city = 'Oslo ;
The text literal needs a closing single quote after Oslo.
The full condition reads city = 'Oslo'.
SELECT name FROM departments WHERE city = 'Oslo';
This query runs without error but returns the wrong people. It is meant to list employees who earn more than the average salary, but the comparison points the wrong way. Reverse it so it returns the above-average earners.
SELECT name, salary FROM employees WHERE salary < (SELECT AVG(salary) FROM employees) ;
Flip the comparison from < to >.
The subquery that computes the average stays the same.
SELECT name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);
You run a query and get: no such column: departmetn_id. What kind of error is this?
The query parsed successfully, so it is not a syntax error. It failed at name resolution, which is a semantic error. The cause is a typo: department_id is misspelled as departmetn_id.
This query fails with no such column: departmetn_id — the column name is misspelled. Correct the spelling so it returns the names of employees in department 1.
SELECT name FROM employees WHERE departmetn_id = 1 ;
Compare the spelled name against the schema, character by character.
The real column is department_id.
SELECT name FROM employees WHERE department_id = 1;
Transfer task: combine what you know. Return the name of every project that is active and belongs to a department located in Oslo. This needs a join, a filter on the project status, and a filter on the department's city.
SELECT p.name FROM projects p -- join to departments, then filter status and city ;
Join projects p to departments d on p.department_id = d.id.
Filter with WHERE p.status = 'active' AND d.city = 'Oslo'.
SELECT p.name FROM projects p JOIN departments d ON p.department_id = d.id WHERE p.status = 'active' AND d.city = 'Oslo';
Recap
- Sort every failure into one of three families before touching code: syntax (will not parse), semantic (parses, bad name or type), logic (runs, wrong rows).
- Read the whole error message; the named token is often one step after the real mistake.
- For semantic errors, confirm real column names with a quick
SELECT * ... LIMIT 1. - Logic errors are silent — verify magnitudes with a
COUNT(*)and check rows you can reason about by hand. - Reduce a failing query to its smallest reproducing form to isolate the bug.
Next you will learn the most important defensive habit in all of SQL: stopping injection before it starts, with parameterized queries.