12k
All articles

ORM vs. Raw SQL: Was sollten Sie wählen?

ORM vs. Raw SQL: Sicherheit, Tempo und Kontrolle vergleichen, sehen, warum N+1 mehr schadet als Abstraktion, und je Query das richtige Tool wählen.

OpenReplay Team
OpenReplay Team
ORM vs. Raw SQL: Was sollten Sie wählen?

Für die meisten Anwendungen ist die ehrliche Standardempfehlung ein ORM oder Query Builder für die 90 % der routinemäßigen CRUD-Operationen – und handgeschriebenes SQL für die kleine Menge an Reporting-, Analyse- und Hot-Path-Abfragen, die es nachweislich rechtfertigen. Es handelt sich nicht um eine Entweder-oder-Entscheidung. Die eigentlich relevante Frage ist nicht „Was ist abstrakt betrachtet besser?”, sondern „Was ist das Richtige für diese Abfrage, in diesem Team, unter diesen Rahmenbedingungen?” Dieser Artikel liefert Ihnen eine Entscheidungsregel, eine Vergleichstabelle und das eine Fehlermuster – das N+1-Query-Problem –, das Teams weit häufiger trifft als der reine Abstraktions-Overhead es je tun würde. Die Beispiele sind im JS/TS-Stack verankert (Prisma, Drizzle, Kysely, Knex), da dort heute die meisten dieser Entscheidungen getroffen werden.

Wichtigste Erkenntnisse

  • Verwenden Sie standardmäßig einen ORM oder Query Builder für routinemäßige CRUD-Operationen, und greifen Sie auf Raw SQL für spezifische Reporting-, Analyse- und Hot-Path-Abfragen zurück, die es nachweislich benötigen – die meisten Produktionsanwendungen nutzen letztlich beides.
  • Der eigentliche Performance-Nachteil eines ORM ist fast nie die Abstraktion selbst, sondern das N+1-Query-Muster: Wenn eine Liste geladen und anschließend ihre Relationen zeilenweise abgefragt werden, entstehen aus einem einzigen Seitenaufruf Hunderte von Datenbankroundtrips.
  • N+1 behebt man durch Eager Loading der Relation in einer einzigen Abfrage (Prismas include oder ein JOIN) und die Auswahl nur der tatsächlich gerenderten Spalten – nicht durch das Aufgeben des ORM.
  • Query Builder wie Drizzle, Kysely und Knex sind die fehlende dritte Option: SQL-ähnliche, typgeprüfte Abfragen mit vollständiger Kontrolle über das generierte SQL und ohne Object-Mapping-Schicht.
  • ORMs parametrisieren Abfragen standardmäßig, was den häufigsten SQL-Injection-Angriffsvektor schließt; Raw SQL ist nur so sicher wie Ihre Disziplin beim Einsatz parametrisierter Statements.

Was ein ORM ist – und was „Raw SQL” bedeutet

Ein ORM (Object-Relational Mapper) ist eine Bibliothek, die Datenbanktabellen auf Objekte in Ihrer Programmiersprache abbildet, sodass Sie Daten über Methodenaufrufe und typisierte Modelle abfragen können, anstatt SQL-Strings manuell zu schreiben. Er generiert das SQL, sendet es an die Datenbank und hydratisiert die Zeilen zurück in Objekte – dabei übernimmt er Relationen, Parametrisierung und Typzuordnung. Im JS/TS-Umfeld sind Prisma und TypeORM die Vertreter des Full-ORM-Ansatzes. Ein typischer Prisma-Lesevorgang sieht so aus:

// Benutzer zusammen mit ihren Posts, in einem einzigen Aufruf
const users = await prisma.user.findMany({
  where: { posts: { some: { title: { contains: 'test' } } } },
  include: { posts: true },
});
// users[0].posts ist bereits ein typisiertes Array – kein manuelles Umformen nötig

Raw SQL bedeutet, den Abfragetext selbst zu schreiben und ihn über einen Datenbanktreiber auszuführen, um anschließend die flachen Ergebniszeilen in die von der Anwendung benötigte Struktur umzuformen. Sie haben die vollständige Kontrolle über die exakte Abfrage, tragen aber auch die Verantwortung für das Mapping. Derselbe Lesevorgang „Benutzer mit ihren Posts” gegen pg erfordert einen Join und ein manuelles Umgruppieren:

const { rows } = await pool.query(
  `SELECT u.id, u.name, p.id AS post_id, p.title
   FROM users u
   LEFT JOIN posts p ON p.author_id = u.id
   WHERE p.title LIKE $1`,
  ['%test%'],
);

// Flache Zeilen -> verschachtelte Benutzer, manuell zusammengebaut
const users = Object.values(
  rows.reduce((acc, r) => {
    acc[r.id] ??= { id: r.id, name: r.name, posts: [] };
    if (r.post_id) acc[r.id].posts.push({ id: r.post_id, title: r.title });
    return acc;
  }, {} as Record<number, { id: number; name: string; posts: any[] }>),
);

Dieses reduce ist der eigentliche Kostenpunkt von Raw SQL bei relationalen Lesevorgängen: Die Datenbank liefert Zeilen zurück, keine Objektgraphen – und Sie schreiben die Join-und-Regroup-Logik jedes Mal neu. Der $1-Platzhalter leistet ebenfalls wichtige Arbeit – dazu mehr im Abschnitt zur Sicherheit.

ORM vs. Raw SQL: Der Vergleich, der die Entscheidung tatsächlich bestimmt

Die Entscheidung hängt von fünf Kriterien ab. So schneiden die drei Ansätze – Full ORM, Query Builder und Raw SQL – im Vergleich ab:

KriteriumFull ORM (Prisma, TypeORM)Query Builder (Drizzle, Kysely, Knex)Raw SQL
Injection-SicherheitStandardmäßig parametrisiertStandardmäßig parametrisiertNur mit parametrisierten Statements sicher
Entwicklungsgeschwindigkeit (CRUD)Am schnellsten – Relationen und Hydratisierung werden übernommenSchnell – SQL-ähnlich, Joins selbst schreibenAm langsamsten – manuelles Mapping
Migrationen / SchemaErstklassig (Prisma Migrate, TypeORM)Integriert oder gekoppeltes Tool (Drizzle Kit, Knex Migrations)Handgeschriebene DDL-Skripte
Portabilität über DBs hinwegHoch – Provider mit wenig Aufwand wechselnMittel – dialektbewusstNiedrig – dialektspezifisches SQL
Kontrolle / DB-spezifische FeaturesEingeschränkt; Escape Hatch erforderlichHoch – nah an SQLVollständig – jedes Feature, jeder Hint
TypsicherheitEnd-to-end aus dem SchemaStark (Kysely/Drizzle); Best-Effort (Knex)Keine ohne zusätzliches Tooling

Beim Thema Sicherheit ist die praktische Wahrheit einfach: ORMs und Query Builder parametrisieren Werte standardmäßig, was den klassischen Injection-Angriffsvektor schließt. Raw SQL ist nur so sicher wie Ihre eigene Disziplin – Prismas eigene Dokumentation warnt, dass wenn Sie die unsichere Methode mit Benutzereingaben verwenden, öffnen Sie die Möglichkeit für SQL-Injection-Angriffe, die Ihre Daten der Manipulation oder Löschung aussetzen können. Verwenden Sie Platzhalter ($1, ?) und interpolieren Sie niemals Benutzereingaben in den Abfrage-String – dann ist Raw SQL sicher; verzichten Sie auf diese Disziplin, ist es das nicht.

Bei Migrationen und Portabilität gewinnen ORMs im Standardfall. Eine Schemaänderung wird zu einer versionierten, wiederholbaren Migration, die in allen Umgebungen identisch ausgeführt wird, und der Wechsel des Datenbankanbieters ist größtenteils eine Konfigurationsänderung. Raw SQL bietet beides nicht von Haus aus – Sie verwalten DDL-Skripte selbst, und Ihre Abfragen sind an die Syntax eines bestimmten Dialekts gebunden. Bei der Kontrolle gewinnt Raw SQL eindeutig: Window-Funktionen, rekursive CTEs, datenbankspezifische Index-Hints und EXPLAIN ANALYZE-Tuning sind alle verfügbar, ohne gegen eine Abstraktion ankämpfen zu müssen.

Die Performance-Wahrheit: Es ist N+1, nicht die Abstraktion

