graphql - Optimierung von Datenbankabfragen

Bei der Arbeit mit Datenbanken tritt das Problem "SELECT N + 1" auf. Wenn eine Anwendung anstelle einer einzelnen Abfrage an die Datenbank, die alle erforderlichen Daten aus mehreren verwandten Tabellen und Sammlungen auswählt, eine zusätzliche Unterabfrage für jede Zeile des Ergebnisses der ersten Abfrage erstellt. verwandte Daten zu erhalten. Zum Beispiel erhalten wir zuerst eine Liste von Universitätsstudenten, in denen sein Fachgebiet durch eine Kennung gekennzeichnet ist, und dann führen wir für jeden der Studierenden eine zusätzliche Unterabfrage zu einer Tabelle oder Sammlung von Fachgebieten durch, um den Namen des Fachgebiets anhand des Kennzeichens des Fachgebiets zu ermitteln. Da für jede der Unterabfragen möglicherweise eine andere Unterabfrage und eine andere Unterabfrage erforderlich ist, nimmt die Anzahl der Abfragen an die Datenbank exponentiell zu.

Wenn Sie mit graphql arbeiten, ist es sehr einfach, das Problem „SELECT N + 1“ zu generieren, wenn Sie in der Resolver-Funktion eine Unterabfrage für die verknüpfte Tabelle durchführen. Das erste, was mir in den Sinn kommt, ist, eine Anfrage sofort unter Berücksichtigung aller zugehörigen Daten zu stellen. Dies muss jedoch, wie Sie zustimmen müssen, irrational sein, wenn die zugehörigen Daten nicht vom Kunden angefordert werden.

Eine der Lösungen für das Problem „SELECT N + 1“ für graphql wird in diesem Beitrag behandelt.

Nehmen Sie zum Beispiel zwei Sammlungen: "Autoren" (Autor) und "Bücher" (Buch). Die Beziehung ist, wie man annehmen würde, viele zu viele. Ein Autor kann mehrere Bücher haben, und ein Buch kann von mehreren Autoren geschrieben werden. Zum Speichern von Informationen verwenden wir die Mongodb-Datenbank und die Bibliothek mongoose.js

Wir erkennen die Beziehung zwischen vielen zu vielen Sammlungen mithilfe der zusätzlichen BookAuthor-Sammlung und virtuellen Feldern.

// Author.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ name: String }); schema.virtual('books', { ref: 'BookAuthor', localField: '_id', foreignField: 'author' }); module.exports = schema; 

 // Book.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ title: String }); schema.virtual('authors', { ref: 'BookAuthor', localField: '_id', foreignField: 'book' }); module.exports = schema; 

 // BookAuthor.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const schema = new Schema({ author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, book: { type: mongoose.Schema.Types.ObjectId, ref: 'Book' } }); module.exports = schema; 

 // mongoSchema.js const mongoose = require('mongoose'); const Author = require('./Author'); const Book = require('./Book'); const BookAuthor = require('./BookAuthor'); mongoose.connect('mongodb://localhost:27017/books') mongoose.set('debug', true); exports.Author = mongoose.model('Author', Author); exports.Book = mongoose.model('Book', Book); exports.BookAuthor = mongoose.model('BookAuthor', BookAuthor); 

Definieren Sie nun die Typen Autor und Buch in graphql. Es gibt ein kleines Problem mit der Tatsache, dass diese Typen gegenseitig referenziert werden. Für den gegenseitigen Zugriff wird daher die Bindung von Links an das Objekt des Exportmoduls verwendet und nicht die Bindung eines neuen Objekts an module.exports (das das ursprüngliche Objekt ersetzt). Das Feld field wird als Funktion implementiert, mit der Sie das Lesen des Links zum Objekt beim Erstellen verschieben können bis alle Zirkelverweise verfügbar sind:

 // graphqlType.js exports.Author = require('./Author'); exports.Book = require('./Book'); 

 // Author.js const graphql = require('graphql') const graphqlType = require('./index') module.exports = new graphql.GraphQLObjectType({ name: 'author', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, name: { type: graphql.GraphQLString, }, books: { type: new graphql.GraphQLList(graphqlType.Book), resolve: obj => obj.books && obj.books.map(book => book.book) } }) }); 

 // Book.js const graphql = require('graphql') const graphqlType = require('./index') module.exports = new graphql.GraphQLObjectType({ name: 'book', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, title: { type: graphql.GraphQLString, }, authors: { type: new graphql.GraphQLList(graphqlType.Author), resolve: obj => obj.authors && obj.authors.map(author => author.author) } }) }); 

Nun definieren wir die Anfrage der Autoren, möglicherweise mit einer Liste ihrer Bücher und möglicherweise mit einer Liste der Autoren (Mitautoren) dieser Bücher.

 const graphql = require('graphql'); const getFieldNames = require('graphql-list-fields'); const graphqlType = require('../graphqlType'); const mongoSchema = require('../mongoSchema'); module.exports = { type: new graphql.GraphQLList(graphqlType.Author), args: { _id: { type: graphql.GraphQLString } }, resolve: (_, {_id}, context, info) => { const fields = getFieldNames(info); const where = _id ? {_id} : {}; const authors = mongoSchema.Author.find(where) if (fields.indexOf('books.authors.name') > -1 ) { authors.populate({ path: 'books', populate: { path: 'book', populate: {path: 'authors', populate: {path: 'author'}} } }) } else if (fields.indexOf('books.title') > -1 ) { authors.populate({path: 'books', populate: {path: 'book'}}) } return authors.exec(); } }; 

Um festzustellen, welche Feldanforderung vom Client kam, wird die Bibliothek graphql-list-fields verwendet. Und wenn eine Anfrage mit verschachtelten Objekten kam, wird die populate () -Methode der Mungobibliothek aufgerufen.

