Quando si lavora con Ruby on Rails, è importante capire come ottimizzare le query al database per migliorare le prestazioni e la user experience. Active Record, lo strumento ORM (Object-Relational Mapping) di Rails, offre potenti funzioni per interrogare i database in modo efficiente.

L’ottimizzazione delle query è un argomento complesso, sul quale sono stati scritti molti libri. Qui esploreremo alcune tecniche e alcuni consigli per ottimizzare le query di Active Record e aumentare la velocità e la reattività della vostra applicazione.

Usare il recupero selettivo delle colonne

Uno dei modi più efficaci per ottimizzare le query di Active Record è recuperare solo le colonne necessarie dal database. Specificando le colonne esatte di cui abbiamo bisogno, minimizziamo i dati trasferiti tra il database e la nostra applicazione Ruby on Rails. Ad esempio, se volessimo utilizzare solo i nomi del database:

# Unoptimized Practice: Retrieving all columns
User.all

# Optimized Practice: Selecting specific columns
User.select(:id, :name)

Usare l’eager loading

L’eager loading aiuta a ridurre il numero di query al database caricando in anticipo i record associati. Caricando in anticipo le associazioni, si evita il problema delle query N+1 per ogni record associato. Di seguito viene riportato un esempio del problema delle query N+1 e viene presentata una tecnica alternativa chiamata Russian Doll Caching.

# N+1 query problem
users = User.all
users.each { |user| puts user.posts.count }  # Executes one query for users and N queries for posts (N = number of users)

Nell’esempio precedente, recuperiamo tutti gli utenti e poi iteriamo su ogni utente per recuperare il numero di post associati. Questo comporta l’esecuzione di N query aggiuntive, con un conseguente calo delle prestazioni.

Per ovviare a questo problema, possiamo impiegare il metodo includes per l’eager loading, come mostrato di seguito:

# Eager loading solution
users = User.includes(:posts).all
users.each { |user| puts user.posts.count }  # Executes two queries: one for users and one for posts (regardless of user count)

Utilizzando il metodo includes(:posts), carichiamo i post associati per tutti gli utenti in sole due query. Il metodo includes precarica in modo efficiente i dati di associazione, eliminando la necessità di ulteriori query e migliorando notevolmente le prestazioni.

Tecnica alternativa: Russian doll caching

Oltre all’eager loading, una tecnica alternativa per ottimizzare le query del database è il Russian Doll Caching. Questa tecnica prevede la memorizzazione nella cache delle strutture dati gerarchiche e delle loro associazioni, consentendo un recupero efficiente senza query ridondanti.

Consideriamo un esempio in cui recuperiamo un elenco di post di un blog e i relativi commenti:

# Without caching (N+1 query problem)
@posts = Post.all
@posts.each do |post|
  @comments = post.comments
  # Perform actions with comments
end

Nel codice precedente, ogni iterazione del ciclo attiva una query per recuperare i commenti di ogni post, il che porta a N query aggiuntive.

Per implementare il Russian Doll Caching, possiamo utilizzare un approccio di caching come il fragment caching. Mettendo in cache l’intera vista o parte di essa, compresi i record associati, possiamo evitare query ridondanti. Ecco un esempio:

# With Russian Doll Caching
<% cache @posts do %>
  <% @posts.each do |post| %>
    <% cache post do %>
      <%= post.title %>
      <% post.comments.each do |comment| %>
        <%= comment.content %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

In questa implementazione, mettiamo in cache l’oggetto @posts e ogni singolo post usando l’helper cache. Al momento del rendering della vista o del parziale, Rails controlla la cache prima di eseguire qualsiasi codice, eliminando così la necessità di ulteriori query.

Implementando il Russian Doll Caching, possiamo ottimizzare le prestazioni riducendo al minimo le query al database e recuperando in modo efficiente le strutture dati gerarchiche e le loro associazioni.

L’Eager Loading è una tecnica potente per evitare il problema delle N+1 query precaricando le associazioni. Inoltre, il Russian Doll Caching offre un approccio alternativo per ottimizzare le query del database mettendo in cache le strutture dati gerarchiche e le loro associazioni.

Utilizzando queste tecniche, possiamo aumentare le prestazioni e la reattività delle applicazioni Ruby on Rails. Scegliete pure l’approccio che meglio si adatta alle esigenze e alle complessità della vostra applicazione.

