Skip to content

PostgreSQL-Specific Indexes

PostgreSQL provides several specialized index types for different use cases. These are PostgreSQL-only and will raise an error on SQLite.

GIN Indexes

General Inverted Indexes are excellent for JSONB, array, and full-text search columns.

In Table Definition

create_table :posts do |t|
  t.primary_key
  t.string :title
  t.text :content

  # Index JSONB metadata for fast containment queries
  t.gin_index("metadata", fastupdate: true)

  # Index arrays for fast array operations
  t.gin_index("tags", name: "idx_posts_tags_gin")

  t.timestamps
end

Standalone Index Creation

# Add GIN index to existing table
add_gin_index :posts, :metadata

# Remove GIN index
remove_gin_index :posts, :metadata

When to use: JSONB queries with containment operators, array containment, overlaps, and full-text search.

GiST Indexes

Generalized Search Tree indexes support range types, geometric types, and specialized queries.

In Table Definition

create_table :places do |t|
  t.primary_key
  t.string :name

  # GiST index for geometric data
  t.gist_index("location")

  # Multi-column GiST for coordinate pairs
  t.gist_index(["latitude", "longitude"], name: "idx_coords_gist")

  t.timestamps
end

Standalone Operations

# Add GiST index
add_gist_index :places, :location

# Remove GiST index
remove_gist_index :places, :location

When to use: Geometric/range type queries, nearest-neighbor searches, or overlap detection.

Full-Text Search Indexes

Dedicated indexes for PostgreSQL full-text search operations.

Single Column

create_table :articles do |t|
  t.primary_key
  t.string :title
  t.text :content

  # Full-text search on content with English tokenization
  t.full_text_index("content", config: "english")

  t.timestamps
end

Multi-Column

create_table :documents do |t|
  t.primary_key
  t.string :title
  t.text :body
  t.text :summary

  # Search across multiple columns
  t.full_text_index(["title", "body"], config: "english", name: "idx_document_search")

  t.timestamps
end

Standalone Operations

# Add full-text index
add_full_text_index :articles, :content, config: "english"

# Add multi-column full-text index
add_full_text_index :articles, [:title, :content], config: "english"

# Remove full-text index
remove_full_text_index :articles, :content

When to use: Querying with where_search, where_phrase_search, and where_websearch methods for optimal performance.

Language Configurations: Common configs include 'english', 'simple', 'french', 'german', 'spanish', 'russian', etc.

Partial Indexes

Conditional indexes that only index rows matching a condition, reducing index size and improving performance for filtered queries.

In Table Definition

create_table :users do |t|
  t.primary_key
  t.string :email
  t.boolean :active, default: true
  t.soft_deletes

  # Only index active users (smaller, faster index)
  t.partial_index("email", condition: "active = true", unique: true)

  # Only index non-deleted records
  t.partial_index("deleted_at", condition: "deleted_at IS NULL")

  t.timestamps
end

Standalone Operations

# Add partial index
add_partial_index :users, :email, condition: "active = true", unique: true

# Add partial unique index for deleted records
add_partial_index :posts, :slug, condition: "deleted_at IS NULL", unique: true

# Remove partial index
remove_partial_index :users, :email

When to use: When most queries filter on specific conditions (soft deletes, status flags, active records). Reduces index size and maintenance overhead.

Expression Indexes

Indexes on computed expressions rather than raw columns, useful for case-insensitive lookups or JSON extraction.

In Table Definition

create_table :users do |t|
  t.primary_key
  t.string :email

  # Case-insensitive email lookup using lower()
  t.expression_index("lower(email)", name: "idx_email_lower", unique: true)

  # Index JSON field extraction
  t.expression_index("(data->>'category')", method: "btree")

  t.timestamps
end

Standalone Operations

# Add expression index for case-insensitive search
add_expression_index :users, "lower(email)", unique: true

# Add expression index on JSON extraction
add_expression_index :posts, "(metadata->>'status')", unique: false

# Remove expression index
remove_expression_index :users, name: "idx_email_lower"

When to use: - Case-insensitive lookups (use lower() or upper()) - Extracting and indexing JSON fields - Complex computed values used in WHERE clauses - Indexes on function results

Index Strategy Summary

Index Type Best For Reduces Example
GIN JSONB, arrays, full-text Containment queries tags @> ARRAY['active']
GiST Ranges, geometry, near searches Range overlaps Location-based queries
Full-Text Text search queries Full-text patterns where_search("content", "...")
Partial Filtered data (soft deletes, status) Index size "active = true" only
Expression Computed/transformed lookups Function calls lower(email) = ...

Comprehensive Example

class CreateBlogSchema_20240115100000 < Ralph::Migrations::Migration
  migration_version 20240115100000

  def up : Nil
    create_table :articles do |t|
      t.primary_key
      t.string :title, null: false
      t.text :content
      t.string_array :tags, default: "[]"
      t.jsonb :metadata, default: "{}"
      t.boolean :published, default: false
      t.soft_deletes
      t.timestamps

      # Full-text search on content
      t.full_text_index("content", config: "english")

      # Case-insensitive email lookup
      t.expression_index("lower(title)", name: "idx_title_lower")

      # Only active published articles
      t.partial_index("published", condition: "deleted_at IS NULL", unique: false)

      # Fast array operations
      t.gin_index("tags")

      # Fast JSONB queries
      t.gin_index("metadata")
    end
  end

  def down : Nil
    drop_table :articles
  end
end