35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Database Indexing: Trage Queries Oplossen Met de Juiste Indexstrategie

Rails Database Indexing: Trage Queries Oplossen Met de Juiste Indexstrategie

roger
Praktische gids voor database-indexering in Rails. Behandelt composite indexes, partial indexes, expression indexes en hoe je ontbrekende indexes in productie vindt.

Een ontbrekende database-index is de meest voorkomende oorzaak van trage Rails queries. Voeg de verkeerde index toe en je verspilt schijfruimte terwijl writes trager worden. Voeg de juiste toe en een query van 3 seconden daalt naar 2 milliseconden.

Deze gids behandelt hoe je het juiste indextype kiest voor je Rails queries, hoe je ontbrekende indexes in productie vindt, en wanneer je niet moet indexeren.

Hoe Rails Migrations Indexes Aanmaken

Wanneer je een migration genereert met add_index, maakt Rails standaard een B-tree index:

# db/migrate/20260226090000_add_index_to_orders.rb
class AddIndexToOrders < ActiveRecord::Migration[8.0]
  def change
    add_index :orders, :customer_id
  end
end

Dit dekt het simpele geval: orders opzoeken op customer_id. Maar de meeste productie-queries zijn niet zo simpel.

Composite Indexes: Kolomvolgorde Is Bepalend

Wanneer een query op meerdere kolommen filtert, heb je een composite index nodig. De kolomvolgorde bepaalt welke queries de index kan bedienen.

add_index :orders, [:customer_id, :status, :created_at]

PostgreSQL gebruikt composite indexes van links naar rechts. Deze index helpt bij:

  • WHERE customer_id = ? — ja
  • WHERE customer_id = ? AND status = ? — ja
  • WHERE customer_id = ? AND status = ? AND created_at > ? — ja
  • WHERE status = ?nee (slaat de meest linkse kolom over)
  • WHERE customer_id = ? AND created_at > ? — gedeeltelijk (gebruikt customer_id, scant voor created_at)

De regel: zet equality-condities eerst, range-condities laatst. Als je regelmatig WHERE status = ? AND created_at > ? zonder customer_id query’t, heb je een aparte index nodig.

Ik heb teams gezien die zes single-column indexes op één tabel maakten terwijl twee goed ontworpen composite indexes elk querypatroon zouden dekken. Elke overbodige index vertraagt INSERT en UPDATE operaties. Op een tabel die 10.000 writes per seconde verwerkt, telt die overhead op.

Partial Indexes: Indexeer Alleen Wat Je Queryt

Als 95% van je orders status: 'completed' heeft en je alleen actieve orders queryt:

add_index :orders, :customer_id, where: "status != 'completed'", name: 'idx_orders_active'

Deze partial index is een fractie van de grootte en sneller om te scannen. In een project verving ik een volledige index op een tabel met 40 miljoen rijen door een partial index — de indexgrootte ging van 860MB naar 12MB. Querytijd daalde omdat PostgreSQL minder data hoefde te doorlopen.

Rails scopes werken met partial indexes als de querycondities overeenkomen:

class Order < ApplicationRecord
  scope :active, -> { where.not(status: 'completed') }
end

# Deze query gebruikt de partial index
Order.active.where(customer_id: 42)

Expression Indexes voor Berekende Lookups

Wanneer je op een berekende waarde zoekt, helpt een gewone index niet:

# Dit triggert een full table scan ondanks een index op 'email'
User.where("LOWER(email) = ?", email.downcase)

Maak een expression index:

add_index :users, 'LOWER(email)', name: 'idx_users_lower_email', unique: true

In Rails 8 met PostgreSQL kun je met expression indexes ook JSONB-paden indexeren:

add_index :events, "(metadata->>'event_type')", name: 'idx_events_type'

Dit maakt het queryen van JSONB-kolommen op schaal praktisch zonder een aparte kolom toe te voegen.

GIN Indexes voor Full-Text Search en Arrays

B-tree indexes werken niet voor array containment of full-text search. Gebruik GIN (Generalized Inverted Index):

# Voor array-kolommen
add_index :articles, :tags, using: :gin

# Voor full-text search
execute <<-SQL
  CREATE INDEX idx_articles_search ON articles
  USING gin(to_tsvector('english', title || ' ' || body));
SQL

GIN indexes zijn trager om te updaten dan B-tree indexes maar veel sneller voor containment queries (@>, &&) en full-text search (@@).

Ontbrekende Indexes Vinden in Productie

pg_stat_user_tables: Sequential Scan Detectie

SELECT schemaname, relname, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 1000
ORDER BY seq_tup_read DESC
LIMIT 20;

Tabellen met hoge seq_scan aantallen en hoge seq_tup_read ten opzichte van idx_scan zijn kandidaten voor ontbrekende indexes.

pg_stat_user_indexes: Ongebruikte Index Detectie

SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;

Indexes met nul scans verspillen ruimte en vertragen writes. Verwijder ze. Reset eerst de stats-teller (pg_stat_reset()) en wacht een volledige bedrijfscyclus voordat je beslist.

De active_record_doctor Gem