Esistono gemme che vi aiuteranno a identificare le query N+1 durante lo sviluppo della vostra applicazione. Gemme come Bullet, Rack Mini Profiler e Prosopite sono alcuni esempi che vale la pena provare nel vostro progetto.

Utilizzare l’indicizzazione

Gli indici migliorano le prestazioni delle query consentendo al database di individuare i record più rapidamente. In Active Record, possiamo aggiungere indici allo schema del database, in particolare sulle colonne utilizzate frequentemente nelle query. Ad esempio:

# Add index to improve performance
add_index :users, :email

Inoltre, ci sono gemme che possono aiutarci a identificare i punti in cui aggiungere gli indici, come le gemme lol_dba o database_consistency.

Ottimizzare le query del database con le condizioni

Quando costruite le query, prendete in considerazione l’utilizzo di funzioni specifiche del database come condizioni per evitare di recuperare dati non necessari. Active Record offre diversi metodi per ottimizzare le condizioni delle query, come where, limit, offset e order. Ecco un esempio:

# Unoptimized query
users = User.all
users.select { |user| user.age > 18 && user.age < 25 }

# Optimized query
users = User.where(age: 19..24).all

Elaborare grandi insiemi di dati in batch

Lavorare con grandi insiemi di dati può avere un impatto sulle prestazioni a causa dei limiti di memoria. Possiamo quindi prendere in considerazione l’utilizzo di tecniche di elaborazione batch per suddividere le query in parti più piccole, riducendo l’utilizzo della memoria. Questo approccio è particolarmente utile quando si eseguono operazioni come l’aggiornamento o la cancellazione di record.

Tuttavia, è importante utilizzare correttamente l’elaborazione batch per ottenere prestazioni ottimali. Vediamo un esempio di cattiva elaborazione batch e come può influire negativamente su un’applicazione:

# Unoptimized Practice: Naive batch processing
users = User.all
users.each do |user|
  # Perform operations on user record
end

Nello snippet di codice qui sopra, recuperiamo tutti i record degli utenti dal database utilizzando User.all. Questo può rappresentare un problema significativo per le prestazioni quando si ha a che fare con grandi insiemi di dati, perché carica tutti i record in memoria in una sola volta. Di conseguenza, l’applicazione potrebbe consumare troppe risorse di memoria e rallentare.

Per risolvere questo problema, rifattorizziamo il codice utilizzando un approccio all’elaborazione batch più ottimizzato:

# Optimized Practice: Batch processing with `find_in_batches`
User.find_in_batches(batch_size: 1000) do |users_batch|
  users_batch.each do |user|
    # Perform operations on user record
  end
end

In questa implementazione aggiornata, utilizziamo il metodo find_in_batches fornito da Active Record. Questo metodo recupera i record in lotti più piccoli, specificati da batch_size, riducendo l’ingombro in memoria. Ogni lotto di record viene elaborato all’interno del proprio contesto di memoria, migliorando notevolmente le prestazioni dell’applicazione quando si tratta di grandi insiemi di dati.

Utilizzando find_in_batches, possiamo elaborare efficacemente grandi insiemi di dati in modo efficiente dal punto di vista della memoria. Ricordate di regolare batch_size in base alle esigenze dell’applicazione e alle risorse di sistema disponibili.

Riepilogo

L’ottimizzazione delle query di Active Record è fondamentale per migliorare le prestazioni delle applicazioni Ruby on Rails. Seguendo i suggerimenti illustrati in questo articolo – tra cui il recupero selettivo delle colonne, l’eager loading, l’indicizzazione, l’ottimizzazione delle condizioni e l’elaborazione in batch – si possono migliorare notevolmente la velocità e l’efficienza delle query al database.

Ricordate che la messa a punto delle query non solo migliora l’esperienza dell’utente, ma riduce anche il carico sul server del database. Tenete a mente queste tecniche di ottimizzazione e la vostra applicazione Ruby on Rails funzionerà senza problemi, anche con grandi quantità di dati. Buona programmazione!

Lee Sheppard

Lee is an Agile certified full stack Ruby on Rails developer. With over six years in the tech industry he enjoys teaching, coaching Agile, and mentoring others. Lee also speaks at tech related events and has a background in design and illustration.