COUNT for existence check
Severity: Info
Using COUNT(*) > 0 or COUNT(*) >= 1 to check if rows exist forces PostgreSQL to scan and count all matching rows, then compare the total. EXISTS short-circuits after finding the first match, which can be orders of magnitude faster on large tables.
Example
Section titled “Example”-- Slow: counts all matching rows, then checks if the count is positiveSELECT id FROM usersWHERE (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) > 0;
-- Fast: stops scanning after the first matchSELECT id FROM usersWHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id);Why it matters
Section titled “Why it matters”COUNT(*) must scan every qualifying row to produce a total. If a customer has 10,000 orders, PostgreSQL reads all 10,000 rows just to confirm “at least one exists.” EXISTS returns true as soon as it finds the first matching row, skipping the remaining 9,999.
The difference is most pronounced when:
- The subquery table is large.
- Many rows match the condition.
- The query runs frequently (e.g. in a loop or dashboard).
Common patterns
Section titled “Common patterns”This nudge fires on several equivalent forms:
-- All of these check existence using COUNTWHERE (SELECT COUNT(*) FROM t) > 0WHERE (SELECT COUNT(1) FROM t) >= 1WHERE (SELECT COUNT(*) FROM t) <> 0
-- In CASE expressionsSELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM t;
-- In HAVING clausesSELECT status FROM orders GROUP BY status HAVING COUNT(*) > 0;How to fix it
Section titled “How to fix it”Replace the count comparison with EXISTS:
-- BeforeSELECT id FROM usersWHERE (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) > 0;
-- AfterSELECT id FROM usersWHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id);For CASE WHEN COUNT(*) > 0 patterns, restructure the query:
-- BeforeSELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false ENDFROM orders WHERE customer_id = 123;
-- AfterSELECT EXISTS (SELECT 1 FROM orders WHERE customer_id = 123);Detection
Section titled “Detection”Query Doctor flags A_Expr comparisons where one side is a COUNT(*) or COUNT(1) expression (either directly or inside a scalar subquery) and the other side is 0 or 1, with an operator that makes this an existence check (>, >=, !=, <>).