Index types, creation, and optimization strategies for CockroachDB
Indexes are critical database structures that improve query performance by allowing fast data retrieval. This page covers index types, creation syntax, and best practices for CockroachDB.
An index maintains an ordered copy of a subset of table data, organized by one or more columns. CockroachDB automatically creates an index on the primary key and supports several additional index types.
Secondary indexes are created on non-primary key columns to speed up queries.
Single Column
Composite
Descending
-- Anonymous indexCREATE INDEX ON users (email);-- Named indexCREATE INDEX users_email_idx ON users (email);-- Unique indexCREATE UNIQUE INDEX users_username_idx ON users (username);
-- Multi-column indexCREATE INDEX users_name_idx ON users (last_name, first_name);-- Ordering matters for queriesCREATE INDEX orders_user_date_idx ON orders (user_id, created_at DESC);
Composite indexes work left-to-right. An index on (a, b, c) can be used for queries on a, (a, b), or (a, b, c), but not b or c alone.
-- Descending order (useful for time-series queries)CREATE INDEX logs_timestamp_idx ON logs (created_at DESC);-- Mixed directionsCREATE INDEX scores_idx ON game_scores (game_id ASC, score DESC);
Inverted indexes enable efficient queries on JSONB, ARRAY, and spatial data types.
JSONB
Arrays
Geography
CREATE TABLE documents ( id INT PRIMARY KEY, metadata JSONB);-- Create inverted indexCREATE INVERTED INDEX ON documents (metadata);-- Query using containmentSELECT * FROM documents WHERE metadata @> '{"status": "active"}';-- Query using existenceSELECT * FROM documents WHERE metadata ? 'user_id';
CREATE TABLE articles ( id INT PRIMARY KEY, tags TEXT[]);CREATE INVERTED INDEX ON articles (tags);-- Find articles with specific tagSELECT * FROM articles WHERE tags @> ARRAY['database'];-- Find articles with any of these tagsSELECT * FROM articles WHERE tags && ARRAY['sql', 'nosql'];
CREATE TABLE locations ( id INT PRIMARY KEY, point GEOGRAPHY);CREATE INVERTED INDEX ON locations (point);-- Spatial queriesSELECT * FROM locations WHERE ST_DWithin( point, 'POINT(-122.4194 37.7749)'::GEOGRAPHY, 1000);
Partial indexes include only rows matching a WHERE condition, reducing storage and improving performance.
-- Index only active usersCREATE INDEX active_users_email_idx ON users (email) WHERE status = 'active';-- Index recent ordersCREATE INDEX recent_orders_idx ON orders (user_id, created_at) WHERE created_at > '2026-01-01';-- Index non-null valuesCREATE INDEX users_phone_idx ON users (phone) WHERE phone IS NOT NULL;-- Query using partial indexSELECT * FROM users WHERE email = '[email protected]' AND status = 'active';
The query’s WHERE clause must match or be more restrictive than the index predicate for the partial index to be used.
Covering indexes store additional columns to avoid table lookups.
-- Index with STORING clauseCREATE INDEX orders_user_idx ON orders (user_id) STORING (total, created_at);-- This query can be satisfied entirely from the indexSELECT user_id, total, created_at FROM orders WHERE user_id = 123;
-- Index on lowercase email for case-insensitive searchCREATE INDEX users_email_lower_idx ON users (LOWER(email));SELECT * FROM users WHERE LOWER(email) = '[email protected]';-- Index on extracted JSON fieldCREATE INDEX metadata_status_idx ON documents ((metadata->>'status'));SELECT * FROM documents WHERE metadata->>'status' = 'active';
-- Drop single indexDROP INDEX users_email_idx;-- Conditional dropDROP INDEX IF EXISTS temp_idx;-- Drop with cascadeDROP INDEX users_email_idx CASCADE;-- Drop concurrently (non-blocking)DROP INDEX CONCURRENTLY users@users_email_idx;
Columns with high selectivity (many distinct values)
Foreign key columns
-- Good: Frequently queried columnCREATE INDEX ON orders (user_id);-- Good: JOIN optimizationCREATE INDEX ON order_items (order_id);-- Good: Sorting optimizationCREATE INDEX ON logs (created_at DESC);
Columns with low selectivity (few distinct values)
Small tables (full scan is faster)
Columns that are rarely queried
Tables with high write:read ratio
-- Poor: Boolean column (low selectivity)-- Don't create: CREATE INDEX ON users (is_active);-- Better: Use partial index if neededCREATE INDEX ON users (email) WHERE is_active = true;
-- See if index is usedEXPLAIN SELECT * FROM users WHERE email = '[email protected]';-- Analyze actual executionEXPLAIN ANALYZE SELECT * FROM users WHERE created_at > '2026-01-01'ORDER BY created_at DESCLIMIT 10;
-- For index on (last_name, first_name)-- Index keys are encoded as:-- /Table/users/index_id/<last_name>/<first_name>/<primary_key>CREATE INDEX users_name_idx ON users (last_name, first_name);
-- Configure index for specific regionALTER INDEX users_email_idx CONFIGURE ZONE USING num_replicas = 3, constraints = '{+region=us-east}';-- Create index with regional survivabilityCREATE INDEX users_regional_idx ON users (region, user_id)STORING (email, name);
-- For similarity search (requires vector extension)CREATE TABLE embeddings ( id INT PRIMARY KEY, vector FLOAT8[]);-- Create vector index for nearest neighbor searchCREATE VECTOR INDEX ON embeddings (vector);