Partial / Filtered Indexes
Indexes that only include rows matching a WHERE condition, reducing index size and improving performance for queries on subsets.
partial index filtered index CREATE INDEX WHERE conditional index
index partial filtered WHERE performance storage
| Database | Status | Syntax Family | Min Version |
|---|---|---|---|
| PostgreSQL | full | partial-index | - |
| MySQL | none | - | - |
| MariaDB | none | - | - |
| Oracle | none | - | - |
| SQL Server | full | filtered-index | 2008 |
| SQLite | full | partial-index | 3.8.9 |
| Redshift | none | - | - |
| DB2 | workaround | - | - |
| Snowflake | none | - | - |
| BigQuery | none | - | - |
Compare Databases
Database Details
PostgreSQL
full
-- Index only active users (omits the much larger set of inactive users)
CREATE INDEX idx_active_users ON users(email)
WHERE is_active = true;
-- Unique partial index: enforce uniqueness only among active records
CREATE UNIQUE INDEX idx_unique_active_email ON users(email)
WHERE is_active = true;
-- Queue processing: index only unprocessed rows
CREATE INDEX idx_pending_jobs ON jobs(created_at)
WHERE status = 'pending';
-- Query uses the partial index automatically
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = true;
-- View partial indexes
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'users'
AND indexdef LIKE '%WHERE%';- WHERE clause must be IMMUTABLE.
- Queries must include a WHERE clause that implies the index predicate for the planner to pick it.
Docs: Partial Indexes, CREATE INDEX
MySQL
none
-- MySQL does not support partial or filtered indexes.
-- Workaround 1: index a generated column with conditional logic
ALTER TABLE users
ADD COLUMN active_email VARCHAR(255)
AS (IF(is_active = 1, email, NULL)) VIRTUAL,
ADD INDEX idx_active_email (active_email);
-- Queries can then use:
SELECT * FROM users WHERE active_email = 'alice@example.com';
-- Workaround 2: use a view (read-only)
CREATE VIEW active_users AS
SELECT * FROM users WHERE is_active = 1;
-- Workaround 3: partition the table by status and use partition pruning- No WHERE on CREATE INDEX. The usual substitute is an expression index or virtual generated column that returns NULL for excluded rows — NULLs aren't indexed.
MariaDB
none
-- MariaDB does not support partial indexes.
-- Same workaround as MySQL: index a virtual generated column
ALTER TABLE users
ADD COLUMN active_email VARCHAR(255)
AS (IF(is_active = 1, email, NULL)) VIRTUAL,
ADD INDEX idx_active_email (active_email);
SELECT * FROM users WHERE active_email = 'alice@example.com';- No WHERE on CREATE INDEX. Index a VIRTUAL generated column that returns NULL for excluded rows.
Oracle
none
-- Oracle does not support partial indexes with a WHERE predicate.
-- Workaround: function-based index using a CASE expression
-- Rows that should be excluded return NULL (not indexed)
CREATE INDEX idx_active_email
ON users (CASE WHEN is_active = 1 THEN email ELSE NULL END);
-- Query must match the expression exactly
SELECT * FROM users
WHERE CASE WHEN is_active = 1 THEN email ELSE NULL END = 'alice@example.com';
-- More natural query rewrite (optimizer may still use the index)
SELECT * FROM users
WHERE email = 'alice@example.com'
AND is_active = 1;- No WHERE on CREATE INDEX. Function-based index on CASE ... THEN col ELSE NULL END is the usual trick, since NULL values aren't indexed.
SQL Server
full
-- Filtered index on active users only
CREATE INDEX idx_active_users
ON users (email)
WHERE is_active = 1;
-- Unique filtered index: one active record per email
CREATE UNIQUE INDEX idx_unique_active_email
ON users (email)
WHERE is_active = 1;
-- Filtered index on a non-nullable sparse column
CREATE INDEX idx_pending_jobs
ON jobs (created_at)
WHERE status = 'pending';
-- Query automatically uses the filtered index
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = 1;
-- View filtered indexes
SELECT name, filter_definition
FROM sys.indexes
WHERE object_id = OBJECT_ID('users')
AND filter_definition IS NOT NULL;- Added in 2008. Filter predicate accepts =, <>, >, <, >=, <=, IS NULL, IS NOT NULL, IN, NOT IN, and AND — but not OR.
- The filter also produces filtered statistics, which are often more selective than full-table stats.
- Predicate is in sys.indexes.filter_definition.
SQLite
full
-- Partial index on active users only
CREATE INDEX idx_active_users
ON users (email)
WHERE is_active = 1;
-- Unique partial index
CREATE UNIQUE INDEX idx_unique_active_email
ON users (email)
WHERE is_active = 1;
-- Queue: index only pending jobs
CREATE INDEX idx_pending_jobs
ON jobs (created_at)
WHERE status = 'pending';
-- Query uses partial index automatically
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = 1;
-- Check index usage with EXPLAIN QUERY PLAN
EXPLAIN QUERY PLAN
SELECT * FROM users WHERE email = 'alice@example.com' AND is_active = 1;- Added in 3.8.9.
Docs: Partial Indexes, CREATE INDEX
Redshift
none
-- Redshift has no user-defined indexes.
-- Use sort keys for range-based access patterns:
CREATE TABLE orders (
id BIGINT,
status VARCHAR(20),
created_at TIMESTAMP
)
SORTKEY (status, created_at);
-- Zone maps (automatic min/max metadata per block) provide
-- some of the same benefits as partial indexes for range scans.
-- Use WHERE clauses that align with the sort key order.- No indexes. Columnar storage, zone maps, and SORTKEY handle most of the same query patterns.
Docs: Sort Keys
DB2
workaround
-- DB2 does not have a direct WHERE clause on CREATE INDEX.
-- Workaround: create an index on a generated column
ALTER TABLE users ADD COLUMN active_email
VARCHAR(255) GENERATED ALWAYS AS
(CASE WHEN is_active = 1 THEN email ELSE NULL END);
CREATE INDEX idx_active_email ON users(active_email);
-- Query using the generated column
SELECT * FROM users WHERE active_email = 'alice@example.com';
-- DB2 also supports indexes on expression values in some versions:
CREATE INDEX idx_expr ON users
(CASE WHEN is_active = 1 THEN email ELSE NULL END);- No WHERE on CREATE INDEX. Closest substitutes: an expression/generated-column index that excludes rows via NULL, or local indexes on a partitioned table.
Docs: CREATE INDEX, Generated Column
Snowflake
none
-- Snowflake has no user-defined indexes.
-- Cluster keys physically co-locate similar rows for micro-partition pruning:
ALTER TABLE orders CLUSTER BY (status, order_date);
-- Search Optimization Service can accelerate selective point lookups:
ALTER TABLE orders ADD SEARCH OPTIMIZATION;
-- Pruning happens automatically based on micro-partition metadata.
-- A query like this benefits from clustering on status:
SELECT * FROM orders WHERE status = 'pending' AND order_date > '2024-01-01';- No indexes. Pruning comes from automatic micro-partition min/max metadata; CLUSTER BY and Search Optimization cover tuned lookups.
BigQuery
none
-- BigQuery has no user-defined B-Tree or partial indexes.
-- Partitioned tables provide time-based pruning:
CREATE TABLE orders
PARTITION BY DATE(order_date)
AS SELECT * FROM staging.orders;
-- Clustered tables co-locate rows for pruning:
CREATE TABLE orders
PARTITION BY DATE(order_date)
CLUSTER BY status, customer_id
AS SELECT * FROM staging.orders;
-- Search Index for SEARCH() function (managed, not user-defined):
CREATE SEARCH INDEX idx ON orders (ALL COLUMNS);
-- Selective queries benefit from partition + cluster pruning:
SELECT * FROM orders
WHERE order_date = '2024-01-15'
AND status = 'pending';- No B-tree indexes. PARTITION BY and CLUSTER BY drive pruning; CREATE SEARCH INDEX feeds the SEARCH() function for substring lookups.