ดึงเฉพาะองค์ประกอบที่สอบถามในอาร์เรย์วัตถุในคอลเลกชัน MongoDB


377

สมมติว่าคุณมีเอกสารต่อไปนี้ในคอลเล็กชันของฉัน:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

ทำแบบสอบถาม:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

หรือ

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})

ส่งคืนเอกสารที่จับคู่(เอกสาร 1)แต่จะเสมอกับรายการอาร์เรย์ทั้งหมดในshapes:

{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

อย่างไรก็ตามฉันต้องการรับเอกสาร(เอกสาร 1)เฉพาะกับอาร์เรย์ที่มีcolor=red:

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

ฉันจะทำสิ่งนี้ได้อย่างไร

คำตอบ:


416

MongoDB 2.2 $elemMatchผู้ดำเนินการเกี่ยวกับการฉายภาพใหม่ให้วิธีการอื่นในการปรับเปลี่ยนเอกสารที่ส่งคืนเพื่อให้มีเพียงองค์ประกอบแรกที่ตรงกันshapes:

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

ผลตอบแทน:

{"shapes" : [{"shape": "circle", "color": "red"}]}

ใน 2.2 คุณสามารถทำได้โดยใช้$ projection operator, โดยที่$ในชื่อฟิลด์วัตถุอ็อบเจกต์แสดงดัชนีขององค์ประกอบอาร์เรย์ที่ตรงกันครั้งแรกของเขตข้อมูลจากแบบสอบถาม ต่อไปนี้จะคืนค่าผลลัพธ์เดียวกันกับด้านบน:

db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

MongoDB 3.2 อัพเดท

เริ่มต้นด้วยการเปิดตัว 3.2 คุณสามารถใช้$filterตัวดำเนินการรวมตัวใหม่เพื่อกรองอาร์เรย์ระหว่างการฉายภาพซึ่งมีประโยชน์ในการรวมการจับคู่ทั้งหมดแทนที่จะเป็นตัวแรก

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

ผล:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]

15
วิธีการแก้ปัญหาถ้าฉันต้องการให้ทุกองค์ประกอบที่ตรงกับมันแทนที่จะเป็นเพียงครั้งแรก?
Steve Ng

ฉันเกรงว่าฉันกำลังใช้Mongo 3.0.X :-(
charliebrownie

@charliebrownie จากนั้นใช้หนึ่งในคำตอบอื่น ๆ aggregateที่ใช้
JohnnyHK

แบบสอบถามนี้จะส่งกลับเฉพาะ "รูปร่าง" ของอาร์เรย์และจะไม่ส่งคืนเขตข้อมูลอื่น ใครรู้วิธีคืนเขตข้อมูลอื่นด้วย
Mark Thien

1
สิ่งนี้ยังใช้งานได้:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul

97

Aggregation Frameworkใหม่ใน MongoDB 2.2+ เป็นทางเลือกให้กับ Map / Reduce $unwindผู้ประกอบการสามารถนำมาใช้ในการแยกของคุณshapesอาเรย์เข้าไปในกระแสของเอกสารที่สามารถจับคู่:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

ผลลัพธ์ใน:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}

6
@JohnnyHK: ในกรณีนี้$elemMatchเป็นตัวเลือกอื่น จริงๆแล้วฉันมาที่นี่ด้วยคำถาม Google Groupที่ $ elemMatch ไม่ทำงานเพราะมันจะคืนค่าการจับคู่ครั้งแรกต่อเอกสารเท่านั้น
Stennie

1
ขอบคุณฉันไม่ได้ตระหนักถึงข้อ จำกัด นั้นจึงเป็นสิ่งที่ดีที่จะรู้ ขออภัยที่ลบความคิดเห็นของฉันที่คุณตอบกลับฉันตัดสินใจที่จะโพสต์คำตอบอื่นแทนและไม่ต้องการทำให้ผู้คนสับสน
JohnnyHK

3
@JohnnyHK: ไม่กังวลขณะนี้มีสามคำตอบที่มีประโยชน์สำหรับคำถามที่ ;-)
Stennie

สำหรับผู้ค้นหาอื่น ๆ นอกจากนี้ฉันยังลองเพิ่ม{ $project : { shapes : 1 } }- ซึ่งดูเหมือนว่าจะใช้งานได้และจะเป็นประโยชน์หากเอกสารที่มีขนาดใหญ่และคุณต้องการดูshapesค่าคีย์
user1063287

