Comment puis-je (dans MongoDB) combiner des données de plusieurs collections en une seule?
Puis-je utiliser map-réduire et si oui, comment?
J'apprécierais beaucoup un exemple, étant novice.
Bien que vous ne puissiez pas effectuer cette opération en temps réel, vous pouvez exécuter plusieurs fois map-réduire pour fusionner des données à l’aide de l’option "réduire" dans MongoDB 1.8+ map/reduction (voir http: // www. mongodb.org/display/DOCS/MapReduce#MapReduce-Outputoptions ). Vous devez avoir une clé dans les deux collections que vous pouvez utiliser en tant que _id.
Par exemple, supposons que vous ayez une collection users
et une collection comments
et que vous souhaitiez créer une nouvelle collection contenant des informations démographiques sur l'utilisateur pour chaque commentaire.
Supposons que la collection users
comporte les champs suivants:
Et puis la collection comments
contient les champs suivants:
Vous feriez cette carte/réduire:
var mapUsers, mapComments, reduce;
db.users_comments.remove();
// setup sample data - wouldn't actually use this in production
db.users.remove();
db.comments.remove();
db.users.save({firstName:"Rich",lastName:"S",gender:"M",country:"CA",age:"18"});
db.users.save({firstName:"Rob",lastName:"M",gender:"M",country:"US",age:"25"});
db.users.save({firstName:"Sarah",lastName:"T",gender:"F",country:"US",age:"13"});
var users = db.users.find();
db.comments.save({userId: users[0]._id, "comment": "Hey, what's up?", created: new ISODate()});
db.comments.save({userId: users[1]._id, "comment": "Not much", created: new ISODate()});
db.comments.save({userId: users[0]._id, "comment": "Cool", created: new ISODate()});
// end sample data setup
mapUsers = function() {
var values = {
country: this.country,
gender: this.gender,
age: this.age
};
emit(this._id, values);
};
mapComments = function() {
var values = {
commentId: this._id,
comment: this.comment,
created: this.created
};
emit(this.userId, values);
};
reduce = function(k, values) {
var result = {}, commentFields = {
"commentId": '',
"comment": '',
"created": ''
};
values.forEach(function(value) {
var field;
if ("comment" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.Push(value);
} else if ("comments" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.Push.apply(result.comments, value.comments);
}
for (field in value) {
if (value.hasOwnProperty(field) && !(field in commentFields)) {
result[field] = value[field];
}
}
});
return result;
};
db.users.mapReduce(mapUsers, reduce, {"out": {"reduce": "users_comments"}});
db.comments.mapReduce(mapComments, reduce, {"out": {"reduce": "users_comments"}});
db.users_comments.find().pretty(); // see the resulting collection
À ce stade, vous aurez une nouvelle collection appelée users_comments
qui contient les données fusionnées et que vous pouvez maintenant utiliser. Ces collections réduites ont toutes _id
qui est la clé que vous émettiez dans vos fonctions de carte, puis toutes les valeurs sont un sous-objet à l'intérieur de la clé value
- les valeurs ne sont pas au niveau supérieur. de ces documents réduits.
Ceci est un exemple assez simple. Vous pouvez répéter cette opération avec davantage de collections autant que vous le souhaitez pour continuer à construire la collection réduite. Vous pouvez également faire des résumés et des agrégations de données dans le processus. Il est probable que vous définiriez plusieurs fonctions de réduction au fur et à mesure que la logique d'agrégation et de préservation des champs existants deviendrait plus complexe.
Vous noterez également qu'il existe désormais un document pour chaque utilisateur avec tous ses commentaires dans un tableau. Si nous fusionnions des données ayant une relation un à un plutôt qu'un à plusieurs, le résultat serait plat et vous pourriez simplement utiliser une fonction de réduction comme celle-ci:
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
Si vous voulez aplatir la collection users_comments
afin qu'elle ne compte qu'un document par commentaire, lancez également ceci:
var map, reduce;
map = function() {
var debug = function(value) {
var field;
for (field in value) {
print(field + ": " + value[field]);
}
};
debug(this);
var that = this;
if ("comments" in this.value) {
this.value.comments.forEach(function(value) {
emit(value.commentId, {
userId: that._id,
country: that.value.country,
age: that.value.age,
comment: value.comment,
created: value.created,
});
});
}
};
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
db.users_comments.mapReduce(map, reduce, {"out": "comments_with_demographics"});
Cette technique ne doit absolument pas être réalisée à la volée. Il convient à un travail cron ou à quelque chose du genre qui met à jour les données fusionnées périodiquement. Vous voudrez probablement exécuter ensureIndex
sur la nouvelle collection pour vous assurer que les requêtes que vous exécutez contre elle s'exécutent rapidement (gardez à l'esprit que vos données sont toujours dans une clé value
, de sorte que vous devez indexer comments_with_demographics
sur le commentaire created
le temps, ce serait db.comments_with_demographics.ensureIndex({"value.created": 1});
MongoDB 3.2 permet désormais de combiner les données de plusieurs collections en une seule à travers la phase étape de l’agrégation $ lookup . À titre d'exemple pratique, supposons que vous disposiez de données sur des livres divisés en deux collections différentes.
Première collection, appelée books
, contenant les données suivantes:
{
"isbn": "978-3-16-148410-0",
"title": "Some cool book",
"author": "John Doe"
}
{
"isbn": "978-3-16-148999-9",
"title": "Another awesome book",
"author": "Jane Roe"
}
Et la deuxième collection, appelée books_selling_data
, contenant les données suivantes:
{
"_id": ObjectId("56e31bcf76cdf52e541d9d26"),
"isbn": "978-3-16-148410-0",
"copies_sold": 12500
}
{
"_id": ObjectId("56e31ce076cdf52e541d9d28"),
"isbn": "978-3-16-148999-9",
"copies_sold": 720050
}
{
"_id": ObjectId("56e31ce076cdf52e541d9d29"),
"isbn": "978-3-16-148999-9",
"copies_sold": 1000
}
Pour fusionner les deux collections, il suffit d'utiliser $ lookup de la manière suivante:
db.books.aggregate([{
$lookup: {
from: "books_selling_data",
localField: "isbn",
foreignField: "isbn",
as: "copies_sold"
}
}])
Après cette agrégation, la collection books
ressemblera à ceci:
{
"isbn": "978-3-16-148410-0",
"title": "Some cool book",
"author": "John Doe",
"copies_sold": [
{
"_id": ObjectId("56e31bcf76cdf52e541d9d26"),
"isbn": "978-3-16-148410-0",
"copies_sold": 12500
}
]
}
{
"isbn": "978-3-16-148999-9",
"title": "Another awesome book",
"author": "Jane Roe",
"copies_sold": [
{
"_id": ObjectId("56e31ce076cdf52e541d9d28"),
"isbn": "978-3-16-148999-9",
"copies_sold": 720050
},
{
"_id": ObjectId("56e31ce076cdf52e541d9d28"),
"isbn": "978-3-16-148999-9",
"copies_sold": 1000
}
]
}
Il est important de noter quelques points:
books_selling_data
, ne peut pas être partagée.Donc, en conclusion, si vous voulez consolider les deux collections en ayant, dans ce cas, un champ plat copies_sold avec le nombre total de copies vendues, vous devrez travailler un peu plus, en utilisant probablement une collection intermédiaire qui, ensuite, be $ out à la collection finale.
S'il n'y a pas d'insertion en bloc dans mongodb, nous bouclons tous les objets du small_collection
et les insérons un par un dans le big_collection
:
db.small_collection.find().forEach(function(obj){
db.big_collection.insert(obj)
});
Exemple très basique avec $ lookup.
db.getCollection('users').aggregate([
{
$lookup: {
from: "userinfo",
localField: "userId",
foreignField: "userId",
as: "userInfoData"
}
},
{
$lookup: {
from: "userrole",
localField: "userId",
foreignField: "userId",
as: "userRoleData"
}
},
{ $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }}
])
Ici est utilisé
{ $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }}
Au lieu de
{ $unwind:"$userRoleData"}
{ $unwind:"$userRoleData"}
Parce que {$ unwind: "$ userRoleData"} cela retournera un résultat vide ou 0 si aucun enregistrement correspondant n'a été trouvé avec $ lookup.
utilisez multiple $ lookup pour plusieurs collections en agrégation
requête:
db.getCollection('servicelocations').aggregate([
{
$match: {
serviceLocationId: {
$in: ["36728"]
}
}
},
{
$lookup: {
from: "orders",
localField: "serviceLocationId",
foreignField: "serviceLocationId",
as: "orders"
}
},
{
$lookup: {
from: "timewindowtypes",
localField: "timeWindow.timeWindowTypeId",
foreignField: "timeWindowTypeId",
as: "timeWindow"
}
},
{
$lookup: {
from: "servicetimetypes",
localField: "serviceTimeTypeId",
foreignField: "serviceTimeTypeId",
as: "serviceTime"
}
},
{
$unwind: "$orders"
},
{
$unwind: "$serviceTime"
},
{
$limit: 14
}
])
résultat:
{
"_id" : ObjectId("59c3ac4bb7799c90ebb3279b"),
"serviceLocationId" : "36728",
"regionId" : 1.0,
"zoneId" : "DXBZONE1",
"description" : "AL HALLAB REST EMIRATES MALL",
"locationPriority" : 1.0,
"accountTypeId" : 1.0,
"locationType" : "SERVICELOCATION",
"location" : {
"makani" : "",
"lat" : 25.119035,
"lng" : 55.198694
},
"deliveryDays" : "MTWRFSU",
"timeWindow" : [
{
"_id" : ObjectId("59c3b0a3b7799c90ebb32cde"),
"timeWindowTypeId" : "1",
"Description" : "MORNING",
"timeWindow" : {
"openTime" : "06:00",
"closeTime" : "08:00"
},
"accountId" : 1.0
},
{
"_id" : ObjectId("59c3b0a3b7799c90ebb32cdf"),
"timeWindowTypeId" : "1",
"Description" : "MORNING",
"timeWindow" : {
"openTime" : "09:00",
"closeTime" : "10:00"
},
"accountId" : 1.0
},
{
"_id" : ObjectId("59c3b0a3b7799c90ebb32ce0"),
"timeWindowTypeId" : "1",
"Description" : "MORNING",
"timeWindow" : {
"openTime" : "10:30",
"closeTime" : "11:30"
},
"accountId" : 1.0
}
],
"address1" : "",
"address2" : "",
"phone" : "",
"city" : "",
"county" : "",
"state" : "",
"country" : "",
"zipcode" : "",
"imageUrl" : "",
"contact" : {
"name" : "",
"email" : ""
},
"status" : "ACTIVE",
"createdBy" : "",
"updatedBy" : "",
"updateDate" : "",
"accountId" : 1.0,
"serviceTimeTypeId" : "1",
"orders" : [
{
"_id" : ObjectId("59c3b291f251c77f15790f92"),
"orderId" : "AQ18O1704264",
"serviceLocationId" : "36728",
"orderNo" : "AQ18O1704264",
"orderDate" : "18-Sep-17",
"description" : "AQ18O1704264",
"serviceType" : "Delivery",
"orderSource" : "Import",
"takenBy" : "KARIM",
"plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"),
"plannedDeliveryTime" : "",
"actualDeliveryDate" : "",
"actualDeliveryTime" : "",
"deliveredBy" : "",
"size1" : 296.0,
"size2" : 3573.355,
"size3" : 240.811,
"jobPriority" : 1.0,
"cancelReason" : "",
"cancelDate" : "",
"cancelBy" : "",
"reasonCode" : "",
"reasonText" : "",
"status" : "",
"lineItems" : [
{
"ItemId" : "BNWB020",
"size1" : 15.0,
"size2" : 78.6,
"size3" : 6.0
},
{
"ItemId" : "BNWB021",
"size1" : 20.0,
"size2" : 252.0,
"size3" : 11.538
},
{
"ItemId" : "BNWB023",
"size1" : 15.0,
"size2" : 285.0,
"size3" : 16.071
},
{
"ItemId" : "CPMW112",
"size1" : 3.0,
"size2" : 25.38,
"size3" : 1.731
},
{
"ItemId" : "MMGW001",
"size1" : 25.0,
"size2" : 464.375,
"size3" : 46.875
},
{
"ItemId" : "MMNB218",
"size1" : 50.0,
"size2" : 920.0,
"size3" : 60.0
},
{
"ItemId" : "MMNB219",
"size1" : 50.0,
"size2" : 630.0,
"size3" : 40.0
},
{
"ItemId" : "MMNB220",
"size1" : 50.0,
"size2" : 416.0,
"size3" : 28.846
},
{
"ItemId" : "MMNB270",
"size1" : 50.0,
"size2" : 262.0,
"size3" : 20.0
},
{
"ItemId" : "MMNB302",
"size1" : 15.0,
"size2" : 195.0,
"size3" : 6.0
},
{
"ItemId" : "MMNB373",
"size1" : 3.0,
"size2" : 45.0,
"size3" : 3.75
}
],
"accountId" : 1.0
},
{
"_id" : ObjectId("59c3b291f251c77f15790f9d"),
"orderId" : "AQ137O1701240",
"serviceLocationId" : "36728",
"orderNo" : "AQ137O1701240",
"orderDate" : "18-Sep-17",
"description" : "AQ137O1701240",
"serviceType" : "Delivery",
"orderSource" : "Import",
"takenBy" : "KARIM",
"plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"),
"plannedDeliveryTime" : "",
"actualDeliveryDate" : "",
"actualDeliveryTime" : "",
"deliveredBy" : "",
"size1" : 28.0,
"size2" : 520.11,
"size3" : 52.5,
"jobPriority" : 1.0,
"cancelReason" : "",
"cancelDate" : "",
"cancelBy" : "",
"reasonCode" : "",
"reasonText" : "",
"status" : "",
"lineItems" : [
{
"ItemId" : "MMGW001",
"size1" : 25.0,
"size2" : 464.38,
"size3" : 46.875
},
{
"ItemId" : "MMGW001-F1",
"size1" : 3.0,
"size2" : 55.73,
"size3" : 5.625
}
],
"accountId" : 1.0
},
{
"_id" : ObjectId("59c3b291f251c77f15790fd8"),
"orderId" : "AQ110O1705036",
"serviceLocationId" : "36728",
"orderNo" : "AQ110O1705036",
"orderDate" : "18-Sep-17",
"description" : "AQ110O1705036",
"serviceType" : "Delivery",
"orderSource" : "Import",
"takenBy" : "KARIM",
"plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"),
"plannedDeliveryTime" : "",
"actualDeliveryDate" : "",
"actualDeliveryTime" : "",
"deliveredBy" : "",
"size1" : 60.0,
"size2" : 1046.0,
"size3" : 68.0,
"jobPriority" : 1.0,
"cancelReason" : "",
"cancelDate" : "",
"cancelBy" : "",
"reasonCode" : "",
"reasonText" : "",
"status" : "",
"lineItems" : [
{
"ItemId" : "MMNB218",
"size1" : 50.0,
"size2" : 920.0,
"size3" : 60.0
},
{
"ItemId" : "MMNB219",
"size1" : 10.0,
"size2" : 126.0,
"size3" : 8.0
}
],
"accountId" : 1.0
}
],
"serviceTime" : {
"_id" : ObjectId("59c3b07cb7799c90ebb32cdc"),
"serviceTimeTypeId" : "1",
"serviceTimeType" : "nohelper",
"description" : "",
"fixedTime" : 30.0,
"variableTime" : 0.0,
"accountId" : 1.0
}
}
Faire des unions dans MongoDB de manière 'SQL UNION' est possible en utilisant des agrégations avec des recherches, dans une seule requête. Voici un exemple que j'ai testé et qui fonctionne avec MongoDB 4.0:
// Create employees data for testing the union.
db.getCollection('employees').insert({ name: "John", type: "employee", department: "sales" });
db.getCollection('employees').insert({ name: "Martha", type: "employee", department: "accounting" });
db.getCollection('employees').insert({ name: "Amy", type: "employee", department: "warehouse" });
db.getCollection('employees').insert({ name: "Mike", type: "employee", department: "warehouse" });
// Create freelancers data for testing the union.
db.getCollection('freelancers').insert({ name: "Stephany", type: "freelancer", department: "accounting" });
db.getCollection('freelancers').insert({ name: "Martin", type: "freelancer", department: "sales" });
db.getCollection('freelancers').insert({ name: "Doug", type: "freelancer", department: "warehouse" });
db.getCollection('freelancers').insert({ name: "Brenda", type: "freelancer", department: "sales" });
// Here we do a union of the employees and freelancers using a single aggregation query.
db.getCollection('freelancers').aggregate( // 1. Use any collection containing at least one document.
[
{ $limit: 1 }, // 2. Keep only one document of the collection.
{ $project: { _id: '$$REMOVE' } }, // 3. Remove everything from the document.
// 4. Lookup collections to union together.
{ $lookup: { from: 'employees', pipeline: [{ $match: { department: 'sales' } }], as: 'employees' } },
{ $lookup: { from: 'freelancers', pipeline: [{ $match: { department: 'sales' } }], as: 'freelancers' } },
// 5. Union the collections together with a projection.
{ $project: { union: { $concatArrays: ["$employees", "$freelancers"] } } },
// 6. Unwind and replace root so you end up with a result set.
{ $unwind: '$union' },
{ $replaceRoot: { newRoot: '$union' } }
]);
Voici l'explication de comment cela fonctionne:
Instanciez une aggregate
collection sur toute de votre base de données contenant au moins un document. Si vous ne pouvez pas garantir qu'une collection de votre base de données ne sera pas vide, vous pouvez contourner ce problème en créant dans votre base de données une sorte de collection 'factice' contenant un seul document vide, spécialement conçu pour les requêtes d'union.
Faites en sorte que la première étape de votre pipeline soit { $limit: 1 }
. Cela enlèvera tous les documents de la collection sauf le premier.
Effacez tous les champs du document restant en utilisant une étape $project
:
{ $project: { _id: '$$REMOVE' } }
Votre agrégat contient maintenant un seul document vide. Il est temps d'ajouter des recherches pour chaque collection que vous souhaitez syndiquer. Vous pouvez utiliser le champ pipeline
pour effectuer un filtrage spécifique ou laisser localField
et foreignField
comme null pour faire correspondre la collection entière.
{ $lookup: { from: 'collectionToUnion1', pipeline: [...], as: 'Collection1' } },
{ $lookup: { from: 'collectionToUnion2', pipeline: [...], as: 'Collection2' } },
{ $lookup: { from: 'collectionToUnion3', pipeline: [...], as: 'Collection3' } }
Vous avez maintenant un agrégat contenant un seul document contenant 3 tableaux comme celui-ci:
{
Collection1: [...],
Collection2: [...],
Collection3: [...]
}
Vous pouvez ensuite les fusionner en un seul tableau en utilisant une étape $project
avec l'opérateur d'agrégation $concatArrays
:
{
"$project" :
{
"Union" : { $concatArrays: ["$Collection1", "$Collection2", "$Collection3"] }
}
}
Vous avez maintenant un agrégat contenant un seul document, dans lequel se trouve un tableau contenant votre union de collections. Ce qui reste à faire est d’ajouter une étape $unwind
et une étape $replaceRoot
pour scinder votre tableau en documents distincts:
{ $unwind: "$Union" },
{ $replaceRoot: { newRoot: "$Union" } }
Voilà. Vous avez maintenant un jeu de résultats contenant les collections que vous souhaitez unifier. Vous pouvez ensuite ajouter d'autres étapes pour le filtrer davantage, le trier, appliquer skip () et limit (). À peu près tout ce que vous voulez.
Mongorestore a cette fonctionnalité d’ajout sur ce qui se trouve déjà dans la base de données, ce comportement peut donc être utilisé pour combiner deux collections:
Je ne l’ai pas encore essayé, mais cela pourrait être plus rapide que l’approche carte/réduction.
Extrait de code. Courtesy-Plusieurs publications sur le débordement de pile, y compris celui-ci.
db.cust.drop();
db.Zip.drop();
db.cust.insert({cust_id:1, Zip_id: 101});
db.cust.insert({cust_id:2, Zip_id: 101});
db.cust.insert({cust_id:3, Zip_id: 101});
db.cust.insert({cust_id:4, Zip_id: 102});
db.cust.insert({cust_id:5, Zip_id: 102});
db.Zip.insert({Zip_id:101, Zip_cd:'AAA'});
db.Zip.insert({Zip_id:102, Zip_cd:'BBB'});
db.Zip.insert({Zip_id:103, Zip_cd:'CCC'});
mapCust = function() {
var values = {
cust_id: this.cust_id
};
emit(this.Zip_id, values);
};
mapZip = function() {
var values = {
Zip_cd: this.Zip_cd
};
emit(this.Zip_id, values);
};
reduceCustZip = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
if ("cust_id" in value) {
if (!("cust_ids" in result)) {
result.cust_ids = [];
}
result.cust_ids.Push(value);
} else {
for (field in value) {
if (value.hasOwnProperty(field) ) {
result[field] = value[field];
}
};
}
});
return result;
};
db.cust_Zip.drop();
db.cust.mapReduce(mapCust, reduceCustZip, {"out": {"reduce": "cust_Zip"}});
db.Zip.mapReduce(mapZip, reduceCustZip, {"out": {"reduce": "cust_Zip"}});
db.cust_Zip.find();
mapCZ = function() {
var that = this;
if ("cust_ids" in this.value) {
this.value.cust_ids.forEach(function(value) {
emit(value.cust_id, {
Zip_id: that._id,
Zip_cd: that.value.Zip_cd
});
});
}
};
reduceCZ = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
db.cust_Zip_joined.drop();
db.cust_Zip.mapReduce(mapCZ, reduceCZ, {"out": "cust_Zip_joined"});
db.cust_Zip_joined.find().pretty();
var flattenMRCollection=function(dbName,collectionName) {
var collection=db.getSiblingDB(dbName)[collectionName];
var i=0;
var bulk=collection.initializeUnorderedBulkOp();
collection.find({ value: { $exists: true } }).addOption(16).forEach(function(result) {
print((++i));
//collection.update({_id: result._id},result.value);
bulk.find({_id: result._id}).replaceOne(result.value);
if(i%1000==0)
{
print("Executing bulk...");
bulk.execute();
bulk=collection.initializeUnorderedBulkOp();
}
});
bulk.execute();
};
flattenMRCollection("mydb","cust_Zip_joined");
db.cust_Zip_joined.find().pretty();
Oui, vous pouvez: prendre cette fonction utilitaire que j'ai écrite aujourd'hui:
function shangMergeCol() {
tcol= db.getCollection(arguments[0]);
for (var i=1; i<arguments.length; i++){
scol= db.getCollection(arguments[i]);
scol.find().forEach(
function (d) {
tcol.insert(d);
}
)
}
}
Vous pouvez transmettre à cette fonction n’importe quel nombre de collections, la première étant la cible. Toutes les autres collections sont des sources à transférer à la cible.