Wenn wir “Ruby” hören, assoziieren wir es häufig mit “Ruby on Rails”. Rails ist ein sehr funktionales und beliebtes Framework, das weit verbreitet für den Aufbau von APIs und Webanwendungen genutzt wird. Rails besteht aus unabhängigen Gems und ActiveRecord ist eines davon. Dieses leistungsstarke Gem vereinfacht die Arbeit mit Datenbanken, ermöglicht es, in objektorientierter Weise mit ihnen zu arbeiten und macht Ruby on Rails bei Entwicklern äußerst beliebt.

Es gibt jedoch zahlreiche Engpässe bei der Arbeit mit ActiveRecord. Viele Entwickler vergessen oft, ignorieren oder wissen einfach nicht über diese Probleme Bescheid. Zu Beginn der Projektentwicklung beeinflusst dies die Leistung der App nicht, aber später kann es zu erheblichen Problemen führen.

In unserer Erfahrung haben wir mit vielen großen Projekten gearbeitet, die Datenbanken mit zahlreichen Tabellen und einfachen sowie komplexen Beziehungen umfassten. In einem dieser Projekte standen wir vor dem Problem, dass Anfragen an den Server zu viele Abfragen an die Datenbank stellten. Neben den nützlichen Operationen gab es viele unnötige Abfragen, die die Leistung erheblich verlangsamten.

Als wir zum ersten Mal in das Konsolenprotokoll schauten, waren wir überwältigt, und die Behebung all dieser Probleme hätte viel Zeit und Mühe gekostet. Also nahmen wir die Herausforderung an, das System von diesen Abfragen zu bereinigen, einige von ihnen umzuschreiben, um die Ausführungszeit zu optimieren, den Code neu zu organisieren und “alles Mögliche zu tun, um das System zu beschleunigen”.

Nachfolgend finden Sie typische Fehler und Lösungen, die uns geholfen haben, unsere Ziele zu erreichen.
Um Ihnen den Unterschied in der Abfrageausführungsgeschwindigkeit zu zeigen, verwenden alle in diesem Artikel beschriebenen Beispiele eine einfache DB-Struktur, die wir für Tests erstellt haben. Sie enthält vier Modelle: User, Hotel, Company, Country mit typischen Spalten. Wir haben die Datenbank mit Testdaten gefüllt und 100.000 Benutzer, 100 Hotels und 1.000 Unternehmen hinzugefügt.

Wir verwenden auch Rubys Benchmark-Modul, um die Ausführungszeit unserer Codebeispiele zu testen.

Lassen Sie uns beginnen.

N+1-Abfrage

Der einfachste und einer der häufigsten Fehler, der tatsächlich die Gesamtleistung des Systems verlangsamt. Obwohl es ziemlich offensichtlich ist, haben wir uns entschieden, dieses Problem zu erwähnen, da die Lösungswege ebenfalls das Ergebnis beeinflussen können.

Als Beispiel nehmen wir ein User-Modell und ein Country-Modell. Das User-Modell hat die Beziehung „belongs_to:country“.

Das Ergebnis sollte die Liste der Benutzer mit ihren Ländern sein.

Der Benchmark liefert 0,063948 Sekunden für diese Abfrage.

Wenn man gerade anfängt, mit RoR zu arbeiten, denkt man zunächst: „Wow!!! Das sieht so einfach und ordentlich aus. Und es funktioniert!“.

Aber schauen wir genauer hin. Bei jeder Schleifeniteration ruft RoR eine SQL-Abfrage auf, um das Land zu finden, das zu einem Benutzer der aktuellen Iteration gehört. Es werden also 1 Abfrage zum Laden der Benutzer und 10 Abfragen zum Laden der Länder bei jeder Schleife aufgerufen.

Wir können dieses Problem leicht optimieren, indem wir die Liste mit zwei Abfragen verarbeiten:

Der Benchmark liefert 0,005315 Sekunden, was tatsächlich 12-mal schneller ist.