2
@calmbird ฉันได้อัปเดตตัวอย่างเพื่อรวมขั้นตอนการแข่งขัน $ เริ่มต้นไว้ด้วย หากคุณสนใจคำแนะนำเกี่ยวกับคุณสมบัติที่มีประสิทธิภาพยิ่งขึ้นฉันจะเฝ้าดู / โหวตขึ้นSERVER-6612: สนับสนุนการฉายค่าหลายค่าในการฉายเช่นตัวระบุการฉาย $ elemMatchในตัวติดตามปัญหา MongoDB
Stennie

30

อีกวิธีหนึ่งที่ interesing คือการใช้$ ตรวจทานซึ่งเป็นหนึ่งในคุณสมบัติที่รวมตัวใหม่ของMongoDB 2.6 หากคุณใช้ 2.6 คุณไม่จำเป็นต้องใช้คำสั่ง $ ผ่อนคลายซึ่งอาจทำให้คุณมีปัญหาด้านประสิทธิภาพหากคุณมีอาร์เรย์ขนาดใหญ่

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "จำกัด เนื้อหาของเอกสารที่อยู่บนพื้นฐานของข้อมูลที่เก็บไว้ในเอกสารที่ตัวเอง" ดังนั้นมันจะทำงานเฉพาะด้านในของเอกสาร โดยทั่วไปจะสแกนเอกสารของคุณลงไปด้านล่างและตรวจสอบว่าตรงกับifเงื่อนไขของคุณที่อยู่ใน$condนั้นหรือไม่หากมีการจับคู่มันจะเก็บเนื้อหา ( $$DESCEND) หรือลบ ( $$PRUNE)

ในตัวอย่างข้างต้นก่อน$matchจะส่งกลับshapesอาร์เรย์ทั้งหมดและ $ redact ตัดมันลงไปตามผลลัพธ์ที่ต้องการ

โปรดทราบว่า{$not:"$color"}จำเป็นเพราะจะสแกนเอกสารบนสุดเช่นกันและหาก$redactไม่พบcolorฟิลด์ในระดับบนสุดจะส่งคืนfalseซึ่งอาจตัดเอกสารทั้งหมดที่เราไม่ต้องการ


1
คำตอบที่สมบูรณ์แบบ ตามที่คุณกล่าวถึง $ relax จะใช้แรมมาก ดังนั้นสิ่งนี้จะดีกว่าเมื่อเปรียบเทียบ
manojpt

ฉันมีข้อสงสัย ในตัวอย่าง "รูปร่าง" เป็นอาร์เรย์ "$ redact" จะสแกนวัตถุทั้งหมดในอาร์เรย์ "รูปร่าง" หรือไม่ วิธีนี้จะดีเมื่อเทียบกับประสิทธิภาพ?
manojpt

ไม่ใช่ทั้งหมด แต่เป็นผลลัพธ์ของการจับคู่ครั้งแรกของคุณ นั่นคือเหตุผลที่คุณใส่$matchเวทีรวมครั้งแรกของคุณ
Anvarik

okkk .. หากดัชนีที่สร้างในฟิลด์ "color" มันจะสแกนวัตถุทั้งหมดในอาร์เรย์ "รูปร่าง" ??? ซึ่งอาจเป็นวิธีที่มีประสิทธิภาพในการจับคู่วัตถุหลาย ๆ อย่างในอาร์เรย์?
manojpt

2
ยอดเยี่ยม! ฉันไม่เข้าใจว่า $ eq ทำงานอย่างไรที่นี่ ฉันทิ้งมันไว้ แต่เดิมและมันก็ไม่ได้ผลสำหรับฉัน อย่างใดมันก็ดูในอาร์เรย์ของรูปร่างเพื่อค้นหาการจับคู่ แต่แบบสอบถามไม่เคยระบุอาร์เรย์ที่จะมองเช่นถ้าเอกสารมีรูปร่างและขนาดตัวอย่างเช่น; $ eq จะค้นหาทั้งสองแถวในการจับคู่หรือไม่ $ redact แค่มองหาอะไรในเอกสารที่ตรงกับเงื่อนไข 'if' หรือไม่
Onosa

30

ข้อควรระวัง:คำตอบนี้ให้คำตอบที่เกี่ยวข้องในขณะนั้นก่อนที่จะมีฟีเจอร์ใหม่ของ MongoDB 2.2 ขึ้นไป ดูคำตอบอื่น ๆ หากคุณใช้ MongoDB รุ่นใหม่กว่า