Lassen Sie uns nun mit den Abfragen experimentieren. Die maximal mögliche Anfrage für unsere Implementierung:

 { author { _id name books { _id title authors { _id name } } } } 

wird durch 5 Aufrufe der Datenbank ausgeführt:

 authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} }) in': [ObjectId ( "5b0fcab305b15d38f672357d"), ObjectId ( "5b0fcabd05b15d38f672357e"), ObjectId ( "5b0fcac405b15d38f672357f"), ObjectId ( "5b0fcad705b15d38f6723580"), ObjectId ( "5b0fcae305b15d38f6723581"), ObjectId authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} }) 

Wie Sie sehen können, verwendet die Funktion mongoose.js - populate () - nicht die relativ neue mongodb-Funktion - $ lookup, sondern erstellt zusätzliche Anforderungen. Dies ist jedoch seitdem kein "SELECT N + 1" -Problem Eine neue Abfrage wird nicht für jede Zeile erstellt, sondern für die gesamte Sammlung. (Der Wunsch zu überprüfen, wie die Funktion mongoose.js populate () tatsächlich funktioniert - mit einer oder mehreren Anforderungen - war eines der Motive für die Auswahl einer nicht relationalen Datenbank für dieses Beispiel.)

Wenn wir eine minimalistische Abfrage verwenden:

 { author { _id name } } 

dann wird es nur einen Aufruf an die Datenbank bilden:

  authors.find({}, { fields: {} }) 

Das habe ich tatsächlich gesucht. Abschließend möchte ich sagen, dass ich, als ich nach Lösungen für dieses Problem suchte, sehr praktische und erweiterte Bibliotheken gefunden habe, die dieses Problem lösen. Einer von ihnen, den ich aufgrund der Struktur der relationalen Datenbank sehr mochte, bildete das graphql-Schema mit allen erforderlichen Operationen. Dieser Ansatz ist jedoch akzeptabel, wenn graphql auf der Backend-Seite der Anwendung verwendet wird. Wenn Sie den Zugriff auf solche Dienste über das Front-End der Anwendung öffnen (was ich benötigt habe), ähnelt dies dem Platzieren eines Admin-Panels für den Datenbankserver im Open Access, as Alle Tabellen sind sofort verfügbar

Zur Vereinfachung der Lesbarkeit befindet sich das Arbeitsbeispiel im Repository .

Ergänzung durch Joniks Kommentar

Der Benutzer von joniks im Feed hat auf die Bibliothek https://github.com/facebook/dataloader verwiesen. Mal sehen, wie Sie mit dieser Bibliothek das Problem "SELECT N + 1" bewältigen können.

In Anbetracht dieser Bibliothek würde eine graphql Authors-Typdefinition folgendermaßen aussehen:

 // Autors.js const graphql = require('graphql') const DataLoader = require('dataloader') const graphqlType = require('./index') const mongoSchema = require('../mongoSchema'); const bookLoader = new DataLoader(async ids => { const data = await mongoSchema.Book.find({ _id: { $in: ids }}).populate('authors').exec(); const books = data.reduce((obj, item) => (obj[item._id] = item) && obj, {}) const response = ids.map(id => books[id]); return response; }); module.exports = new graphql.GraphQLObjectType({ name: 'authors', description: '', fields: () => ({ _id: {type: graphql.GraphQLString}, name: { type: graphql.GraphQLString, }, books: { type: new graphql.GraphQLList(graphqlType.Books), resolve: obj => obj.books && obj.books.map(book => bookLoader.load(book.book)) } }) }); 


Was bringt die Verwendung dieser Bibliothek? Einzelne bookLoader.load (id) -Anforderungen werden akkumuliert und zur Verarbeitung mit einem Array von Bezeichnern gesendet. Const bookLoader = new DataLoader (async ids => {...
Bei der Ausgabe müssen wir das Versprechen eines Arrays oder ein Array von Versprechen zurückgeben, die sich in derselben Reihenfolge wie das IDs-Eingabearray befinden.

Jetzt können wir unsere Anfrage für die Autoren wie folgt umschreiben:

 // authors.js const graphql = require('graphql'); const getFieldNames = require('graphql-list-fields'); const graphqlType = require('../graphqlType'); const mongoSchema = require('../mongoSchema'); module.exports = { type: new graphql.GraphQLList(graphqlType.Authors), args: { _id: { type: graphql.GraphQLString } }, resolve: (_, {_id}, context, info) => { const fields = getFieldNames(info); const where = _id ? {_id} : {}; const authors = mongoSchema.Author.find(where).populate('books') return authors.exec(); } }; 


Infolgedessen können wir verwandte Objekte einer beliebigen Verschachtelungsebene abfragen, ohne uns um das SELECT N + 1-Problem kümmern zu müssen (allerdings auf Kosten eines bedingungslosen Aufrufs von populate (), selbst wenn es nicht benötigt wurde):

 { authors { _id name books { _id title authors { _id name books { _id title authors { _id name } } } } } } 


Aber hier müssen Sie wirklich verstehen, dass es auf jeder Ebene der Verschachtelung von Objekten eine aggregierte Abfrage gibt, wenn wir mit SQL-Servern arbeiten. Gleichzeitig ist es manchmal erforderlich, dass dies immer noch genau eine Anforderung ist. Dies können Sie jedoch nicht so direkt mit der Datenlader-Bibliothek erreichen. Ein Beispiel für Änderungen finden Sie im Data-Loader-Repository-Zweig.

apapacy@gmail.com
31. Mai 2018

Source: https://habr.com/ru/post/de412847/


All Articles