Zu diesem Zeitpunkt mögen die Zahlen sehr klein erscheinen, aber dieser Unterschied wird umso größer, je mehr Daten und Last auf den Servern vorhanden sind.

Obwohl dieser Fehler typisch und einfach erscheint, ist es entscheidend, ihn zu vermeiden. Eine detaillierte Beschreibung dieses Problems finden Sie in der offiziellen Ruby on Rails-Dokumentation.

Verwendung von JOINS zur Vermeidung der n+1-Abfrage

ActiveRecord verfügt über eine JOINS-Methode. Wenn wir sie verwenden, wird ActiveRecord die übergebene Tabelle in der Abfrage verknüpfen (oder, abhängig von der verwendeten Syntax, den übergebenen JOINS-String zur Abfrage hinzufügen). Üblicherweise wird JOINS verwendet, um Klauseln zu Abfragen hinzuzufügen.
Zum Beispiel:

Lassen Sie uns überprüfen, wie die Abfrageergebnisse aussehen:

Schauen Sie sich die Protokolle an: JOINS lädt keine Beziehungen und wird nur zum Filtern von Daten in einer verwandten Tabelle verwendet. Durch die Nutzung dieses Codes werden wir zum oben beschriebenen n+1-Problem zurückkehren.

Working with a database in Ruby on Rails - Using JOINS to prevent n+1 query

Verwendung von INCLUDES anstelle von SELECT+JOINS

Wenn Sie einige Datensätze und Beziehungen laden, benötigen Sie oft nur ein oder ein paar Felder aus einer verwandten Tabelle(n).

Der Benchmark liefert 0,038902 Sekunden.

Das Problem besteht darin, dass Rails für jedes geladene Feld Speicherplatz reserviert. In einigen Fällen enthält die verwandte Tabelle viele Spalten und speichert viele Daten. Warum sollten Sie also diese unnötigen Daten laden, wenn Sie nur ein Feld benötigen (wie im Beispiel)? Sie können den Code wie folgt umschreiben:

Der Benchmark liefert 0,001806 Sekunden, was auf den Beispieldaten 12-mal schneller ist. Wenn Sie eine komplexere Datenbank mit mehr Feldern haben, wird dieser Unterschied noch größer sein.

COUNT/SIZE/LENGTH bei geladenen Objekten

Eine der grundlegenden Aufgaben ist es, eine Anzahl von Datensätzen anzuzeigen. Es gibt drei verschiedene Methoden, dies zu tun: COUNT, SIZE und LENGTH.

Und alle diese Methoden arbeiten auf unterschiedliche Weise:

  • LENGTH lädt alle Datensätze aus der Datenbank (falls sie noch nicht geladen wurden) und berechnet deren Größe.
  • COUNT führt eine SQL-Abfrage aus, um die Anzahl der Datensätze in der Datenbank zu berechnen.
  • SIZE überprüft, ob die Datensätze geladen wurden – ruft dann die LENGTH-Methode für den Scope auf, oder führt ansonsten eine SELECT COUNT(*)-Abfrage aus.

Wenn Sie absolut sicher sind, dass Sie keine Liste der Datensätze benötigen, sollten Sie COUNT verwenden. Wenn Sie nicht wissen, ob die Daten geladen werden oder nicht, verwenden Sie die SIZE-Methode. Dies erspart Ihnen überflüssige Abfragen.

Zusätzlich möchten wir Ihnen einen kleinen Trick verraten. Stellen Sie sich vor, Sie haben einen Scope und wissen, dass Sie Daten aus diesem Scope laden und deren Anzahl berechnen müssen. Aber zuerst möchten wir die Anzahl der Datensätze erhalten.

Hier sind die Protokolle der Ausführung:

Wie Sie sehen, hat Rails zwei Abfragen ausgeführt. Der Benchmark liefert 0,047347 Sekunden.