พารามิเตอร์ตัวเลือกฟิลด์ถูก จำกัด คุณสมบัติที่สมบูรณ์ ไม่สามารถใช้เพื่อเลือกส่วนหนึ่งของอาร์เรย์ได้มีเพียงอาร์เรย์ทั้งหมดเท่านั้น ฉันลองใช้ตัวดำเนินการ$ ตำแหน่งแต่นั่นไม่ได้ผล

วิธีที่ง่ายที่สุดคือเพียงกรองรูปร่างในลูกค้า

หากคุณต้องการผลลัพธ์ที่ถูกต้องโดยตรงจาก MongoDB คุณสามารถใช้ map- ลดเพื่อกรองรูปร่าง

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()

24

ดีกว่าคุณสามารถสอบถามในการจับคู่องค์ประกอบอาร์เรย์โดยใช้$sliceมันจะมีประโยชน์ในการส่งคืนวัตถุสำคัญในอาร์เรย์

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

$sliceมีประโยชน์เมื่อคุณรู้ว่าดัชนีขององค์ประกอบ แต่บางครั้งคุณต้องการองค์ประกอบอาร์เรย์ใดที่ตรงกับเกณฑ์ของคุณ คุณสามารถส่งคืนองค์ประกอบที่ตรงกันด้วย$โอเปอเรเตอร์



12

ไวยากรณ์สำหรับ find ใน mongodb คือ

    db.<collection name>.find(query, projection);

และแบบสอบถามที่สองที่คุณเขียนนั่นคือ

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

ในส่วนนี้คุณได้ใช้$elemMatchโอเปอเรเตอร์ในส่วนของการสืบค้นในขณะที่ถ้าคุณใช้โอเปอเรเตอร์ในส่วนของการฉายภาพคุณจะได้ผลลัพธ์ที่ต้องการ คุณสามารถเขียนแบบสอบถามของคุณเป็น

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

สิ่งนี้จะให้ผลลัพธ์ที่ต้องการ


1
มันใช้งานได้สำหรับฉัน อย่างไรก็ตามปรากฏว่า"shapes.color":"red"ในพารามิเตอร์การสืบค้น (พารามิเตอร์แรกของวิธีการค้นหา) ไม่จำเป็น คุณสามารถแทนที่ด้วย{}และรับผลลัพธ์เดียวกัน
Erik Olson

2
@ErikOlson ข้อเสนอแนะของคุณถูกต้องในกรณีด้านบนซึ่งเราจำเป็นต้องค้นหาเอกสารทั้งหมดที่มีสีแดงและใช้การฉายภาพกับพวกเขาเท่านั้น แต่ถ้าสมมุติว่ามีใครต้องการค้นหาเอกสารทั้งหมดที่มีสีฟ้า แต่ควรคืนเฉพาะองค์ประกอบเหล่านั้นของอาร์เรย์รูปร่างที่มีสีแดง ในกรณีนี้แบบสอบถามดังกล่าวสามารถนำมาอ้างอิงโดยคนอื่นยัง ..
วิกกี้

ดูเหมือนว่าจะง่ายที่สุด แต่ฉันไม่สามารถใช้งานได้ มันจะส่งคืนเอกสารย่อยที่ตรงกันแรกเท่านั้น
นิวแมน

8

ขอบคุณJohnnyHK JohnnyHK

ที่นี่ฉันต้องการเพิ่มการใช้งานที่ซับซ้อนมากขึ้น

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}

7

คุณเพียงแค่ต้องเรียกใช้แบบสอบถาม

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

ผลลัพธ์ของแบบสอบถามนี้คือ

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

ตามที่คุณคาดหวังมันจะให้ฟิลด์ที่แน่นอนจากอาร์เรย์ที่ตรงกับสี: 'สีแดง'


3

พร้อมกับโครงการ $ มันจะเหมาะสมกว่าองค์ประกอบการจับคู่ที่ชาญฉลาดอื่น ๆ จะถูก clubbed พร้อมกับองค์ประกอบอื่น ๆ ในเอกสาร

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)

คุณช่วยอธิบายได้ไหมว่าสิ่งนี้ทำได้สำเร็จด้วยชุดอินพุตและเอาต์พุต
Alexander Mills

2

ในทำนองเดียวกันคุณสามารถค้นหาหลายรายการ

db.getCollection('localData').aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
  {$match: {'shapes.color': {$in : ['red','yellow'] } }},
  {$project: {
     shapes: {$filter: {
        input: '$shapes',
        as: 'shape',
        cond: {$in: ['$$shape.color', ['red', 'yellow']]}
     }}
  }}
])

