Activerecord Query Patterns
by Kaakati
Complete guide to ActiveRecord query optimization, associations, scopes, and PostgreSQL-specific patterns. Use this skill when writing database queries, designing model associations, creating migrations, optimizing query performance, or debugging N+1 queries and grouping errors.
Skill Details
Repository Files
1 file in this skill directory
name: "ActiveRecord Query Patterns" description: "Complete guide to ActiveRecord query optimization, associations, scopes, and PostgreSQL-specific patterns. Use this skill when writing database queries, designing model associations, creating migrations, optimizing query performance, or debugging N+1 queries and grouping errors."
ActiveRecord Query Patterns Skill
This skill provides comprehensive guidance for writing efficient, correct ActiveRecord queries in Rails applications with PostgreSQL.
When to Use This Skill
- Writing complex ActiveRecord queries
- Designing model associations
- Creating database migrations
- Optimizing query performance
- Debugging N+1 queries
- Working with GROUP BY operations
- Implementing scopes and query objects
Model Structure
Standard Model Template
# app/models/task.rb
class Task < ApplicationRecord
# == Constants ============================================================
STATUSES = %w[pending in_progress completed failed cancelled].freeze
# == Associations =========================================================
belongs_to :account
belongs_to :merchant
belongs_to :carrier, optional: true
belongs_to :recipient
belongs_to :zone, optional: true
has_many :timelines, dependent: :destroy
has_many :task_actions, dependent: :destroy
has_many :photos, dependent: :destroy
# == Validations ==========================================================
validates :status, presence: true, inclusion: { in: STATUSES }
validates :tracking_number, presence: true, uniqueness: { scope: :account_id }
# == Scopes ===============================================================
scope :active, -> { where.not(status: %w[completed failed cancelled]) }
scope :completed, -> { where(status: 'completed') }
scope :for_carrier, ->(carrier) { where(carrier: carrier) }
scope :created_between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
scope :by_status, ->(status) { where(status: status) if status.present? }
# == Callbacks ============================================================
before_validation :generate_tracking_number, on: :create
after_commit :notify_recipient, on: :create
# == Class Methods ========================================================
def self.search(query)
where("tracking_number ILIKE :q OR description ILIKE :q", q: "%#{query}%")
end
# == Instance Methods =====================================================
def completable?
%w[pending in_progress].include?(status)
end
def complete!
update!(status: 'completed', completed_at: Time.current)
end
private
def generate_tracking_number
self.tracking_number ||= SecureRandom.hex(8).upcase
end
def notify_recipient
TaskNotificationJob.perform_later(id)
end
end
Association Patterns
Basic Associations
# One-to-Many
class Account < ApplicationRecord
has_many :users, dependent: :destroy
has_many :tasks, dependent: :destroy
end
class User < ApplicationRecord
belongs_to :account
end
# Many-to-Many (with join table)
class Task < ApplicationRecord
has_many :task_tags, dependent: :destroy
has_many :tags, through: :task_tags
end
class Tag < ApplicationRecord
has_many :task_tags, dependent: :destroy
has_many :tasks, through: :task_tags
end
class TaskTag < ApplicationRecord
belongs_to :task
belongs_to :tag
end
# Polymorphic
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Task < ApplicationRecord
has_many :comments, as: :commentable
end
class Invoice < ApplicationRecord
has_many :comments, as: :commentable
end
Association Options
class Task < ApplicationRecord
# Foreign key specification
belongs_to :creator, class_name: 'User', foreign_key: 'created_by_id'
# Optional association
belongs_to :carrier, optional: true
# Counter cache
belongs_to :merchant, counter_cache: true
# Dependent options
has_many :photos, dependent: :destroy # Delete associated records
has_many :logs, dependent: :nullify # Set foreign key to NULL
has_many :exports, dependent: :restrict_with_error # Prevent deletion
# Scoped association
has_many :active_timelines, -> { where(active: true) }, class_name: 'Timeline'
# Touch parent on update
belongs_to :bundle, touch: true
end
Query Patterns
Basic Queries
# Find
Task.find(1) # Raises RecordNotFound
Task.find_by(id: 1) # Returns nil if not found
Task.find_by!(id: 1) # Raises RecordNotFound
# Where
Task.where(status: 'pending')
Task.where(status: %w[pending in_progress]) # IN query
Task.where.not(status: 'completed')
Task.where(created_at: 1.week.ago..) # Range (>= date)
Task.where(created_at: ..1.week.ago) # Range (<= date)
Task.where(created_at: 1.month.ago..1.week.ago) # Between
# Order
Task.order(created_at: :desc)
Task.order(:status, created_at: :desc)
# Limit & Offset
Task.limit(10).offset(20)
# Distinct
Task.distinct.pluck(:status)
Avoiding N+1 Queries
# WRONG - N+1 query
tasks = Task.all
tasks.each { |t| puts t.carrier.name } # Query per task!
# CORRECT - Eager loading
tasks = Task.includes(:carrier)
tasks.each { |t| puts t.carrier.name } # Single query
# Multiple associations
Task.includes(:carrier, :merchant, :recipient)
# Nested associations
Task.includes(merchant: :branches)
# With conditions on association (use joins or references)
Task.includes(:carrier).where(carriers: { active: true }).references(:carriers)
# OR
Task.joins(:carrier).where(carriers: { active: true })
Choosing Loading Strategy
# includes - Smart loading (preload or eager_load based on usage)
Task.includes(:carrier)
# preload - Separate queries (can't filter on association)
Task.preload(:carrier)
# SELECT * FROM tasks
# SELECT * FROM carriers WHERE id IN (...)
# eager_load - Single LEFT JOIN query
Task.eager_load(:carrier)
# SELECT tasks.*, carriers.* FROM tasks LEFT JOIN carriers...
# joins - INNER JOIN (no loading, just filtering)
Task.joins(:carrier).where(carriers: { active: true })
GROUP BY Queries (Critical for PostgreSQL)
Rule: Every non-aggregated column in SELECT must appear in GROUP BY.
# CORRECT - Only grouped columns and aggregates
Task.group(:status).count
# => { "pending" => 10, "completed" => 25 }
Task.group(:status).sum(:amount)
# => { "pending" => 1000, "completed" => 5000 }
# CORRECT - Multiple GROUP BY columns
Task
.group(:status, :task_type)
.count
# => { ["pending", "express"] => 5, ["completed", "standard"] => 10 }
# CORRECT - Explicit select with aggregates
Task
.select(:status, 'COUNT(*) as task_count', 'AVG(amount) as avg_amount')
.group(:status)
# CORRECT - Date grouping
Task
.group("DATE(created_at)")
.count
# WRONG - includes with group
Task.includes(:carrier).group(:status).count # ERROR!
# CORRECT - Separate queries if you need associated data
status_counts = Task.group(:status).count
tasks_by_status = status_counts.keys.each_with_object({}) do |status, hash|
hash[status] = Task.where(status: status).includes(:carrier).limit(5)
end
Subqueries
# Subquery in WHERE
active_carrier_ids = Carrier.where(active: true).select(:id)
Task.where(carrier_id: active_carrier_ids)
# SELECT * FROM tasks WHERE carrier_id IN (SELECT id FROM carriers WHERE active = true)
# Subquery with join
Task.where(carrier_id: Carrier.active.select(:id))
.where(merchant_id: Merchant.premium.select(:id))
Raw SQL (When Needed)
# Safe with sanitization
Task.where("created_at > ?", 1.week.ago)
Task.where("description ILIKE ?", "%#{query}%")
# Named bindings
Task.where("status = :status AND amount > :min", status: 'pending', min: 100)
# Select with raw SQL
Task.select("*, amount * 0.1 as commission")
# Find by SQL
Task.find_by_sql(["SELECT * FROM tasks WHERE status = ?", 'pending'])
Scope Patterns
Simple Scopes
class Task < ApplicationRecord
scope :active, -> { where.not(status: %w[completed cancelled]) }
scope :completed, -> { where(status: 'completed') }
scope :recent, -> { order(created_at: :desc) }
scope :today, -> { where(created_at: Time.current.all_day) }
end
Parameterized Scopes
class Task < ApplicationRecord
scope :by_status, ->(status) { where(status: status) }
scope :created_after, ->(date) { where('created_at >= ?', date) }
scope :for_carrier, ->(carrier_id) { where(carrier_id: carrier_id) }
# With default
scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
# Conditional scope
scope :by_status_if_present, ->(status) { where(status: status) if status.present? }
end
Chainable Scopes
# All scopes are chainable
Task.active.recent.by_status('pending').for_carrier(123)
# Combine with where
Task.active.where(merchant_id: 456)
Query Objects
# app/queries/tasks/pending_delivery_query.rb
module Tasks
class PendingDeliveryQuery
def initialize(relation = Task.all)
@relation = relation
end
def call(zone_id: nil, since: 24.hours.ago)
result = @relation
.where(status: 'pending')
.where('created_at >= ?', since)
.includes(:carrier, :recipient)
result = result.where(zone_id: zone_id) if zone_id.present?
result.order(created_at: :asc)
end
end
end
# Usage
Tasks::PendingDeliveryQuery.new.call(zone_id: 123)
Tasks::PendingDeliveryQuery.new(account.tasks).call(since: 1.hour.ago)
Migration Patterns
Create Table
class CreateTasks < ActiveRecord::Migration[7.1]
def change
create_table :tasks do |t|
t.references :account, null: false, foreign_key: true
t.references :merchant, null: false, foreign_key: true
t.references :carrier, foreign_key: true # nullable
t.string :tracking_number, null: false
t.string :status, null: false, default: 'pending'
t.decimal :amount, precision: 10, scale: 2
t.jsonb :metadata, default: {}
t.datetime :completed_at
t.timestamps
t.index :tracking_number, unique: true
t.index :status
t.index [:account_id, :status]
t.index [:merchant_id, :created_at]
t.index :metadata, using: :gin # For JSONB queries
end
end
end
Safe Migrations
# Add column with default (safe in PostgreSQL 11+)
class AddPriorityToTasks < ActiveRecord::Migration[7.1]
def change
add_column :tasks, :priority, :integer, default: 0, null: false
end
end
# Add index concurrently (for large tables)
class AddIndexToTasksStatus < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :tasks, :status, algorithm: :concurrently
end
end
# Remove column safely
class RemoveOldColumnFromTasks < ActiveRecord::Migration[7.1]
def change
safety_assured { remove_column :tasks, :old_column, :string }
end
end
JSONB Columns
# Migration
add_column :tasks, :metadata, :jsonb, default: {}
add_index :tasks, :metadata, using: :gin
# Model
class Task < ApplicationRecord
# Using jsonb_accessor gem
jsonb_accessor :metadata,
priority: :integer,
tags: [:string, array: true],
notes: :string
end
# Queries
Task.where("metadata @> ?", { priority: 1 }.to_json)
Task.where("metadata->>'priority' = ?", '1')
Task.where("metadata ? 'special_flag'")
Performance Optimization
Batch Processing
# WRONG - Loads all records into memory
Task.all.each { |task| process(task) }
# CORRECT - Batches of 1000
Task.find_each(batch_size: 1000) { |task| process(task) }
# With specific order
Task.order(:id).find_each { |task| process(task) }
# In batches (for batch operations)
Task.in_batches(of: 1000) do |batch|
batch.update_all(processed: true)
end
Select Only Needed Columns
# WRONG - Loads all columns
users = User.all
users.each { |u| puts u.email }
# CORRECT - Only needed columns
users = User.select(:id, :email)
users.each { |u| puts u.email }
# With pluck (returns arrays, not AR objects)
emails = User.pluck(:email)
Counter Caches
# Migration
add_column :merchants, :tasks_count, :integer, default: 0
# Model
class Task < ApplicationRecord
belongs_to :merchant, counter_cache: true
end
# Now merchant.tasks_count doesn't query
merchant.tasks_count # Uses cached count
Exists? vs Any? vs Present?
# EFFICIENT - Stops at first match
Task.where(status: 'pending').exists?
# SELECT 1 FROM tasks WHERE status = 'pending' LIMIT 1
# LESS EFFICIENT - Loads records
Task.where(status: 'pending').any?
# May load records depending on implementation
# INEFFICIENT - Loads all records
Task.where(status: 'pending').present?
# SELECT * FROM tasks WHERE status = 'pending'
Explain & Analyze
# In Rails console
Task.where(status: 'pending').explain
Task.where(status: 'pending').explain(:analyze)
# Check for sequential scans on large tables
# Look for "Seq Scan" - may need index
Debugging Queries
# In Rails console, enable query logging
ActiveRecord::Base.logger = Logger.new(STDOUT)
# Or in development.rb
config.active_record.verbose_query_logs = true
# Using bullet gem for N+1 detection
# Gemfile: gem 'bullet', group: :development
Rails 7.x/8.x Modern Features
Composite Primary Keys (Rails 7.1+)
# Migration
class CreateBookOrders < ActiveRecord::Migration[7.1]
def change
create_table :book_orders, primary_key: [:shop_id, :id] do |t|
t.integer :shop_id
t.integer :id
t.string :status
t.timestamps
end
end
end
# Model
class BookOrder < ApplicationRecord
self.primary_key = [:shop_id, :id]
belongs_to :shop
has_many :line_items, foreign_key: [:shop_id, :order_id]
end
# Usage
order = BookOrder.find([shop_id: 1, id: 100])
order.id # => { shop_id: 1, id: 100 }
ActiveRecord::Encryption (Rails 7+)
For encrypting sensitive data at rest:
# config/credentials.yml.enc
active_record_encryption:
primary_key: <%= ENV['AR_ENCRYPTION_PRIMARY_KEY'] %>
deterministic_key: <%= ENV['AR_ENCRYPTION_DETERMINISTIC_KEY'] %>
key_derivation_salt: <%= ENV['AR_ENCRYPTION_KEY_DERIVATION_SALT'] %>
# Model
class User < ApplicationRecord
encrypts :email # Non-deterministic (can't query)
encrypts :ssn, deterministic: true # Deterministic (can query equality)
encrypts :credit_card, ignore_case: true
end
# Queries with deterministic encryption
User.where(ssn: '123-45-6789') # Works with deterministic: true
User.where(email: 'user@example.com') # Doesn't work without deterministic
# Unencrypted reads (for migration)
class User < ApplicationRecord
encrypts :email, ignore_case: true, previous: { ignore_case: false }
end
Multi-Database Configuration (Rails 6.1+)
# config/database.yml
production:
primary:
<<: *default
database: my_primary_database
analytics:
<<: *default
database: my_analytics_database
replica: true
migrations_paths: db/analytics_migrate
# Models
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary }
end
class AnalyticsRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :analytics, reading: :analytics }
end
class Event < AnalyticsRecord
end
# Switching databases
ActiveRecord::Base.connected_to(role: :reading) do
# Read from replica
end
ActiveRecord::Base.connected_to(role: :writing) do
# Write to primary
end
# Prevent writes
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
# Raises error on write
end
Horizontal Sharding (Rails 7.1+)
# config/database.yml
production:
primary:
database: my_primary_database
shard_one:
database: my_shard_one_database
shard_two:
database: my_shard_two_database
# Model
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to shards: {
shard_one: { writing: :shard_one },
shard_two: { writing: :shard_two }
}
end
# Usage
ActiveRecord::Base.connected_to(shard: :shard_one) do
User.create!(name: "User in shard one")
end
ActiveRecord::Base.connected_to(shard: :shard_two) do
User.create!(name: "User in shard two")
end
# Switching shards based on data
def with_user_shard(user_id)
shard = user_id.even? ? :shard_one : :shard_two
ActiveRecord::Base.connected_to(shard: shard) do
yield
end
end
Enum Patterns with i18n
# Model
class Task < ApplicationRecord
enum status: {
pending: 0,
in_progress: 1,
completed: 2,
failed: 3,
cancelled: 4
}, _prefix: true # status_pending?, status_completed?
enum priority: {
low: 0,
medium: 1,
high: 2,
urgent: 3
}, _suffix: true # low_priority?, high_priority?
# Auto-generated methods:
# task.status => "pending"
# task.pending? => true
# task.status_pending? => true (with prefix)
# task.completed! => Changes to completed
# Task.statuses => {"pending" => 0, "completed" => 2, ...}
# Task.pending => Scope for pending tasks
# Task.not_pending => Scope for non-pending tasks
end
# i18n
# config/locales/en.yml
en:
activerecord:
attributes:
task:
status:
pending: "Pending"
in_progress: "In Progress"
completed: "Completed"
failed: "Failed"
cancelled: "Cancelled"
# Usage in views
<%= t("activerecord.attributes.task.status.#{task.status}") %>
# Or with enum_help gem
gem 'enum_help'
Task.human_attribute_name("status.#{task.status}")
# Scopes with enums
Task.pending # SELECT * FROM tasks WHERE status = 0
Task.not_pending # SELECT * FROM tasks WHERE status != 0
Task.where.not(status: :completed)
Database Views
# Migration
class CreateActiveTasksView < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE VIEW active_tasks AS
SELECT
tasks.*,
merchants.name AS merchant_name,
carriers.name AS carrier_name
FROM tasks
INNER JOIN merchants ON merchants.id = tasks.merchant_id
LEFT JOIN carriers ON carriers.id = tasks.carrier_id
WHERE tasks.status IN ('pending', 'in_progress')
SQL
end
def down
execute "DROP VIEW IF EXISTS active_tasks"
end
end
# Model
class ActiveTask < ApplicationRecord
# Read-only model backed by view
self.primary_key = :id
def readonly?
true
end
end
# Usage
ActiveTask.all
ActiveTask.where(merchant_name: "ACME Corp")
# Materialized Views (faster, but need refresh)
class CreateTaskSummaryView < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE MATERIALIZED VIEW task_summaries AS
SELECT
DATE(created_at) as date,
status,
COUNT(*) as count,
AVG(amount) as average_amount
FROM tasks
GROUP BY DATE(created_at), status
SQL
add_index :task_summaries, :date
end
def down
execute "DROP MATERIALIZED VIEW IF EXISTS task_summaries"
end
end
# Refresh materialized view
ActiveRecord::Base.connection.execute("REFRESH MATERIALIZED VIEW task_summaries")
# Concurrent refresh (non-blocking)
ActiveRecord::Base.connection.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY task_summaries")
Common Table Expressions (CTEs)
# Simple CTE
Task.with(
active_merchants: Merchant.where(active: true).select(:id)
).joins("INNER JOIN active_merchants ON tasks.merchant_id = active_merchants.id")
# Complex CTE example
Task.with(
recent_tasks: Task.where('created_at > ?', 30.days.ago).select(:id, :merchant_id),
active_merchants: Merchant.where(active: true).select(:id)
).from("recent_tasks")
.joins("INNER JOIN active_merchants ON recent_tasks.merchant_id = active_merchants.id")
# Recursive CTE for hierarchical data
Category.with_recursive(
category_tree: [
Category.where(id: 1), # Base case
Category.joins("INNER JOIN category_tree ON categories.parent_id = category_tree.id") # Recursive
]
).from(:category_tree)
# Using raw SQL for complex CTEs
sql = <<-SQL
WITH RECURSIVE subordinates AS (
SELECT id, name, manager_id, 1 as level
FROM employees
WHERE id = ?
UNION ALL
SELECT e.id, e.name, e.manager_id, s.level + 1
FROM employees e
INNER JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates
SQL
Employee.find_by_sql([sql, manager_id])
Single Table Inheritance (STI) Patterns
# Base model
class Vehicle < ApplicationRecord
# Columns: id, type, name, ...
validates :name, presence: true
# Shared behavior
def describe
"#{self.class.name}: #{name}"
end
end
class Car < Vehicle
# Car-specific behavior
def drive
"Driving #{name}"
end
# Car-specific validations
validates :num_doors, presence: true
end
class Motorcycle < Vehicle
def ride
"Riding #{name}"
end
end
# Usage
car = Car.create!(name: "Tesla", num_doors: 4)
motorcycle = Motorcycle.create!(name: "Harley")
Vehicle.all # Returns mix of cars and motorcycles
Car.all # Returns only cars
car.type # => "Car"
# Scopes work with STI
class Vehicle < ApplicationRecord
scope :recent, -> { where('created_at > ?', 1.week.ago) }
end
Car.recent # Only recent cars
Vehicle.recent # All recent vehicles
# Custom type column name
class Vehicle < ApplicationRecord
self.inheritance_column = 'vehicle_type'
end
# Disable STI (use 'type' column for something else)
class Vehicle < ApplicationRecord
self.inheritance_column = nil
end
STI Best Practices:
- Use when subclasses share 80%+ of attributes
- Avoid if types have very different attributes (use polymorphic instead)
- Watch for sparse tables (lots of nulls) - consider delegated_type
- Add database constraint on type column
STI Anti-patterns:
# BAD - Too many type-specific columns
create_table :vehicles do |t|
t.string :type
t.string :name
# Car-specific
t.integer :num_doors
t.string :trunk_type
# Boat-specific
t.integer :hull_length
t.string :sail_type
# Plane-specific
t.integer :max_altitude
t.string :engine_type
end
# GOOD - Use polymorphic or separate tables instead
Generated Columns (PostgreSQL/MySQL)
# PostgreSQL generated columns (Rails 7.0+)
class AddFullNameToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :full_name, :virtual,
type: :string,
as: "first_name || ' ' || last_name",
stored: true # Or false for computed on-the-fly
add_index :users, :full_name
end
end
# Model (read-only)
class User < ApplicationRecord
# full_name is automatically calculated
end
user = User.create!(first_name: "Alice", last_name: "Smith")
user.full_name # => "Alice Smith"
# Can query generated columns
User.where("full_name ILIKE ?", "%smith%")
Full-Text Search with pg_search
# Gemfile
gem 'pg_search'
# Model
class Article < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_full_text,
against: {
title: 'A', # Higher weight
body: 'B',
author: 'C'
},
using: {
tsearch: {
prefix: true,
dictionary: 'english'
}
}
# Multi-table search
pg_search_scope :search_with_comments,
against: [:title, :body],
associated_against: {
comments: [:body]
}
# Trigram similarity search
pg_search_scope :fuzzy_search,
against: [:title, :body],
using: {
trigram: { threshold: 0.3 }
}
end
# Migration for indexes
class AddPgSearchIndexes < ActiveRecord::Migration[7.1]
def up
# tsvector column for better performance
add_column :articles, :tsv, :tsvector
add_index :articles, :tsv, using: :gin
execute <<-SQL
UPDATE articles
SET tsv = to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
SQL
# Trigger to keep it updated
execute <<-SQL
CREATE TRIGGER articles_tsv_update BEFORE INSERT OR UPDATE
ON articles FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);
SQL
# For trigram search
enable_extension 'pg_trgm'
add_index :articles, :title, using: :gin, opclass: :gin_trgm_ops
end
def down
execute "DROP TRIGGER IF EXISTS articles_tsv_update ON articles"
remove_column :articles, :tsv
end
end
# Usage
Article.search_full_text("rails tutorial")
Article.fuzzy_search("raails") # Finds "rails"
Article.search_with_comments("ruby")
# With rankings
Article.search_full_text("rails")
.with_pg_search_rank
.order('pg_search_rank DESC')
Advanced JSONB Queries
# Model with JSONB
class Product < ApplicationRecord
# Column: specifications (jsonb)
# Using jsonb_accessor for typed access
jsonb_accessor :specifications,
color: :string,
weight: :float,
dimensions: [:string, array: true],
features: [:string, array: true]
end
# Query patterns
# Contains
Product.where("specifications @> ?", { color: 'red' }.to_json)
# Has key
Product.where("specifications ? 'warranty'")
# Array contains element
Product.where("specifications -> 'features' ? 'wireless'")
# Extract and compare
Product.where("specifications ->> 'color' = ?", 'red')
Product.where("(specifications ->> 'weight')::float > ?", 5.0)
# With indexes
add_index :products, :specifications, using: :gin
add_index :products, "(specifications -> 'color')", using: :btree
# Array queries
Product.where("specifications -> 'features' @> ?", ['wireless'].to_json)
Pre-Query Checklist
Before writing any complex query:
[ ] What columns am I selecting?
[ ] Am I using GROUP BY? If so, is every SELECT column grouped or aggregated?
[ ] Am I using includes/preload with GROUP BY? (DON'T!)
[ ] Will this query run on a large table? Do indexes exist?
[ ] Am I iterating and accessing associations? Use includes.
[ ] Am I loading more data than needed? Use select/pluck.
[ ] Is this sensitive data? Consider ActiveRecord::Encryption.
[ ] Should this be in a separate database? (multi-database)
[ ] Is this a hierarchical query? Consider CTEs.
[ ] Need full-text search? Use pg_search.
Related Skills
Xlsx
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
Clickhouse Io
ClickHouse database patterns, query optimization, analytics, and data engineering best practices for high-performance analytical workloads.
Clickhouse Io
ClickHouse database patterns, query optimization, analytics, and data engineering best practices for high-performance analytical workloads.
Analyzing Financial Statements
This skill calculates key financial ratios and metrics from financial statement data for investment analysis
Data Storytelling
Transform data into compelling narratives using visualization, context, and persuasive structure. Use when presenting analytics to stakeholders, creating data reports, or building executive presentations.
Team Composition Analysis
This skill should be used when the user asks to "plan team structure", "determine hiring needs", "design org chart", "calculate compensation", "plan equity allocation", or requests organizational design and headcount planning for a startup.
Kpi Dashboard Design
Design effective KPI dashboards with metrics selection, visualization best practices, and real-time monitoring patterns. Use when building business dashboards, selecting metrics, or designing data visualization layouts.
Dbt Transformation Patterns
Master dbt (data build tool) for analytics engineering with model organization, testing, documentation, and incremental strategies. Use when building data transformations, creating data models, or implementing analytics engineering best practices.
Sql Optimization Patterns
Master SQL query optimization, indexing strategies, and EXPLAIN analysis to dramatically improve database performance and eliminate slow queries. Use when debugging slow queries, designing database schemas, or optimizing application performance.
Anndata
This skill should be used when working with annotated data matrices in Python, particularly for single-cell genomics analysis, managing experimental measurements with metadata, or handling large-scale biological datasets. Use when tasks involve AnnData objects, h5ad files, single-cell RNA-seq data, or integration with scanpy/scverse tools.
