I. Overview
CockroachDB is powerful, but like any database, poor query patterns or schema design can bottleneck your application. In this final part of the series, we'll look at the tools and techniques for understanding and optimizing query performance.

II. EXPLAIN & EXPLAIN ANALYZE
The starting point for any performance investigation is EXPLAIN.
EXPLAIN — Shows the query execution plan without running the query:
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com'; tree | field | description
-------------+--------------------+------------------------
| distributed | false
| vectorized | true
index join | |
│ | table | users@primary
└── scan | |
| table | users@users_email_key
| spans | [/'alice@example.com']EXPLAIN ANALYZE — Actually runs the query and shows real execution stats:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';planning time: 1ms
execution time: 45ms
• filter
│ actual row count: 12
│ filter: status = 'pending'
│
└── • scan
actual row count: 8,432
KV rows read: 8,432 ← reading too many rows!
table: orders@orders_user_idxThe output above shows a problem: we're scanning 8,432 rows to return 12. That's a sign we need a better index.
III. Index Strategies
Basic Composite Index
-- Without index: full table scan on status
CREATE INDEX ON orders (user_id, status);
-- Now the query above scans only 12 rowsCovering Index (STORING)
Avoid going back to the primary index by storing needed columns in the index itself:
CREATE INDEX ON orders (user_id, status) STORING (total, created_at);
-- This query is served entirely from the index — no table lookup needed
SELECT total, created_at FROM orders WHERE user_id = 123 AND status = 'pending';Partial Index
Index only the rows that match a condition — smaller and faster:
-- Only index pending orders (much smaller than indexing all orders)
CREATE INDEX ON orders (user_id, created_at)
WHERE status = 'pending';Hash-Sharded Indexes
A common CockroachDB-specific problem: hotspot ranges. If your primary key is sequential (like an auto-increment ID or timestamp), all inserts go to the same Range — creating a write hotspot.
-- Distribute inserts evenly across 8 shards
CREATE INDEX ON events (created_at) USING HASH WITH BUCKET_COUNT = 8;IV. Common Pitfalls When Migrating from PostgreSQL
If you're migrating an existing app from PostgreSQL to CockroachDB, watch out for these:
1. Sequential Primary Keys — creates write hotspots
-- ❌ Avoid: all inserts hit the same Range
CREATE TABLE orders (id SERIAL PRIMARY KEY, ...);
-- ✅ Better: distributes writes evenly
CREATE TABLE orders (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ...);2. Large Transactions — increases conflict chance
-- ❌ Avoid: one giant transaction
DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days';
-- ✅ Better: batch deletes in a loop
DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days' LIMIT 1000;3. Schema Changes Are Online
Unlike PostgreSQL, CockroachDB performs all schema changes online — adding a column, creating an index, or dropping a table never locks the table. You can safely run migrations in production without downtime.
4. Large Imports
CockroachDB's IMPORT command is much faster than psql COPY for large datasets:
IMPORT INTO users (id, name, email)
CSV DATA ('gs://my-bucket/users.csv');V. Monitoring Performance
Beyond EXPLAIN ANALYZE, CockroachDB provides several built-in tools:
Statement Statistics — See the slowest queries across your cluster:
SELECT query, mean_latency_seconds, execution_count
FROM crdb_internal.statement_statistics
ORDER BY mean_latency_seconds DESC
LIMIT 10;Admin UI — The built-in dashboard at :8080 shows:
- SQL Statement Fingerprints with P50/P90/P99 latency
- Hot ranges (ranges with disproportionate traffic)
- Index usage recommendations
- Network latency heatmap across nodes
VI. Conclusion
Performance optimization in CockroachDB follows many of the same principles as PostgreSQL — good indexes, avoiding full table scans, batching large operations — but with a few distributed-specific considerations like hotspot avoidance and transaction retries.
This wraps up our CockroachDB series! Here's what we covered:
- Part 1: What CockroachDB is and its core ACID guarantees
- Part 2: Internal architecture — Ranges, Raft, Leaseholders, HLC
- Part 3: Getting started with a local cluster
- Part 4: Transactions and serializable isolation
- Part 5: Geo-distribution and multi-region deployments
- Part 6: Performance and query optimization
I hope this series gives you a solid foundation for using CockroachDB in production. If you have questions or feedback, feel free to reach out!
Thank you! 🙌