Voor geautomatiseerde detectie in Rails:

# Gemfile
gem 'active_record_doctor', group: :development

# Voer vervolgens uit:
# bundle exec rake active_record_doctor:missing_foreign_key_indexes
# bundle exec rake active_record_doctor:unindexed_foreign_keys

Dit vangt de meest voorkomende misser: foreign keys zonder indexes. Elke belongs_to associatie moet een index hebben op de foreign key kolom.

EXPLAIN ANALYZE: Controleer of Je Index Wordt Gebruikt

Een index toevoegen garandeert niet dat PostgreSQL hem gebruikt. De query planner maakt kosten-gebaseerde beslissingen. Controleer met EXPLAIN ANALYZE:

# In Rails console
Order.where(customer_id: 42, status: 'pending').explain(:analyze)

Zoek naar Index Scan of Index Only Scan in de output. Als je Seq Scan ziet ondanks een index, veelvoorkomende oorzaken:

  1. Tabel is te klein — PostgreSQL beslist dat een sequential scan sneller is
  2. Statistics zijn verouderd — Voer ANALYZE orders; uit
  3. Query matcht niet met indexvorm — Controleer kolomvolgorde in composite indexes
  4. Type mismatch — Een string vergelijken met een integer-kolom omzeilt de index

Covering Indexes (Index-Only Scans)

PostgreSQL 11+ ondersteunt INCLUDE-kolommen in indexes. Dit laat PostgreSQL een query volledig vanuit de index beantwoorden zonder de tabel te raadplegen:

execute <<-SQL
  CREATE INDEX idx_orders_covering ON orders (customer_id, status)
  INCLUDE (total_amount, created_at);
SQL

Op I/O-gebonden workloads scheelt dit aanzienlijk in querytijd.

Wanneer Niet Indexeren

Niet elke kolom heeft een index nodig:

  • Low-cardinality boolean kolommen — Een index op een boolean kolom met 50/50 verdeling helpt zelden
  • Write-heavy tabellen met weinig reads — Elke index voegt write-overhead toe
  • Kolommen alleen in SELECT lijsten — Indexes helpen bij WHERE, JOIN, ORDER BY en GROUP BY
  • Kleine tabellen — Onder een paar duizend rijen is een sequential scan vaak sneller

Index Bloat Monitoren

PostgreSQL indexes worden opgeblazen na verloop van tijd door updates en deletes.

Check bloat:

SELECT
  nspname, relname,
  pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
  idx_scan
FROM pg_stat_user_indexes ui
JOIN pg_index i ON ui.indexrelid = i.indexrelid
JOIN pg_class c ON i.indrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE nspname = 'public'
ORDER BY pg_relation_size(i.indexrelid) DESC
LIMIT 10;

Voor tabellen met veel updates, plan periodiek REINDEX CONCURRENTLY (PostgreSQL 12+). Je zero-downtime migratiestrategie moet indexonderhoud bevatten.

Praktische Checklist

  1. Audit foreign keys — Elke _id kolom in associaties heeft een index nodig
  2. Check je slow query log — PostgreSQL’s log_min_duration_statement op 100ms vangt de ergste gevallen
  3. Ontwerp composite indexes voor je daadwerkelijke queries — Raad niet, check EXPLAIN ANALYZE
  4. Gebruik partial indexes — Bij status-gefilterde queries, indexeer alleen de rijen die ertoe doen
  5. Verwijder ongebruikte indexes — Ze zijn pure overhead
  6. Monitor bloat — Vooral op high-write tabellen

FAQ

Hoeveel indexes is te veel op één tabel?

Er is geen harde limiet, maar elke index voegt write-overhead toe. Ik mik doorgaans op minder dan 8 indexes per tabel. Als je er meer hebt, heb je waarschijnlijk redundante indexes — een composite index op (a, b) maakt een single-column index op a overbodig.

Moet ik indexes toevoegen aan kolommen in ORDER BY?

Ja, als de ORDER BY op een grote resultaatset is. PostgreSQL kan een B-tree index gebruiken om rijen gesorteerd terug te geven zonder een aparte sorteerstap. Combineer het met je WHERE-condities in een composite index voor het beste resultaat.

Kan ik indexes aanmaken zonder downtime?

Ja. Gebruik algorithm: :concurrently in Rails migrations:

class AddIndexConcurrently < ActiveRecord::Migration[8.0]
  disable_ddl_transaction!

  def change
    add_index :orders, :customer_id, algorithm: :concurrently
  end
end

De disable_ddl_transaction! is vereist omdat concurrent index creation niet binnen een transactie kan draaien. Zie onze gids over zero-downtime database migraties voor meer hierover.

Hoe weet ik of PostgreSQL mijn index gebruikt?

Voer EXPLAIN ANALYZE uit op je query. Zoek naar Index Scan of Index Only Scan in het plan. Als PostgreSQL Seq Scan kiest op een grote tabel ondanks je index, controleer dat je querycondities overeenkomen met de indexkolommen in de juiste volgorde.

#ruby-on-rails #database #performance #postgresql
r

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation