PostgreSQL IN Operator

Learn how to use the PostgreSQL IN operator to match a value against a list or subquery result, when to prefer IN over OR, and how NOT IN behaves with NULL values.

6 min read · PostgreSQL 9.0+ · Back to overview

Quick Answer

The IN operator returns TRUE when a value matches any item in a list or subquery result. It is a concise and optimizer-friendly replacement for chaining multiple OR equality conditions. Use NOT IN to exclude matches, but avoid it when the list might contain NULL values.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The IN operator in PostgreSQL tests whether a value matches any item in a list or the result of a subquery. It produces cleaner, more readable SQL than chaining multiple OR equality conditions and the query planner can often execute it more efficiently.

Syntax

-- List form
value IN (value1, value2, ...)

-- Subquery form
value IN (SELECT column FROM table WHERE condition)

-- Negation
value NOT IN (value1, value2, ...)

IN is equivalent to:

value = value1 OR value = value2 OR ...

NOT IN is equivalent to:

value <> value1 AND value <> value2 AND ...

Practical Example

Create a sample schema for a project tracking system:

CREATE TABLE departments (
  dept_id   SERIAL PRIMARY KEY,
  name      VARCHAR(80) NOT NULL,
  is_active BOOLEAN     NOT NULL DEFAULT TRUE
);

CREATE TABLE projects (
  project_id  SERIAL PRIMARY KEY,
  title       VARCHAR(150) NOT NULL,
  dept_id     INT REFERENCES departments (dept_id),
  priority    VARCHAR(10)  NOT NULL DEFAULT 'medium',
  budget_usd  NUMERIC(12,2),
  status      VARCHAR(20)  NOT NULL DEFAULT 'open'
);

INSERT INTO departments (name, is_active) VALUES
  ('Engineering',  TRUE),
  ('Marketing',    TRUE),
  ('Operations',   FALSE),
  ('Data Science', TRUE);

INSERT INTO projects (title, dept_id, priority, budget_usd, status) VALUES
  ('API Redesign',          1, 'high',   85000.00, 'open'),
  ('Brand Refresh',         2, 'medium', 40000.00, 'open'),
  ('Server Migration',      3, 'low',    22000.00, 'open'),
  ('ML Pipeline',           4, 'high',   120000.00,'open'),
  ('Analytics Dashboard',   4, 'medium', 35000.00, 'closed'),
  ('Mobile App',            1, 'high',   95000.00, 'open'),
  ('SEO Campaign',          2, 'low',    18000.00, 'closed');

Find open projects with high or medium priority:

SELECT title, priority, budget_usd
FROM projects
WHERE status = 'open'
  AND priority IN ('high', 'medium')
ORDER BY budget_usd DESC;

Expected result:

       title        | priority | budget_usd
--------------------+----------+------------
 ML Pipeline        | high     |  120000.00
 Mobile App         | high     |   95000.00
 API Redesign       | high     |   85000.00
 Brand Refresh      | medium   |   40000.00

Find projects belonging to active departments using a subquery:

SELECT p.title, d.name AS department
FROM projects p
JOIN departments d ON d.dept_id = p.dept_id
WHERE p.dept_id IN (
  SELECT dept_id FROM departments WHERE is_active = TRUE
)
ORDER BY d.name, p.title;

Verify the index is used with EXPLAIN ANALYZE:

EXPLAIN ANALYZE
SELECT title FROM projects
WHERE priority IN ('high', 'medium');

NOT IN and NULL Pitfall

-- Safe: all IDs are non-NULL
SELECT title FROM projects
WHERE dept_id NOT IN (3, 4);

-- Dangerous: if the subquery returns any NULL, zero rows come back
SELECT title FROM projects
WHERE dept_id NOT IN (SELECT dept_id FROM departments WHERE is_active = FALSE);

The subquery could return NULL if dept_id is nullable. Fix it with a NOT EXISTS alternative:

-- Safe alternative using NOT EXISTS
SELECT p.title
FROM projects p
WHERE NOT EXISTS (
  SELECT 1 FROM departments d
  WHERE d.dept_id = p.dept_id
    AND d.is_active = FALSE
);

IN with Large Lists

For very large sets of values (hundreds or thousands), a temporary table joined with INNER JOIN is more efficient than a long IN list:

-- Create a temp list
CREATE TEMP TABLE target_depts AS VALUES (1), (2), (4);

-- Join instead of IN list
SELECT p.title
FROM projects p
JOIN target_depts t ON t.column1 = p.dept_id;

Testing with Vela

IN with subqueries is common in data migration scripts and bulk update operations. Before running a large UPDATE ... WHERE id IN (SELECT ...) in production, use Vela’s database branching to validate the subquery returns the expected set of IDs. Clone your production database to a branch, run the SELECT form to inspect affected rows, then apply the UPDATE to the branch first to confirm the results before promoting the change to production.

Production Tips

  • Replace same-column OR chains with IN — it is more readable and the planner handles it as a hash lookup for small lists.
  • Always index the column tested with IN. The planner can use a bitmap index scan when the list has a reasonable number of values.
  • Avoid NOT IN when the list or subquery might include NULL values — use NOT EXISTS instead.
  • For IN (subquery), compare performance against EXISTS and INNER JOIN alternatives with EXPLAIN ANALYZE. The planner often transforms IN (subquery) into a semi-join internally, but explicit EXISTS can give the planner more flexibility.
  • Large IN lists (thousands of values) increase query parsing and planning time. Prefer a temporary table or ANY(ARRAY[...]) for large dynamic sets.

Continue in Filtering Data: BETWEEN.

Related in this section: PostgreSQL WHERE · AND Operator · OR Operator

Frequently Asked Questions

What does the IN operator do in PostgreSQL?
IN checks whether a value matches any item in a specified list and returns TRUE if a match is found, FALSE if no match is found. It is equivalent to value = v1 OR value = v2 OR ..., but is more readable and can be better optimized by the query planner.
Does IN lock the table in PostgreSQL?
No. IN is a predicate operator used at query evaluation time. It acquires no table locks beyond the standard shared read lock used by any SELECT statement. Write operations that use IN in a WHERE clause acquire the same locks they would without it.
Why should I avoid NOT IN when the list might contain NULL?
If any value in a NOT IN list is NULL, the entire expression evaluates to NULL (not TRUE) for every row, returning zero rows. This is because NULL comparisons always yield NULL in SQL's three-valued logic. Use NOT EXISTS or add an IS NOT NULL filter to the subquery when NULL values are possible.
Can I use IN with a subquery in PostgreSQL?
Yes. IN accepts a subquery that returns a single column: WHERE dept_id IN (SELECT dept_id FROM departments WHERE active = TRUE). For large subquery results, an EXISTS predicate or an INNER JOIN often performs better because the planner can optimize them more flexibly.
What is the safest way to use IN with a dynamic list in production?
For very large dynamic sets (thousands of values), use a temporary table joined with INNER JOIN instead of an IN list. Large IN lists can bloat query plans and slow parsing. Always index the column used with IN, and use EXPLAIN ANALYZE to confirm the planner uses the index rather than falling back to a sequential scan.