คำตอบนี้เป็นวิธีที่ต้องการอย่างยิ่ง 4.x: $matchเพื่อลดพื้นที่แล้ว$filterเก็บสิ่งที่คุณต้องการเขียนทับฟิลด์อินพุต (ใช้เอาต์พุตของ$filterฟิลด์ออนshapesเพื่อ$projectกลับสู่shapesสไตล์โน้ต: ดีที่สุดที่จะไม่ใช้ชื่อฟิลด์เป็น การasโต้เถียงเพราะสามารถนำไปสู่ความสับสนในภายหลังด้วย$$shapeและ$shapeฉันชอบzzเป็นasฟิลด์เพราะมันโดดเด่นจริงๆ
Buzz Moschetti

1
db.test.find( {"shapes.color": "red"}, {_id: 0})

1
ยินดีต้อนรับสู่ Stack Overflow! ขอบคุณสำหรับข้อมูลโค้ดซึ่งอาจให้ความช่วยเหลือแบบ จำกัด และ จำกัด ทันที คำอธิบายที่เหมาะสมจะช่วยปรับปรุงมูลค่าในระยะยาวได้อย่างมากโดยการอธิบายว่าเหตุใดจึงเป็นวิธีแก้ปัญหาที่ดีและจะทำให้มีประโยชน์มากขึ้นสำหรับผู้อ่านในอนาคตด้วยคำถามที่คล้ายกัน โปรดแก้ไขคำตอบของคุณเพื่อเพิ่มคำอธิบายรวมถึงข้อสมมติที่คุณได้ทำไว้
sepehr

1

ใช้ฟังก์ชันการรวมและ$projectเพื่อรับฟิลด์วัตถุเฉพาะในเอกสาร

db.getCollection('geolocations').aggregate([ { $project : { geolocation : 1} } ])

ผลลัพธ์:

{
    "_id" : ObjectId("5e3ee15968879c0d5942464b"),
    "geolocation" : [ 
        {
            "_id" : ObjectId("5e3ee3ee68879c0d5942465e"),
            "latitude" : 12.9718313,
            "longitude" : 77.593551,
            "country" : "India",
            "city" : "Chennai",
            "zipcode" : "560001",
            "streetName" : "Sidney Road",
            "countryCode" : "in",
            "ip" : "116.75.115.248",
            "date" : ISODate("2020-02-08T16:38:06.584Z")
        }
    ]
}

0

แม้ว่าคำถามจะถูกถามเมื่อ 9.6 ปีที่แล้ว แต่สิ่งนี้ได้ช่วยคนจำนวนมาก แต่ฉันก็เป็นหนึ่งในนั้น ขอบคุณทุกคนสำหรับทุกข้อสงสัยคำแนะนำและคำตอบ รับจากคำตอบข้อใดข้อหนึ่งที่นี่ .. ฉันพบว่าวิธีการต่อไปนี้ยังสามารถใช้ในการฉายฟิลด์อื่น ๆ ในเอกสารหลักได้ซึ่งอาจเป็นประโยชน์กับใครบางคน

สำหรับเอกสารดังต่อไปนี้จำเป็นต้องตรวจสอบว่าพนักงาน (emp # 7839) มีประวัติการลาของเขาในปี 2020 หรือไม่ประวัติการลาถูกนำไปใช้เป็นเอกสารฝังตัวภายในเอกสารพนักงานผู้ปกครอง

db.employees.find( {"leave_history.calendar_year": 2020}, 
    {leave_history: {$elemMatch: {calendar_year: 2020}},empno:true,ename:true}).pretty()


{
        "_id" : ObjectId("5e907ad23997181dde06e8fc"),
        "empno" : 7839,
        "ename" : "KING",
        "mgrno" : 0,
        "hiredate" : "1990-05-09",
        "sal" : 100000,
        "deptno" : {
                "_id" : ObjectId("5e9065f53997181dde06e8f8")
        },
        "username" : "none",
        "password" : "none",
        "is_admin" : "N",
        "is_approver" : "Y",
        "is_manager" : "Y",
        "user_role" : "AP",
        "admin_approval_received" : "Y",
        "active" : "Y",
        "created_date" : "2020-04-10",
        "updated_date" : "2020-04-10",
        "application_usage_log" : [
                {
                        "logged_in_as" : "AP",
                        "log_in_date" : "2020-04-10"
                },
                {
                        "logged_in_as" : "EM",
                        "log_in_date" : ISODate("2020-04-16T07:28:11.959Z")
                }
        ],
        "leave_history" : [
                {
                        "calendar_year" : 2020,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                },
                {
                        "calendar_year" : 2021,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                }
        ]
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.