The PostgreSQL "syntax error at or near UNION" occurs when a UNION query is malformed, typically due to ORDER BY or LIMIT clauses appearing before UNION, mismatched column counts, incompatible data types, or missing parentheses around subqueries. Fixing requires understanding proper UNION syntax and ensuring all SELECT statements match in structure.
PostgreSQL raises a syntax error when the UNION operator is not used correctly in a query. UNION combines result sets from multiple SELECT statements, but they must follow strict syntactic rules: all SELECT statements must have the same number of columns with compatible data types, ORDER BY and LIMIT must apply to the entire result set (not individual SELECT statements), and parentheses may be required to disambiguate the query structure. The error "syntax error at or near 'UNION'" typically appears during query parsing, before execution. PostgreSQL's parser encounters the UNION keyword in an unexpected position or context, preventing the query from being compiled. This is different from runtime errors that occur after parsing succeeds but during execution (like type mismatches). Common scenarios include: placing ORDER BY or LIMIT before UNION instead of after, wrapping a single SELECT with parentheses but not all of them in a multi-part UNION, using WITH (CTE) clauses without proper structure, or with query builders that generate malformed SQL.
The most common cause of this error is placing ORDER BY or LIMIT on an individual SELECT before combining with UNION.
-- INCORRECT: ORDER BY before UNION
SELECT id, name FROM users ORDER BY id
UNION
SELECT id, name FROM archived_users;
-- CORRECT: ORDER BY and LIMIT after UNION
SELECT id, name FROM users
UNION
SELECT id, name FROM archived_users
ORDER BY id
LIMIT 10;If you need to ORDER BY or LIMIT individual subqueries before combining them, wrap each in parentheses:
(SELECT id, name FROM users ORDER BY id LIMIT 5)
UNION
(SELECT id, name FROM archived_users ORDER BY id LIMIT 5)
ORDER BY id;When using parentheses, ensure all SELECT statements in the UNION are consistently parenthesized, especially with LIMIT, ORDER BY, or CTEs.
```sql
-- INCORRECT: Only first is wrapped
(SELECT id, name FROM users LIMIT 10)
UNION
SELECT id, name FROM archived_users;
-- CORRECT: Both wrapped consistently
(SELECT id, name FROM users LIMIT 10)
UNION
(SELECT id, name FROM archived_users LIMIT 10);
-- Also correct: Neither wrapped (when no LIMIT/ORDER BY per subquery)
SELECT id, name FROM users
UNION
SELECT id, name FROM archived_users;
All SELECT statements in a UNION must return the same number of columns in the same order.
```sql
-- INCORRECT: Different number of columns
SELECT id, name, email FROM users
UNION
SELECT id, name FROM archived_users; -- Only 2 columns
-- CORRECT: Same columns in same order
SELECT id, name, email FROM users
UNION
SELECT id, name, email FROM archived_users;
-- Or add NULL placeholders for missing columns:
SELECT id, name, email FROM users
UNION
SELECT id, name, NULL as email FROM archived_users;
PostgreSQL requires compatible data types across UNION columns. While type coercion may occur, mismatches can cause errors.
```sql
-- INCORRECT: Incompatible types (int vs text)
SELECT id::int, name FROM users
UNION
SELECT code::text, description FROM error_codes;
-- CORRECT: Explicit casting to ensure compatible types
SELECT id::text as code, name as description FROM users
UNION
SELECT code::text, description::text FROM error_codes;
-- Check result types with EXPLAIN:
EXPLAIN SELECT id, name FROM users UNION SELECT id, name FROM archived_users;
Common table expressions (WITH clauses) combined with UNION can cause syntax errors if not properly structured.
```sql
-- INCORRECT: WITH might not apply to second SELECT
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
)
SELECT * FROM active_users
UNION
SELECT id, name FROM archived_users; -- archived_users not in CTE scope
-- CORRECT: Include all referenced tables
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
),
archived AS (
SELECT id, name FROM archived_users
)
SELECT * FROM active_users
UNION
SELECT * FROM archived;
Query builders (Knex, SQLAlchemy, ORMs) may generate malformed UNION queries. Always validate the generated SQL.
// Example: Knex.js - ensure UNION is called correctly
const query = knex
.select('id', 'name')
.from('users')
.union([
knex.select('id', 'name').from('archived_users')
])
.orderBy('id');
console.log(query.toString()); // Print SQL to verify it's correctIf the generated SQL looks wrong, consult the query builder's UNION documentation or switch to raw SQL if the builder is not flexible enough.
UNION removes duplicate rows (expensive operation), while UNION ALL keeps all rows (fast). If you don't need deduplication, use UNION ALL.
-- UNION: removes duplicates (sorts results internally)
SELECT id, name FROM users
UNION
SELECT id, name FROM archived_users; -- Slower
-- UNION ALL: keeps all rows (no sorting)
SELECT id, name FROM users
UNION ALL
SELECT id, name FROM archived_users; -- FasterAlso check PostgreSQL documentation on query optimization: use EXPLAIN ANALYZE to see if UNION is the bottleneck.
For complex UNION queries, use subqueries to improve readability and avoid syntax errors. Wrap the entire UNION expression in parentheses and apply ORDER BY at the outermost level:
(
SELECT id, name, 'active' as status FROM users WHERE status = 'active'
UNION ALL
SELECT id, name, 'archived' FROM archived_users
)
ORDER BY name
LIMIT 100;When working with nested UNIONs (UNION of UNION results), ensure type resolution is unambiguous. PostgreSQL resolves types left-to-right, so if you have SELECT NULL UNION SELECT NULL UNION SELECT 1, the middle UNION outputs text type, then tries to combine text with integer on the outer UNION. Explicitly cast NULL to the desired type to avoid this:
SELECT NULL::int
UNION
SELECT NULL::int
UNION
SELECT 1;For performance-critical queries, consider UNION ALL over UNION to avoid the costly sort/deduplication step. If you need deduplication, use DISTINCT on a subquery instead:
SELECT DISTINCT id, name FROM (
SELECT id, name FROM users
UNION ALL
SELECT id, name FROM archived_users
) combined;Use PostgreSQL's EXPLAIN and EXPLAIN ANALYZE to inspect the query plan and verify that UNION is being executed efficiently. Watch for sequential scans or external sorts that indicate missing indexes or table statistics issues.
ERROR: syntax error at end of input
Syntax error at end of input in PostgreSQL
Bind message supplies N parameters but prepared statement requires M
Bind message supplies N parameters but prepared statement requires M in PostgreSQL
Multidimensional arrays must have sub-arrays with matching dimensions
Multidimensional arrays must have sub-arrays with matching dimensions
ERROR: value too long for type character varying
Value too long for type character varying
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL