Error Message
ERROR: syntax error at or near "FROM"
LINE 1: SELECT * FORM users
^
SQLSTATE: 42601The error always points at the first token PostgreSQL could not parse, with a caret (^) under the offending position. The actual mistake is often one or two tokens earlier — Postgres only notices the problem when it hits the next thing it can't make sense of.
What Triggers This Error
42601 is a parse-time error. It fires before the planner ever sees your query, so it never depends on table contents or permissions. The most common triggers:
- Misspelled keyword —
FORMinstead ofFROM,SLECTinstead ofSELECT,WEREinstead ofWHERE - Reserved word used as identifier without quotes — column named
user,order,group,table,select - Missing or misplaced punctuation — trailing comma before
FROM, missing comma between columns, unclosed parenthesis - Mixed quote styles — using
"(identifier quotes) where'(string literals) is required - Postgres-specific syntax in MySQL/SQL Server style —
LIMIT 10, 20(MySQL pagination),TOP 10(SQL Server), backticks for identifiers - Version-mismatch features —
MERGEon PostgreSQL 14 or earlier,NULLS NOT DISTINCTon PostgreSQL 14 or earlier - Dollar-quoted strings with unmatched tags —
$func$ ... $func2$inside a function body - Stray semicolon inside a
CASE, function, orDOblock — closing the statement early
Fix by Scenario
Misspelled keyword
The error caret usually points at the next token, not the typo itself. Read the error in reverse: the keyword before the caret position is where the error usually is.
SELECT * FORM users WHERE id = 1;
-- ^^^^ caret will be at "users" because Postgres expected `FROM` and got `FORM`Fix: correct the keyword. If your SQL is generated, log the final query string before sending — most "phantom" 42601 errors come from string concatenation that drops a space or adds an extra one.
Reserved word as identifier
PostgreSQL has a list of reserved keywords. If you use one as a column or table name without double-quoting it, you get 42601.
-- fails
CREATE TABLE order (id BIGINT, total NUMERIC);
-- ERROR: syntax error at or near "order"
-- works (but you'll have to quote it forever after)
CREATE TABLE "order" (id BIGINT, total NUMERIC);
-- best: rename the table
CREATE TABLE orders (id BIGINT, total NUMERIC);Common offenders: user, order, group, table, select, from, where, case, when, then, end, array, cast, current_user, session_user. Renaming the column is almost always cheaper than living with the quoting forever.
Trailing comma or missing comma
-- trailing comma before FROM
SELECT id, name, email, FROM users;
-- ERROR: syntax error at or near "FROM"
-- missing comma between columns
SELECT id name email FROM users;
-- ERROR: syntax error at or near "name"These are mostly editor-paste artifacts. A SQL formatter (e.g. pg_format, sqlfluff) catches both before they reach the database.
Mixed quote styles
PostgreSQL uses single quotes for string literals and double quotes for identifiers. They are not interchangeable — and they have opposite meanings from MySQL where backticks quote identifiers and either kind of quote works for strings.
-- fails: "alice" is interpreted as a column name
SELECT * FROM users WHERE name = "alice";
-- ERROR: column "alice" does not exist (sometimes 42601, sometimes 42703)
-- works
SELECT * FROM users WHERE name = 'alice';If you migrated SQL from MySQL, search for backticks (`) and double-quoted string-like values — both are 42601 magnets.
MySQL or SQL Server syntax used in Postgres
PostgreSQL does not understand:
| MySQL / SQL Server | PostgreSQL equivalent |
|---|---|
LIMIT 10, 20 (offset, count) | LIMIT 20 OFFSET 10 |
SELECT TOP 10 ... | SELECT ... LIMIT 10 |
SELECT `id` FROM `users` | SELECT id FROM users (no backticks) |
IFNULL(x, y) | COALESCE(x, y) |
DATETIME column type | TIMESTAMP |
AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY or SERIAL |
If a query was copy-pasted from a different dialect, the 42601 caret will point at the first incompatible token.
Version-mismatch features
Some SQL features are only valid in newer PostgreSQL versions:
-- ERROR on PostgreSQL 14 or earlier — MERGE is PostgreSQL 15+
MERGE INTO target USING source ON ...
-- ERROR on PostgreSQL 14 or earlier — NULLS NOT DISTINCT is 15+
CREATE UNIQUE INDEX ON t (col) NULLS NOT DISTINCT;Check your server version before assuming a syntax error is a typo:
SELECT version();
SHOW server_version_num; -- 150000 means 15.0, 140000 means 14.0If you must support an older version, rewrite the statement (INSERT ... ON CONFLICT instead of MERGE, partial unique index instead of NULLS NOT DISTINCT).
Dollar-quoted strings with mismatched tags
PostgreSQL allows custom tags inside dollar quotes to safely embed code that contains $$:
-- works: matching tags
CREATE FUNCTION greet() RETURNS text AS $body$
BEGIN
RETURN 'hello';
END;
$body$ LANGUAGE plpgsql;
-- fails: mismatched tags
CREATE FUNCTION greet() RETURNS text AS $body$
BEGIN
RETURN 'hello';
END;
$func$ LANGUAGE plpgsql;
-- ERROR: syntax error at or near "$func$"When generating function bodies programmatically, pick a unique tag string and use it consistently for the open and close.
Stray semicolon inside a function or DO block
A DO block, anonymous block, or function body is delimited by dollar quotes — semicolons inside don't end the outer statement, but if you forget the closing dollar quote, the parser sees the semicolon as the end and fails on the next token.
-- fails: missing closing $$
DO $$
BEGIN
RAISE NOTICE 'hello';
END
LANGUAGE plpgsql;
-- ERROR: syntax error at or near "LANGUAGE"Add the closing dollar quote and the language clause becomes a separate (correct) statement:
DO $$
BEGIN
RAISE NOTICE 'hello';
END
$$ LANGUAGE plpgsql;Generated SQL with empty bind values
ORMs and query builders sometimes emit invalid SQL when a list parameter is empty:
-- generated when ids = []
SELECT * FROM users WHERE id IN ();
-- ERROR: syntax error at or near ")"Fix at the application layer — short-circuit the query when the list is empty, or use a sentinel:
SELECT * FROM users WHERE id IN (NULL); -- always returns empty, but parsesBetter: the application should not issue a query at all when the input list is empty.
Prevention
- Run SQL through a formatter or linter (
pg_format,sqlfluff) in CI — most 42601 errors are caught at the formatter stage - Quote any identifier that would otherwise be a reserved word, or rename the column to something safe
- When porting SQL from another database, search for
`,LIMIT n, m,TOP, andIFNULLpatterns first - For dynamic SQL, log the final query string before execution — guessing at the failing query from production logs is the slowest way to debug 42601
- When using new syntax (
MERGE,NULLS NOT DISTINCT, etc.), check the minimum supported PostgreSQL version in your fleet, not just your dev machine - Configure ORMs and query builders to short-circuit empty
INlists rather than emittingIN ()
Bytebase's SQL Review parses every change against your target PostgreSQL version before it runs, catching version-mismatch syntax (e.g. MERGE on PG 14) and reserved-word collisions during change review rather than at deploy time. See also ERROR 42703: Undefined Column for the closely-related "column does not exist" failure.