Aber Sie können dies tatsächlich mit einer, optimierteren Abfrage tun. Alles, was Sie tun müssen, ist, eine LOAD-Methode zum Scope hinzuzufügen. Die Datensätze werden sofort geladen und die SIZE-Methode wird keine zusätzliche SQL-Abfrage ausführen.

Mit diesem Code gibt es nur eine Abfrage in den Protokollen:

Der Benchmark liefert 0,016334 Sekunden.

Dieser Trick ist relevant in Situationen, in denen wir die Scoped-Daten auf jeden Fall laden und keine zusätzlichen Scopes darunter hinzufügen.

Berechnungen auf der Ruby-Seite durchführen

Eine weitere beliebte Aufgabe ist es, einige komplexe Informationen über einen Benutzer anzuzeigen, die nicht in dem Zustand aus der Datenbank geladen werden können, in dem sie gespeichert sind.

Zum Beispiel nehmen wir zwei Modelle – User und Company mit vielen-zu-vielen-Assoziationen. Außerdem gehört das User-Modell zum Hotel-Modell. Wir möchten die Anzahl der eindeutigen Benutzer nach Hotels für jede Firma ausgeben.

Sie können dies in Ruby wie folgt berechnen:

Der Benchmark liefert 1,771338 Sekunden.

Und wir können dieselben Berechnungen durchführen, aber die Zählung auf der SQL-Seite verarbeiten:

Der Benchmark liefert 0,127908 Sekunden.

Die Leistungssteigerung ergibt sich daraus, dass alle Berechnungen auf der SQL-Seite durchgeführt werden und Sie keine unnötigen Daten aus der Datenbank laden.

Missbrauch von ActiveRecord-Callbacks

Ein Callback in ActiveRecord-Modellen ist ein sehr mächtiges Werkzeug, das hilft, das Verhalten von Entitäten zu steuern. Doch das übermäßige Verwenden von Callbacks kann das System erheblich verlangsamen.

Wenn wir einem Modell einen Callback hinzufügen, wird dieser für jedes Ereignis (das mit diesem Callback zusammenhängt) ausgelöst, aber einige dieser Aufrufe können überflüssig sein. Sie führen keine nützlichen Aktionen durch und können beispielsweise unnötige SQL-Abfragen ausführen oder unnötige Berechnungen durchführen. Daher sollten Sie, bevor Sie einem Modell einen Callback hinzufügen, überlegen, ob dieser Callback wirklich für jedes Ereignis notwendig ist.

Um diese Situation zu vermeiden, können Sie den Code der Callbacks in Methoden oder Klassen verschieben und sie nur dann ausführen, wenn es wirklich notwendig ist. Dies reinigt und beschleunigt Ihr System erheblich.

Zusammenfassung

Wie Sie sehen, gibt es viele Ansätze, um grundlegende Aufgaben mit Ruby on Rails zu implementieren, aber nicht jeder ist nützlich. Die Wahl der richtigen Lösungen wird Ihre Anwendung definitiv beschleunigen und Sie vor schmerzhaften nachträglichen Optimierungen bewahren.

Für diejenigen, die sich auf innovative Dienste verlassen möchten, gibt es ein großartiges Tool, das hilft, einige der oben beschriebenen Fehler zu vermeiden. Es heißt Bullet und Sie finden es hier: https://github.com/flyerhzm/bullet

Über Redwerk

Wenn Sie auf der Suche nach erfahrenen Outsourcing-Entwicklern sind, ist Redwerk der richtige Ansprechpartner. Wir bieten hochwertige Outsourcing von Software-Entwicklungsdienstleistungen basierend auf unserer umfangreichen Erfahrung mit verschiedenen Programmiersprachen und Technologien. Egal, ob Sie Datenbankerstellung oder Ruby on Rails-Entwicklungsdienste benötigen, unser Team ist gerne bereit, Teil Ihres Projekts zu werden. Wir sind auch als eines der besten Ruby on Rails-Unternehmen auf DesignRush anerkannt.