Die am häufigsten geäußerte Kritik an ORMs – „sie generieren langsame Abfragen” – ist für CRUD größtenteils falsch und für ein spezifisches Muster größtenteils richtig. Moderne ORMs generieren kompetentes SQL für alltägliche Lese- und Schreibvorgänge. Der eigentliche Performance-Nachteil ist das N+1-Query-Problem: Sie laden eine Liste von N Zeilen und lösen dann für jede Zeile eine weitere Abfrage aus, um eine Relation zu laden – so wird ein einzelner Seitenaufruf zu N+1 Datenbankroundtrips.

Hier ist das Fehlermuster, das sich meist hinter einer harmlos wirkenden Schleife verbirgt:

// 1 Abfrage für die Benutzer...
const users = await prisma.user.findMany();

// ...dann N weitere, eine pro Benutzer — das ist die N+1-Falle
for (const user of users) {
  user.posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

Die Lösung besteht nicht darin, den ORM aufzugeben. Sie laden die Relation per Eager Loading in einer einzigen Abfrage und wählen nur die Spalten aus, die tatsächlich gerendert werden:

const users = await prisma.user.findMany({
  include: { posts: { select: { id: true, title: true } } },
});

Das reduziert 1 + N Abfragen auf eine einzige. Das benutzerseitige Symptom eines nicht behobenen N+1-Problems ist eine Listen- oder Dashboard-Seite, die mit Seed-Daten flott ist und unter Produktionslast langsam wird – genau die Art von Frontend-Latenz, die ein Session-Replay von der Nutzerseite aufzeigt und Sie zurück zur Abfrageschicht führt, um Abfragen zu zählen und Eager Loading hinzuzufügen.

Untersuchen Sie das generierte SQL, bevor Sie das Tool beschuldigen

Bevor Sie zu dem Schluss kommen, dass ein ORM langsam ist, schauen Sie sich an, was er tatsächlich ausgibt. Prisma protokolliert jede Abfrage, wenn Sie den Client mit log: ['query'] instanziieren; Kysely stellt .compile() bereit, um das SQL und die Parameter zurückzugeben; und Drizzle bietet zu demselben Zweck eine .toSQL()-Methode auf einer Abfrage. Führen Sie dann EXPLAIN ANALYZE auf der Ausgabe aus, um den Plan zu sehen, den die Datenbank tatsächlich ausführt.

const prisma = new PrismaClient({ log: ['query'] });
// Jede Abfrage, die Prisma ausführt, wird jetzt ausgegeben — zählen Sie sie, kopieren Sie sie in EXPLAIN ANALYZE

Das verwandelt „der ORM ist langsam” von einem Bauchgefühl in einen nachvollziehbaren Workflow: SQL lesen, Roundtrips zählen, den Plan prüfen.

Ein Hinweis zur Engine selbst: Eine der bedeutendsten Änderungen in Prisma 7 ist die vollständige Entfernung der Rust-basierten Query Engine zugunsten einer TypeScript-Implementierung. Der neue Query Compiler läuft in TypeScript und WebAssembly, was den sprachübergreifenden Serialisierungsschritt eliminiert und zu schnellerer Abfrageausführung führt. Da die Engine nicht mehr von einem nativen Binary abhängt, können Sie Prisma nun in Umgebungen einsetzen, die JavaScript oder WASM unterstützen, wie Cloudflare Workers, Bun und Deno. Prisma berichtet, dass das Rewrite oft deutlich schneller ist, wo es am meisten zählt – bei großen und komplexen Abfragen –, während es bei einfacheren Abfragen vergleichbar bleibt. Der Punkt für diesen Vergleich: Die Engine-Debatte hat ihre Form verändert, ist aber nicht verschwunden – und sie ist nach wie vor zweitrangig gegenüber der Abfrageanzahl. Bei der überwältigenden Mehrheit langsamer Seiten sind N+1 und Over-Fetching die Ursache, nicht die Abstraktionsschicht.

Query Builder: Die dritte Option, die das binäre Framing übersieht

Das Framing „ORM vs. Raw SQL” verdeckt eine starke Mittelposition. Query Builder wie Drizzle, Kysely und Knex ermöglichen es Ihnen, SQL-ähnliche, typgeprüfte Abfragen zu schreiben und die vollständige Kontrolle über das generierte SQL zu behalten – ohne die Object-Mapping-Schicht eines ORM. Kysely ist der leistungsstärkste typsichere SQL-Query-Builder für TypeScript, der in der Produktion von Deno, Maersk und Cal.com eingesetzt wird – mit modernem TypeScript und ohne Runtime-Overhead. Es ist eine dünne Abstraktionsschicht über SQL, die auf Vertrautheit durch Benennung und Struktur sowie auf Vorhersagbarkeit durch 1:1-Kompilierung setzt.

const users = await db
  .selectFrom('users')
  .innerJoin('posts', 'posts.author_id', 'users.id')
  .select(['users.id', 'users.name', 'posts.title'])
  .where('posts.title', 'like', '%test%')
  .execute();

Die Form spiegelt SQL wider, aber Spalten- und Tabellennamen werden gegen Ihr Schema typgeprüft. Wie Kyselys Dokumentation es formuliert, gibt ein Query Builder Ihnen die vollständige Kontrolle über Ihr SQL, während TypeScript Fehler frühzeitig erkennt, bevor Ihr Code überhaupt ausgeführt wird. Knex ist die langjährig bewährte Option und funktioniert mit Postgres, MySQL, SQLite und mehr – beachten Sie jedoch seine Typisierungsgrenzen: Laut der eigenen Dokumentation ist TypeScript-Unterstützung Best-Effort, nicht alle Verwendungsmuster können typgeprüft werden, und das Fehlen von Typfehlern garantiert derzeit nicht, dass die generierten Abfragen korrekt sind – daher werden Tests auch mit TypeScript empfohlen. Für ein neues TS-Projekt, das Typsicherheit ohne ORM-Overhead möchte, ist Kysely oder Drizzle die bessere Wahl.

Eine Entscheidungsregel nach Szenario

Passen Sie das Werkzeug an die Aufgabe an, anstatt eines für die gesamte Codebasis zu wählen:

  1. Einfache bis mittlere CRUD-Operationen, ein Team und ein sich entwickelndes Schema → ORM. Sie erhalten parametrisierte Abfragen, versionierte Migrationen, Relationsladung und End-to-End-Typen. Dies deckt den Großteil des Anwendungscodes ab.
  2. Reporting, Analysen, Massenoperationen oder ein gemessener Hot Path → Raw SQL, Views oder Stored Procedures. Wenn Sie Window-Funktionen, komplexe Aggregationen oder mit EXPLAIN ANALYZE verifizierte, handoptimierte Pläne benötigen, schreiben Sie das SQL direkt.
  3. Sie möchten Typsicherheit und SQL-Kontrolle ohne ORM → Query Builder. Drizzle oder Kysely bieten SQL-ähnliche, typisierte Abfragen mit vorhersagbarer Ausgabe und ohne verstecktes N+1-Verhalten.

Der hybride Ansatz, auf den die meisten Produktionsanwendungen hinauslaufen

In der Praxis wählen ausgereifte Codebasen keine Seite – sie verwenden einen ORM für den Großteil der Arbeit und behalten einen Raw-SQL-Escape-Hatch für den Rest. Prisma macht dies mit TypedSQL explizit: ein Feature von Prisma ORM, das es Ihnen ermöglicht, Ihre Raw-SQL-Abfragen vollständig typsicher zu schreiben. Sie schreiben SQL in einer .sql-Datei und rufen es über eine generierte, typisierte Funktion auf:

import { getUsersByAge } from './generated/prisma/sql';
const users = await prisma.$queryRawTyped(getUsersByAge(18, 30));

Es handelt sich um ein Preview-Feature: Sie aktivieren es mit previewFeatures = ["typedSql"] im Generator-Block, und da es statisch typisiert ist, kann es bestimmte Szenarien wie dynamisch generierte WHERE-Klauseln möglicherweise nicht verarbeiten – diese benötigen weiterhin die untypisierten Raw-Methoden. Prisma beschreibt den Workflow genau so, wie es dieser Artikel tut: eine übergeordnete Abstraktion, die die Mehrheit der Abfragen bedient, plus ein typsicherer Escape Hatch für Fälle, in denen Sie SQL direkt formulieren müssen. Deshalb lautet die moderne Antwort auf „ORM oder Raw SQL?” meist: „Ein ORM (oder Query Builder), der Ihnen erlaubt, bei Bedarf auf Raw SQL auszuweichen.”

Fazit

Wählen Sie den ORM oder Query Builder als Standard für alltägliche CRUD-Operationen, greifen Sie auf Raw SQL für Abfragen zurück, die es nachweislich verdienen, und behandeln Sie beides als Schichten einer einzigen Datenzugriffsstrategie – nicht als rivalisierende Lager. Der konkrete nächste Schritt bei jedem bestehenden Projekt: Aktivieren Sie Query Logging, identifizieren Sie Listen- und Dashboard-Endpunkte, zählen Sie die Abfragen pro Request, und beheben Sie N+1-Probleme mit Eager Loading und engerer Spaltenauswahl – dieser einzelne Durchgang bringt in der Regel mehr Performance zurück als jedes ORM-versus-SQL-Rewrite es je könnte.

FAQs

Was ist der Unterschied zwischen einem ORM und einem Query Builder?

Ein ORM bildet Datenbanktabellen auf Objekte ab und hydratisiert Abfrageergebnisse in typisierte Modelle – er übernimmt Relationen und Object-Mapping für Sie. Ein Query Builder hingegen generiert typgeprüftes SQL, gibt aber einfache Zeilen ohne Object-Mapping-Schicht zurück. Prisma und TypeORM sind ORMs; Drizzle, Kysely und Knex sind Query Builder. Query Builder liegen näher an SQL, bieten mehr Kontrolle über die generierte Abfrage mit weniger Abstraktion und ohne verstecktes Relationsladeverhalten.

Verhindert die Verwendung eines ORM SQL-Injection?

Ja, bei normaler Verwendung. ORMs und Query Builder parametrisieren Werte standardmäßig und senden Benutzereingaben als gebundene Parameter statt sie in den Abfrage-String zu interpolieren – das schließt den klassischen Injection-Angriffsvektor. Die Ausnahme sind unsichere Raw-Query-Methoden: Prismas Dokumentation warnt, dass die Verwendung der unsicheren Methode mit Benutzereingaben die Möglichkeit für SQL-Injection öffnet. Raw SQL ist nur so sicher wie Ihre Disziplin beim Einsatz von Platzhaltern wie `$1` oder `?`.

Ist Raw SQL schneller als ein ORM?

In den meisten realen Fällen nicht. Moderne ORMs generieren kompetentes SQL für alltägliche CRUD-Operationen, und der übliche Performance-Unterschied entsteht durch die Abfrageanzahl, nicht durch die Abstraktion. Die häufigste Ursache für langsame Seiten ist das N+1-Muster, bei dem das Laden einer Liste und ihrer Relationen zeilenweise Hunderte von Roundtrips erzeugt. Raw SQL lohnt sich für Reporting-, Analyse- und gemessene Hot-Path-Abfragen, die wirklich handoptimierte Pläne benötigen.

Wie sehe ich das tatsächliche SQL, das ein ORM generiert?

Jedes Tool stellt das generierte SQL direkt bereit. Prisma protokolliert jede Abfrage, wenn Sie den Client mit aktivierter Query-Log-Option instanziieren. Kysely bietet eine `compile`-Methode, die den SQL-String und seine Parameter zurückgibt. Drizzle stellt zu demselben Zweck eine `toSQL`-Methode auf einer Abfrage bereit. Sobald Sie das SQL haben, führen Sie `EXPLAIN ANALYZE` in PostgreSQL darauf aus, um den Ausführungsplan zu untersuchen und zu prüfen, ob Indizes wie erwartet verwendet werden.

Kann ich Raw SQL innerhalb eines ORM-Projekts schreiben?

Ja, und das ist das gängige Produktionsmuster. Die meisten ORMs behalten einen Raw-SQL-Escape-Hatch neben ihren generierten Abfragen. Prisma bietet TypedSQL, ein Preview-Feature, das mit dem `typedSql`-Preview-Flag aktiviert wird und es Ihnen ermöglicht, SQL in einer `.sql`-Datei zu schreiben und es über eine generierte, vollständig typisierte Funktion aufzurufen. Da es statisch typisiert ist, verarbeitet es keine dynamisch generierten WHERE-Klauseln – diese benötigen weiterhin die untypisierten Raw-Query-Methoden.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.