PostgreSQL Tips For Rails Developers
Practical PostgreSQL techniques and lesser-known features that can make your Rails development more efficient and your applications more performant.

Rails makes it remarkably easy to work with databases through ActiveRecord, but there are times when dropping down to PostgreSQL directly can save you hours of debugging or unlock performance improvements that just aren't possible through the ORM.
Over the years, I've collected a set of PostgreSQL techniques that have proven invaluable in Rails projects. Some of these are basic navigation commands you'll use daily, while others are more advanced techniques for performance optimization and debugging complex queries.
Whether you're troubleshooting a slow query, exploring your schema, or implementing advanced features, these tips should help you work more effectively with PostgreSQL in your Rails applications.
Getting Started: Console Navigation
Let's start with the essentials you'll use most often:
Connecting to Your Database
The quickest way to connect to your Rails database is with rails dbconsole
(or rails db
for short). This automatically connects to the database specified in your config/database.yml
for the current Rails environment.
If you need to connect manually, use psql -d database_name
or check your database configuration with:
psql $(rails runner "puts ActiveRecord::Base.connection_config[:database]")
Essential psql Commands
Once you're in the psql console, these commands will be your daily drivers:
\l
- List all databases\c database_name
- Connect to a specific database\dt
- List all tables\d table_name
- Describe a table's structure\di
- List all indexes\du
- List all users/roles\q
- Quit psql
Performance Analysis and Optimization
Understanding Query Execution with EXPLAIN
When ActiveRecord queries are running slowly, EXPLAIN ANALYZE
is your best friend. It shows you exactly how PostgreSQL is executing your query and where time is being spent:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
Look for key indicators like:
- Seq Scan - Table scans that might benefit from indexes
- Nested Loop - Join operations that could be optimized
- High cost values - Expensive operations
- Actual time - Real execution time vs. estimates
Identifying Missing Indexes
Find queries that are doing table scans and might benefit from indexes:
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE schemaname = 'public'
AND n_distinct > 100;
Analyzing Table and Index Sizes
Understanding what's taking up space in your database:
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
Advanced PostgreSQL Features for Rails
Working with JSONB Data
PostgreSQL's JSONB support is incredibly powerful for Rails applications. You can query JSON data directly:
-- Find users with specific preferences
SELECT * FROM users WHERE preferences->>'theme' = 'dark';
-- Query nested JSON structures
SELECT * FROM orders WHERE details->'shipping'->>'method' = 'express';
-- Use JSON path queries
SELECT * FROM products WHERE features @> '{"color": "red"}';
Full-Text Search
PostgreSQL's built-in full-text search can be more efficient than external search engines for many use cases:
-- Create a full-text search index
CREATE INDEX idx_posts_search ON posts USING gin(to_tsvector('english', title || ' ' || content));
-- Search for posts
SELECT * FROM posts
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('rails postgresql');
Working with Arrays
PostgreSQL arrays can replace many-to-many relationships in certain scenarios:
-- Find records containing specific array elements
SELECT * FROM articles WHERE tags && ARRAY['ruby', 'rails'];
-- Check if array contains all specified elements
SELECT * FROM articles WHERE tags @> ARRAY['ruby', 'rails'];
Development and Debugging Tools
Finding Slow Queries
Enable query logging to identify performance bottlenecks:
-- Enable slow query logging (requires superuser)
ALTER SYSTEM SET log_min_duration_statement = 1000; -- Log queries taking > 1 second
SELECT pg_reload_conf();
Monitoring Active Connections
See what's currently running on your database:
SELECT pid, now() - pg_stat_activity.query_start AS duration, query
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';
Checking Lock Conflicts
When you have hanging queries, check for lock conflicts:
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
Data Maintenance and Cleanup
Vacuum and Analyze
Keep your database healthy with regular maintenance:
-- Manual vacuum for a specific table
VACUUM ANALYZE users;
-- Check when tables were last vacuumed
SELECT schemaname, tablename, last_vacuum, last_autovacuum
FROM pg_stat_user_tables;
Finding Duplicate Data
Identify potential data quality issues:
-- Find duplicate emails
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Advanced Querying Techniques
Window Functions
Window functions can replace complex subqueries and are often more efficient:
-- Rank users by order count
SELECT name, order_count,
RANK() OVER (ORDER BY order_count DESC) as rank
FROM (
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
) ranked_users;
Common Table Expressions (CTEs)
CTEs make complex queries more readable and can improve performance:
-- Recursive CTE for hierarchical data
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 as level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY level, name;
Lateral Joins
Lateral joins let you reference earlier tables in the FROM clause:
-- Get each user's 3 most recent orders
SELECT u.name, recent_orders.created_at, recent_orders.total
FROM users u
CROSS JOIN LATERAL (
SELECT created_at, total
FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) recent_orders;
Rails-Specific Tips
Inspecting ActiveRecord Queries
See the actual SQL that ActiveRecord is generating:
# In rails console
User.where(email: '[email protected]').to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"email\" = '[email protected]'"
Database-Level Constraints
Some validations are better handled at the database level:
-- Add a check constraint
ALTER TABLE orders ADD CONSTRAINT positive_total CHECK (total > 0);
-- Add a unique constraint with custom error handling
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
Useful psql Configuration
Make your psql experience more pleasant by adding these to your ~/.psqlrc
:
-- Better formatting
\set COMP_KEYWORD_CASE upper
\set HISTFILE ~/.psql_history- :DBNAME
\set HISTCONTROL ignoredups
\set PROMPT1 '%[%033[1m%]%M %n@%/%R%[%033[0m%]%# '
\set PROMPT2 '[more] %R > '
-- Useful aliases
\set version 'SELECT version();'
\set extensions 'SELECT * FROM pg_available_extensions;'
Wrapping Up
These techniques have saved me countless hours of debugging and optimization work over the years. PostgreSQL is incredibly powerful, and while ActiveRecord handles most day-to-day database operations beautifully, knowing how to work directly with the database opens up a whole new level of capability.
The key is to start with the basics—get comfortable navigating in psql and understanding query execution plans—then gradually incorporate more advanced techniques as your applications grow in complexity.