Last Updated on 3월 26, 2024 by Jade(정현호)
안녕하세요.
이번 포스팅에서는 MongoDB 에서의 데이터 모델링에 대한 내용과 다양한 조회 쿼리 종류 와 사용 예제에 대해서 확인해 보도록 하겠습니다.
Contents
MongoDB Data Modeling
모델링이란 실 세계의 내용을 그림과 도형으로 나타내며, 데이터가 저장되어 있는 모양 또는 틀을 의미합니다.
개념적 데이터 모델링 → 논리적 데이터 모델링 → 물리적 데이터 모델링
기존의 관계형 데이터베이스를 구축하기 위해서 필요 및 수행하였던 체계적인 분석, 설계가 필요한 것처럼 MongoDB도 체계적인 분석과 설계가 필요 합니다.
1) 개념적 데이터 모델링: 비즈니스 영역으로 부터 데이터를 수집하는 단계
2) 논리적 데이터 모델링: Document 구조에 맞게 분석, 설계하는 단계
3) 물리적 데이터 모델링: MongoDB 물리적 구조에 맞게 설계하는 단계
관계형 데이터베이스에서 ERD(Entity Relationship Diagram) 을 작성하는 것처럼 MongoDB의 경우에도 설계도인 CD(Collection Diagram) 을 작성을 하게 됩니다.
MongoDB의 설계 주요 특징
저장 환경 및 저장장소에 따른 데이터 모델링시 중점 사항
• 파일시스템(Fiesystem)에 저장시에는 프로세스 중심
• RDBMS 에 저장할 때는 데이터 중심
• 클라우드 컴퓨팅 환경의 NoSQL 에 저장할 때는 프로세스+데이터
MongoDB는 데이터와 프로세스 모두가 설계의 중심이 되게 됩니다.
Document Structure 에 따른 차이
관계형 Database 에서는 정규화를 통해 데이터의 중복을 제거하며, 무결성을 보장하는 설계를 지향하고, NoSQL은 데이터의 중복을 허용하며, 역정규화된 설계를 지향합니다.
데이터 중복이 일부 발생한다 하더라도 빠른 데이터 입력 및 처리 그리고 효율적인 관리가 보장될 수 있다면 비정규화 설계 구조도 고려할 수 있을 것입니다. MongoDB 에서는 이와 같은 설계 구조를 사용하고 있습니다.
기존의 관계형 Database는 일부 서버를 중심으로 데이터베이스를 구축하고 활용하는 ScaleUp 중심의 관리 기법을 사용하였다면, MongoDB는 여러 대의 서버를 중심으로 구축 및 확장, 복제하는 ScaleOut 중심의 관리 기법을 지향합니다.
또한 관계형 데이터베이스는 데이터 무결성을 통한 데이터의 중복을 제거하는 설계 방법을 지향한다면 MongoDB는 데이터 중복을 허용하여 유연성을 최대한 활용하는 설계방법을 제공 및 지향하고 있습니다.
중첩 구조
관계형 데이터베이스에서는 Entity간의 관계성(Relationship) 을 중심으로 데이터 무결성을 보장하지만 필요 이상의 JOIN 을 유발시킬 수 있으며 그로 인하여 검색 성능을 저하되는 원인이 될 수도 있습니다.
NoSQL은 중첩 데이터 구조를 쉽게 설계할 수 있기 때문에 불필요한 JOIN을 최소화시킬 수 있습니다.
MongoDB 는 이렇게 중첩 구조를 활용하여 사용할 수 있는 반면에 Relationship을 통한 데이터 무결성은 보장하지 않습니다.
MongoDB 의 설계 기준
데이터 조작에 따른: MongoDB는 하나의 Collection 안에서 여러 개의 필드와 그 필드 내에서 다른 필드가 있는 중첩된 형태로 구성되어 있습니다. 필드 중에서는 적은 데이터를 가진 필드도 있을 것이며, 대량의 데이터를 가진 필드도 존재할 것입니다.
또는 아주 빈번하게 조회되는 필드가 있는 반면에 아주 드물게(주마다 월마다) 조회되는 필드도 있을 수 있습니다.
이렇게 다양한 요건의 필드를 하나의 collection으로 생성하게 된다면 불필요한 리소스 사용을 할 수도 있습니다.
데이터 ACCESS 패턴: Collection 에 대해서 얼마나 많은 데이터를 자주 READ 하는지 WRITE 하는지에 대한 비율과 해당 데이터의 보관 주기에 대한 부분으로 설계를 결정하게 됩니다.
Collection 필드 중에서 쓰기 작업만 주로 수행되는 필드가 있는 반면, 읽기/쓰기 모두 동시에 빈번하게 발생되는 필드가 있을 수 있습니다. 이러한 부분들이 고려되어 Collection 에 대한 설계가 필요할 것입니다.
데이터 타입 및 저장 기술에 따른: MongoDB 에도 다양한 데이터 타입이 존재합니다. MongoDB가 지원하는 데이터 타입에 대한 이해가 필요하며, Large Object 를 저장하기 위한 GridFS 라는 기능도 지원하고 있기 때문에 MongoDB에서 지원하는 저장 기술에 대한 명확한 이해도 필요합니다.
다양한 도큐먼트 저장 방식과 조회
Collection 에 도큐먼트를 입력하는 방식에 따라서 이전에 설명한 중첩 형태로 데이터를 저장할 수 있으며 그에 따른 데이터 조회에 대해서 확인해보도록 하겠습니다.
Embedded Document Pattern
NoSQL은 조인이 없으며 데이터 결합 방식을 지양하는 방향입니다.
MongoDB에서도 관계형 데이터베이스의 명시적인 조인은 지원하지 않으나, 동일한 성격을 갖는 필드들을 묶어 Document-in-Document 형태로 구성할 수 있습니다.
좀 더 쉽게 표현하면 JSON 특징을 이용하여 필드들을 계층 구조로 구성할 수 있습니다.
임베디드(Embedded) 방식은 관계를 갖는 데이터 집합을 단일 도큐먼트에 포함하여 저장하는 방식입니다.
• Embedded 데이터 저장 구조 예시)
// Collection - ord { "_id" : ObjectId("63f65e65a1db2a17107e6c70"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y", "item_id" : [ { "item_id" : "1", "product_name" : "Bunny Boots", <=== Embedded sub-document "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_id" : "2", "product_name" : "Pro Ski Boots", "item_price" : "380", <=== Embedded sub-document "qty" : "400", "price" : "152000" } ] }
Embedded Structure 와 Extent Document 로 나눌 수 있으며 아래 테스트 케이스에서 내용을 확인해보도록 하겠습니다.
Embedded Structure
ord(주문) 컬렉션에 데이터를 입력할 때 주문 정보와 주문 상세 정보를 함께 저장하는 경우입니다.
> db.ord.insert( { ord_id : "201209012345", customer_name : "Woman & Sports", emp_name : "Magee", total : "601100", payment_type : "Credit", order_filled : "Y", item_id : [ { item_id : "1", product_name : "Bunny Boots", item_price : "135", qty : "500", price : "67000" }, { item_id : "2", product_name : "Pro Ski Boots", item_price : "380", qty : "400", price : "152000" } ] } ) > db.ord.find().pretty() { "_id" : ObjectId("640dcbaa7b5ed14ecbe08565"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y", "item_id" : [ { "item_id" : "1", "product_name" : "Bunny Boots", // <=== Embedded sub-document "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_id" : "2", "product_name" : "Pro Ski Boots", // <=== Embedded sub-document "item_price" : "380", "qty" : "400", "price" : "152000" } ] }
Extent Document
주문 정보(ord) 컬렉션을 먼저 생성 후에 추가로 주문 상세 정보를 update 메서드을 통해서 추가하는 방법입니다.
> db.ord.drop() // 주문정보 입력 > db.ord.insert( { ord_id : "201209012345", customer_name : "Woman & Sports", emp_name : "Magee", total : "601100", payment_type : "Credit", order_filled : "Y" }) // 주문정보 입력 확인 > db.ord.find().pretty() { "_id" : ObjectId("640dcc7c7b5ed14ecbe08566"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y" } // ord 컬렉션에 주문 상세 정보를 추가 입력 db.ord.update( { ord_id : "201209012345"}, { $set : { item_id : [ { item_no : "1", product_name : "Bunny Boots", item_price : "135", qty : "500", price : "67000" }, { item_no : "2", product_name : "Pro Ski Boots", item_price : "380", qty : "400", price : "152000" } ] } } ) // 조회 > db.ord.find().pretty() { "_id" : ObjectId("640dcc7c7b5ed14ecbe08566"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y", "item_id" : [ { "item_no" : "1", "product_name" : "Bunny Boots", "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_no" : "2", "product_name" : "Pro Ski Boots", "item_price" : "380", "qty" : "400", "price" : "152000" } ] } > db.ord.drop()
Database References
입력된 ObjectId 를 추출해서 연관된 다른 collection 에 필드를 추가해서 입력하여 조회하는 방법을 데이터베이스 레퍼런스 라고 할 수 있습니다.
도큐먼트의 고유 식별자를 다른 도큐먼트의 참조키 형태로 지정하여 연결 관계를 맺어주는 방법입니다.
RDBMS의 테이블간 연결 관계와 유사합니다. 관계형 데이터베이스에서는 FK 로 연결된 테이블 구조를 가지지만, MongoDB는 ObjectId 를 통해 서로 간의 연결고리를 설정하여 유사 하게 사용할 수 있습니다.
Database References 는 Manual References 와 DBRefs 으로 나눌 수 있으며, 참조 ObjectId 를 입력하는 방법에는 여러가지 있습니다.
1) ObjectId 를 직접 입력 - Manual References
2) 조회 결과를 변수(variable)에 넣은 후 입력 - Manual References
3) 입력시 서브쿼리 형태의 find 수행결과를 통해 입력 - Manual References
4) DBRef 함수를 이용 - DBRef
1,2,3번은 Manual References 입니다.
• ObjectId 를 직접 입력
// 컬렉션 삭제 > db.ord_detail.drop() // ord 컬렉션 생성 > db.ord.insert( { ord_id : "201209012345", customer_name : "Woman & Sports", emp_name : "Magee", total : "601100", payment_type : "Credit", order_filled : "Y" } ) // 컬렉션 삭제 > db.ord_detail.drop() // 생성시 ordid_id 값을 ord 컬렉션의 ObjectId 값을 넣어서 생성 합니다. > db.ord_detail.insert( { ord_id : "201209012345", item_id : [ { item_id : "1", product_name : "Bunny Boots", item_price : "135", qty : "500", price : "67000" }, { item_id : "2", product_name : "Pro Ski Boots", item_price : "380", qty : "400", price : "152000" } ], ordid_id : ObjectId("640dcd0f7b5ed14ecbe08567") } ) // <== 위에서 조회된 ObjectId 을 입력 // ord_detail 조회 > db.ord_detail.find().pretty() { "_id" : ObjectId("640dcfcb7b5ed14ecbe08568"), "ord_id" : "201209012345", "item_id" : [ { "item_id" : "1", "product_name" : "Bunny Boots", "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_id" : "2", "product_name" : "Pro Ski Boots", "item_price" : "380", "qty" : "400", "price" : "152000" } ], "ordid_id" : ObjectId("640dcd0f7b5ed14ecbe08567") } // ordid_id 에는 ord 의 ObjectId 가 입력되어 있습니다.
• 조회 결과를 변수(variable)에 넣은 후 입력
// 컬렉션 삭제 > db.ord.drop() // ord 컬렉션 생성 > db.ord.insert( { ord_id : "201209012345", customer_name : "Woman & Sports", emp_name : "Magee", total : "601100", payment_type : "Credit", order_filled : "Y" } ) // 위에서 입력한 201209012345 의 조회결과를 o 변수 출력 > o = db.ord.findOne( { "ord_id" : "201209012345" } ) { "_id" : ObjectId("640dcd0f7b5ed14ecbe08567"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y" } // 컬렉션 삭제 > db.ord_detail.drop() // 생성시 ordid_id 값을 ord 컬렉션의 ObjectId 값을 넣어서 생성 합니다. // o._id 를 사용 > db.ord_detail.insert( { ord_id : "201209012345", item_id : [ { item_id : "1", product_name : "Bunny Boots", item_price : "135", qty : "500", price : "67000" }, { item_id : "2", product_name : "Pro Ski Boots", item_price : "380", qty : "400", price : "152000" } ], ordid_id : o._id } ) // 입력된 ord_detail 컬렉션 조회 합니다. > db.ord_detail.find().pretty() { "_id" : ObjectId("640dd1327b5ed14ecbe0856a"), "ord_id" : "201209012345", "item_id" : [ { "item_id" : "1", "product_name" : "Bunny Boots", "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_id" : "2", "product_name" : "Pro Ski Boots", "item_price" : "380", "qty" : "400", "price" : "152000" } ], "ordid_id" : ObjectId("640dcd0f7b5ed14ecbe08567") } // ordid_id 에는 ord 의 ObjectId 가 입력되어 있습니다.
• 입력시 서브쿼리 형태의 find 수행결과를 통해 입력
// 컬렉션 삭제 > db.ord.drop() // ord 컬렉션 생성 > db.ord.insert( { ord_id : "201209012345", customer_name : "Woman & Sports", emp_name : "Magee", total : "601100", payment_type : "Credit", order_filled : "Y" } ) // 생성된 ord 컬렉션 조회 > db.ord.find().pretty(); { "_id" : ObjectId("641d449a44a14ca8f1cafd16"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y" } // 컬렉션 삭제 > db.ord_detail.drop() // ord_detail 생성시 ord 컬렉션을 조회하는 find을 사용해서 도큐먼트를 입력 합니다. > db.ord_detail.insert( { ord_id : "201209012345", item_id : [ { item_id : "1", product_name : "Bunny Boots", item_price : "135", qty : "500", price : "67000" }, { item_id : "2", product_name : "Pro Ski Boots", item_price : "380", qty : "400", price : "152000" } ], ordid_id : db.ord.findOne({ord_id: "201209012345"})._id } ) // 입력된 ord_detail 컬렉션 조회 합니다. > db.ord_detail.find().pretty() { "_id" : ObjectId("641d44b144a14ca8f1cafd17"), "ord_id" : "201209012345", "item_id" : [ { "item_id" : "1", "product_name" : "Bunny Boots", "item_price" : "135", "qty" : "500", "price" : "67000" }, { "item_id" : "2", "product_name" : "Pro Ski Boots", "item_price" : "380", "qty" : "400", "price" : "152000" } ], "ordid_id" : ObjectId("641d449a44a14ca8f1cafd16") } // ordid_id 에는 ord 의 ObjectId 가 입력되어 있습니다.
위의 세개의 예제는 Manual Reference 방식으로 명시적으로 연관 관계가 있는 도큐먼트의 ObjectId 를 입력하는 방식을 사용합니다.
Reference 된 데이터는 아래와 같이 참조하여 조회 및 활용할 수 있습니다.
> db.ord.findOne({ _id: db.ord_detail.findOne({ord_id: "201209012345"}).ordid_id }) { "_id" : ObjectId("641d449a44a14ca8f1cafd16"), "ord_id" : "201209012345", "customer_name" : "Woman & Sports", "emp_name" : "Magee", "total" : "601100", "payment_type" : "Credit", "order_filled" : "Y" }
• DBRef 를 통합 입력
DBRef는 MongoDB의문서 간에 참조를 만드는 방법 중 하나입니다. DBRef는 다른 문서의 _id 필드와 함께 컬렉션 이름과 데이터베이스 이름을 저장합니다.
// creators 컬렉션 생성 > db.creators.insertOne( { name: "John Doe", email: "johndoe@example.com" } ) { "acknowledged" : true, "insertedId" : ObjectId("641c8bcb85e24e6051c1b2b4") } // book 컬렉션 생성 > db.book.insert( {"title" : "MongoDB Tutorial", "author" : { "$ref" : "creators", "$id" : ObjectId("641c8bcb85e24e6051c1b2b4"), "$db" : "test" } }) // book 컬렉션 조회 > db.book.find().pretty(); { "_id" : ObjectId("641c8c3685e24e6051c1b2b5"), "title" : "MongoDB Tutorial", "author" : DBRef("creators", ObjectId("641c8bcb85e24e6051c1b2b4"), "test") }
위의 컬렉션에서 DBRef는 test 데이터베이스의 creators 컬렉션에 있는 ObjectId("641c8bcb85e24e6051c1b2b4")라는 문서를 가리킵니다.
DBRef를 사용하려면 MongoDB 드라이버나 클라이언트가 지원해야 합니다. DBRef를 쿼리 하려면 $ref 와 $id, $db 필드를 사용할 수 있습니다.
입력된 다른 컬렉션의 ObjectId 를 통해서 조회는 다음과 같이 수행하게 됩니다.
> var v_book = db.book.findOne({"title": "MongoDB Tutorial"}); > var v_dbRef = v_book.author > db[v_dbRef.$ref].find({ "_id":(v_dbRef.$id) }).pretty() { "_id" : ObjectId("641c8bcb85e24e6051c1b2b4"), "name" : "John Doe", "email" : "johndoe@example.com" }
DBRef와 Manual references의 차이점
Manual references는 참조하려는 문서의 _id 필드만 저장합니다. 예를 들어, 다음과 같은 Manual reference가 있을 수 있습니다.
{ "_id" : ObjectId("5126bbf64aed4daf9e2ab771"), "title" : "Good Book", "author_id" : ObjectId("5126bc054aed4daf9e2ab772") }
이 Manual reference는 author_id 필드에 저장된 _id 값을 가진 문서를 참조합니다. 이 경우 컬렉션 이름이나 데이터베이스 이름을 알 수 없습니다.
DBRef는 참조하려는 문서의 _id 필드뿐만 아니라 컬렉션 이름과 옵션으로 데이터베이스 이름도 저장합니다. 예를 들어, 다음과 같은 DBRef가 있을 수 있습니다.
{ "_id" : ObjectId("5126bbf64aed4daf9e2ab771"), "title" : "Good Book", "author" : { "$ref" : "creators", "$id" : ObjectId("5126bc054aed4daf9e2ab772"), "$db" : "users" } }
이 DBRef는 users 데이터베이스의 creators 컬렉션에 있는 ObjectId("5126bc054aed4daf9e2ab772")라는 문서를 가리킵니다.
DBRef는 참조하는 컬렉션과 데이터베이스의 이름을 명시적으로 저장하기 때문에 Manual references보다 더 명확하고 일관된 방식으로 문서 간에 링크를 표현할 수 있습니다
DBRef는 MongoDB 드라이버나 클라이언트가 지원해야 하며, 일부 드라이버나 클라이언트에서 자동으로 DBRef를 해석하고 관련된 도큐먼트를 가져올 수 있습니다.
반면 Manual references는 어떤 드라이버나 클라이언트에서도 사용할 수 있으며, 관련된 문서를 가져오기 위해서는 별도의 쿼리가 필요합니다.
위에서 설명한 내용과 같이 DBRef 에는 데이터베이스 정보도 포함할 수 있기 때문에 서로 다른 데이터베이스의 컬렉션 참조를 만들 때 유용하게 사용할 수 있습니다.
DBRef와 Manual references 중 어떤 것을 사용할지 결정하기 위해서는 애플리케이션의 요구사항과 상황을 고려해야 합니다.
$lookup 연산자 & Natural Join & Aggregation
이번에는 Link & Aggregation 를 사용하여 관계형 데이터베이스의 조인과 같은 형태로 조회를 해보려고 합니다.
MongoDB의 Aggregation 조회에 관한 자세한 내용은 아래 포스팅을 먼저 참조하시면 됩니다.
1:N 관계 구도는 데이터의 성격에 따라 다양한 형태의 설계 패턴으로 구현되는데 이를 Master-Detail 또는 Parent-Child 관계 구조이며, MongoDB에서 Multiple Join 으로 부릅니다.
order(주문) 테이블은 Master 테이블이고 order_detail(주문상세)은 child 상세 테이블입니다.
// order 컬렉션 생성 db.order.insertMany([ { ord_id: 100, emp_no: 204, order_date: ISODate('2018-08-30'), ship_date : ISODate('2018-09-10'), amount: 601020, pay_type: 'CREDIT', order_filled: 'Y'}, { ord_id: 101, emp_no: 205, order_date: ISODate('2018-08-30'), ship_date : ISODate('2018-09-15'), amount: 8056.6, pay_type: 'CREDIT', order_filled: 'Y'}, { ord_id: 102, emp_no: 206, order_date: ISODate('2018-09-01'), ship_date : ISODate('2018-09-08'), amount: 8335, pay_type: 'CREDIT', order_filled: 'Y'}, { ord_id: 103, emp_no: 208, order_date: ISODate('2018-09-02'), ship_date : ISODate('2018-09-22'), amount: 377, pay_type: 'CASH', order_filled: 'Y'} ]) // order 조회 > db.order.find().pretty() { "_id" : ObjectId("641d5aad44a14ca8f1cafd18"), "ord_id" : 100, "emp_no" : 204, "order_date" : ISODate("2018-08-30T00:00:00Z"), "ship_date" : ISODate("2018-09-10T00:00:00Z"), "amount" : 601020, "pay_type" : "CREDIT", "order_filled" : "Y" } { "_id" : ObjectId("641d5aad44a14ca8f1cafd19"), "ord_id" : 101, "emp_no" : 205, "order_date" : ISODate("2018-08-30T00:00:00Z"), "ship_date" : ISODate("2018-09-15T00:00:00Z"), "amount" : 8056.6, "pay_type" : "CREDIT", "order_filled" : "Y" } { "_id" : ObjectId("641d5aad44a14ca8f1cafd1a"), "ord_id" : 102, "emp_no" : 206, "order_date" : ISODate("2018-09-01T00:00:00Z"), "ship_date" : ISODate("2018-09-08T00:00:00Z"), "amount" : 8335, "pay_type" : "CREDIT", "order_filled" : "Y" } { "_id" : ObjectId("641d5aad44a14ca8f1cafd1b"), "ord_id" : 103, "emp_no" : 208, "order_date" : ISODate("2018-09-02T00:00:00Z"), "ship_date" : ISODate("2018-09-22T00:00:00Z"), "amount" : 377, "pay_type" : "CASH", "order_filled" : "Y" } // order_detail 컬렉션 생성 db.order_detail.insertMany([ {ord_id:100, item_id:1, product_no: 10011, price: 135, qty: 4452, amt: 601020}, {ord_id:100, item_id:2, product_no: 10013, price: 1380, qty: 400, amt: 55200}, {ord_id:101, item_id:1, product_no: 30421, price: 16, qty: 15, amt: 240}, {ord_id:101, item_id:5, product_no: 50169, price: 4.29, qty: 40, amt: 171.6}, {ord_id:102, item_id:1, product_no: 20108, price: 28, qty: 100, amt: 2800}, {ord_id:103, item_id:2, product_no: 32779, price: 7, qty: 11, amt: 77} ])
입력된 데이터는 아래와 같이 Link & Aggregation 을 사용하여 조회해보도록 하겠습니다.
> db.order.aggregate([ { $lookup: { from : "order_detail", let : { order_id : "$ord_id", order_amt: "$amount" }, pipeline : [ { $match: { $expr: { $and: [ { $eq : [ "$ord_id", "$$order_id" ] } ] } } }, { $project: { ord_id : 0, _id: 0 } } ], as: "OrderData" } } ]).pretty() { "_id" : ObjectId("641d5aad44a14ca8f1cafd18"), "ord_id" : 100, "emp_no" : 204, "order_date" : ISODate("2018-08-30T00:00:00Z"), "ship_date" : ISODate("2018-09-10T00:00:00Z"), "amount" : 601020, "pay_type" : "CREDIT", "order_filled" : "Y", "OrderData" : [ { "item_id" : 1, "product_no" : 10011, "price" : 135, "qty" : 4452, "amt" : 601020 }, { "item_id" : 2, "product_no" : 10013, "price" : 1380, "qty" : 400, "amt" : 55200 } ] } { "_id" : ObjectId("641d5aad44a14ca8f1cafd19"), "ord_id" : 101, "emp_no" : 205, "order_date" : ISODate("2018-08-30T00:00:00Z"), "ship_date" : ISODate("2018-09-15T00:00:00Z"), "amount" : 8056.6, "pay_type" : "CREDIT", "order_filled" : "Y", "OrderData" : [ { "item_id" : 1, "product_no" : 30421, "price" : 16, "qty" : 15, "amt" : 240 }, { "item_id" : 5, "product_no" : 50169, "price" : 4.29, "qty" : 40, "amt" : 171.6 } ] } { "_id" : ObjectId("641d5aad44a14ca8f1cafd1a"), "ord_id" : 102, "emp_no" : 206, "order_date" : ISODate("2018-09-01T00:00:00Z"), "ship_date" : ISODate("2018-09-08T00:00:00Z"), "amount" : 8335, "pay_type" : "CREDIT", "order_filled" : "Y", "OrderData" : [ { "item_id" : 1, "product_no" : 20108, "price" : 28, "qty" : 100, "amt" : 2800 } ] } { "_id" : ObjectId("641d5aad44a14ca8f1cafd1b"), "ord_id" : 103, "emp_no" : 208, "order_date" : ISODate("2018-09-02T00:00:00Z"), "ship_date" : ISODate("2018-09-22T00:00:00Z"), "amount" : 377, "pay_type" : "CASH", "order_filled" : "Y", "OrderData" : [ { "item_id" : 2, "product_no" : 32779, "price" : 7, "qty" : 11, "amt" : 77 } ] }
mergeObject
$mergeObjects는 MongoDB의 집계 파이프라인 연산자 중 하나입니다. 이 연산자는 여러 문서를 하나의 문서로 결합합니다.
$mergeObjects 는 관계형 데이터배이스에서 Union ALL 과 유사한 기능입니다.
• 테스트 예제
// 컬렉션 생성 > db.orders.insertMany( [ { "_id" : 1, "item" : "abc", "price" : 12, "ordered" : 2 }, { "_id" : 2, "item" : "jkl", "price" : 20, "ordered" : 1 } ] ) // 컬렉션 생성 > db.items.insertMany( [ { "_id" : 1, "item" : "abc", description: "product 1", "instock" : 120 }, { "_id" : 2, "item" : "def", description: "product 2", "instock" : 80 }, { "_id" : 3, "item" : "jkl", description: "product 3", "instock" : 60 } ] ) // $mergeObjects 이용하여 조회 > db.orders.aggregate( [ { $lookup: { from: "items", localField: "item", // field in the orders collection foreignField: "item", // field in the items collection as: "fromItems" } }, { $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$fromItems", 0 ] }, "$$ROOT" ] } } }, { $project: { fromItems: 0 } } ] ).pretty() { "_id" : 1, "item" : "abc", "description" : "product 1", "instock" : 120, "price" : 12, "ordered" : 2 } { "_id" : 2, "item" : "jkl", "description" : "product 3", "instock" : 60, "price" : 20, "ordered" : 1 }
Validator
MongoDB 3.2 버전에서 추가된 기능으로 제약조건 기능을 사용할 수 있습니다.
관계형 데이터베이스에서의 NOT NULL 과 CHECK 제약 조건과 같은 기능을 지원 및 사용할 수 있으며 특정 데이터 타입과 특정 값만 넣을 수 있다는 등과 같은 제약 조건의 설정을 할 수 있습니다.
사용시 장단점
장점: 데이터 무결성
단점: CPU 사용률 증가와 성능이 일부 저하될 수도 있음
• 테스트 예제
// 컬렉션 삭제 > db.emp.drop() // emp 컬렉션을 생성시 제약조건을 설정하여 생성(validator) // 제약조건 : empno 에는 타입이 string 으로 입력 받으며, deptno 에서는 10 만 입력 할 수 있음 > db.createCollection( "emp", { validator : { $and: [ { empno: { $type: "string" } }, { deptno: { $in: [ 10 ] } } ] } } ) // 정상입력 가능 > db.emp.insert({ empno : "1111", ename : "JMJOO", deptno : 10 }) WriteResult({ "nInserted" : 1 }) // 입력시 에러 발생 > db.emp.insert({ empno : "1111", ename : "JMJOO", deptno : 20 }) WriteResult({ "nInserted" : 0, "writeError" : { "code" : 121, "errmsg" : "Document failed validation" } }) // emp 컬렉션 삭제 > db.emp.drop() // emp 컬렉션 재생성 // validator 에 각 필드별로 데이터 타입을 지정하여 생성, deptno 필드는 데이터 타입 및 값의 제약사항이 있음 > db.createCollection( "emp", {capped : false, validator : { $and: [ { empno : { $type : "double" } }, { ename : { $type : "string" } }, { job : { $type : "string" } }, { sal : { $type : "double" } }, { hiredate : { $type : "date" } }, { deptno : { $type : "double"} }, { deptno : { $in : [ 10, 20 ] } } ] } } ) // 정상적으로 입력됨 - 각 필드별로 설정된 데이터 타입 제약사항에 맞게 입력이 되었음 > db.emp.insert( { empno : 1111, ename : "JJM", job : "MANAGER", sal : 1200, hiredate : ISODate(), deptno : 10 }) WriteResult({ "nInserted" : 1 }) // 에러가 발생함 - deptno 입력 값의 제한사항에 위배됨 > db.emp.insert( { empno : 2222, ename : "JJM", job : "MANAGER", sal : 1200, hiredate : ISODate(), deptno : 30 }) WriteResult({ "nInserted" : 0, "writeError" : { "code" : 121, "errmsg" : "Document failed validation" } }) // 에러가 발생함 - empno 필드의 타입이 double 형으로 제약사항을 설정하였으나 입력된 값은 string이 입력이 되어 에러가 발생함 > db.emp.insert( { empno : "2222", ename : "JJM", job : "MANAGER", sal : 1200, hiredate : ISODate(), deptno : 10 }) WriteResult({ "nInserted" : 0, "writeError" : { "code" : 121, "errmsg" : "Document failed validation" } }) // 정상적으로 입력됨 - 각 필드별로 설정된 데이터 타입 제약사항에 맞게 입력이 되었음 > db.emp.insert( { empno : 2222, ename : "JJM", job : "MANAGER", sal : 1200, hiredate : ISODate(), deptno : 10 }) WriteResult({ "nInserted" : 1 })
Uncorrelated subquery는 from 컬렉션의 문서 필드를 참조하지 않는 pipeline을 사용하는 쿼리입니다.
즉, pipeline이 from 컬렉션과 독립적으로 실행됩니다. uncorrelated subquery는 단일 동등 매칭 외에 다른 join 조건을 허용합니다.
Uncorrelated subquery의 장점은 다양한 join 조건을 사용할 수 있다는 점입니다. 단점은 pipeline이 from 컬렉션의 문서 수만큼 실행되므로 성능이 저하될 수 있다는 점입니다.
• 테스트 예제
// absences 컬렉션 생성 > db.absences.insertMany( [ { "_id" : 1, "student" : "Ann Aardvark", sickdays: [ new Date ("2018-05-01"),new Date ("2018-08-23") ] }, { "_id" : 2, "student" : "Zoe Zebra", sickdays: [ new Date ("2018-02-01"),new Date ("2018-05-23") ] }, ] ) > db.holidays.insertMany( [ { "_id" : 1, year: 2018, name: "New Years", date: new Date("2018-01-01") }, { "_id" : 2, year: 2018, name: "Pi Day", date: new Date("2018-03-14") }, { "_id" : 3, year: 2018, name: "Ice Cream Day", date: new Date("2018-07-15") }, { "_id" : 4, year: 2017, name: "New Years", date: new Date("2017-01-01") }, { "_id" : 5, year: 2017, name: "Ice Cream Day", date: new Date("2017-07-16") } ] ) // 조회 // 이 쿼리는 absences 컬렉션과 holidays 컬렉션 사이에 uncorrelated subquery를 수행합니다. // holidays 컬렉션에서 year이 2018인 문서들을 찾아서 name 과 date 필드만 가져옵니다. // 그리고 그 결과를 holidays라는 배열로 absences 컬렉션의 문서에 추가합니다. // 이 쿼리는 let 변수를 사용하지 않으므로 uncorrelated subquery 가 되게 됩니다. db.absences.aggregate( [ { $lookup: { from: "holidays", pipeline: [ { $match: { year: 2018 } }, { $project: { _id: 0, date: { name: "$name", date: "$date" } } }, { $replaceRoot: { newRoot: "$date" } } ], as: "holidays" } } ] ).pretty() // 조회결과 { "_id" : 1, "student" : "Ann Aardvark", "sickdays" : [ ISODate("2018-05-01T00:00:00Z"), ISODate("2018-08-23T00:00:00Z") ], "holidays" : [ { "name" : "New Years", "date" : ISODate("2018-01-01T00:00:00Z") }, { "name" : "Pi Day", "date" : ISODate("2018-03-14T00:00:00Z") }, { "name" : "Ice Cream Day", "date" : ISODate("2018-07-15T00:00:00Z") } ] } { "_id" : 2, "student" : "Zoe Zebra", "sickdays" : [ ISODate("2018-02-01T00:00:00Z"), ISODate("2018-05-23T00:00:00Z") ], "holidays" : [ { "name" : "New Years", "date" : ISODate("2018-01-01T00:00:00Z") }, { "name" : "Pi Day", "date" : ISODate("2018-03-14T00:00:00Z") }, { "name" : "Ice Cream Day", "date" : ISODate("2018-07-15T00:00:00Z") } ] }
Tree Structure - 계층형 구조
Tree Structure 쿼리는 트리 구조를 가진 데이터를 조회하는 쿼리입니다.
트리 구조는 부모-자식 관계를 가진 문서들로 이루어져 있습니다. 트리 구조를 표현하는 방법에는 여러 가지가 있습니다.
먼저 테스트 데이터 입력 및 확인합니다.
> db.categories.drop() > db.categories.insertMany([ { _id: "Books", ancestors: [ ], parent: null }, { _id: "Programming", ancestors: [ "Books" ], parent: "Books" }, { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" }, { _id: "Java", ancestors: [ "Books", "Programming", "Languages" ], parent: "Languages" }, { _id: "Effective Java", ancestors: [ "Books", "Programming", "Languages","Java" ], parent: "Java" }, { _id: "Head First Design Patterns", ancestors: [ "Books", "Programming", "Languages","Java" ], parent: "Java" }, { _id: "Spring in Action", ancestors: [ "Books", "Programming", "Languages","Java" ], parent: "Java" } ]) // 조회 db.categories.find().pretty() { "_id" : "Books", "ancestors" : [ ], "parent" : null } { "_id" : "Programming", "ancestors" : [ "Books" ], "parent" : "Books" } { "_id" : "Languages", "ancestors" : [ "Books", "Programming" ], "parent" : "Programming" } { "_id" : "Java", "ancestors" : [ "Books", "Programming", "Languages" ], "parent" : "Languages" } { "_id" : "Effective Java", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" } { "_id" : "Head First Design Patterns", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" } { "_id" : "Spring in Action", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" }
이 트리 구조는 각 문서에 _id, ancestors, parent 필드를 사용하여 표현하고 있습니다.
이렇게 하면 부모나 자식을 쉽게 찾을 수 있습니다.
예를 들어, Java 카테고리의 부모 카테고리를 찾으려면 다음과 같은 쿼리를 사용할 수 있습니다.
// 이 쿼리는 Languages라는 값을 반환합니다. > db.categories.findOne({ _id: "Java" }).parent Languages
Java 카테고리의 자식 찾으려면 다음과 같은 쿼리를 사용할 수 있습니다.
// 이 쿼리는 Java 카테고리의 모든 자식 문서들을 반환합니다. > db.categories.find({ parent: "Java" }) { "_id" : "Effective Java", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" } { "_id" : "Head First Design Patterns", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" } { "_id" : "Spring in Action", "ancestors" : [ "Books", "Programming", "Languages", "Java" ], "parent" : "Java" }
Java 카테고리의 조상 카테고리들을 찾으려면 다음과 같은 쿼리를 사용할 수 있습니다.
> db.categories.find({ _id: { $in: db.categories.findOne({ _id: "Java" }).ancestors } }) { "_id" : "Books", "ancestors" : [ ], "parent" : null } { "_id" : "Languages", "ancestors" : [ "Books", "Programming" ], "parent" : "Programming" } { "_id" : "Programming", "ancestors" : [ "Books" ], "parent" : "Books" }
View
Collection 을 기초로 하는 가상(논리) Collection 입니다.
물리적인 저장 공간을 가지고 있지 않습니다.
하나 이상의 컬렉션을 결합하여 생성할 수 있습니다.
• 테스트 예제
> db.employees.drop() // 예제 데이터 입력 db.employees.insert({empno:7369 , ename : "SMITH", job : "CLERK", manager : "FORD", hiredate : "17-12-1980", sal : 800, deptno : 20 }) db.employees.insert({empno:7499 , ename : "ALLEN", job : "SALESMAN", manager : "BLAKE", hiredate : "20-02-1981", sal :1600, comm : 300, deptno : 30 }) db.employees.insert({empno:7521 , ename : "WARD", job : "SALESMAN", manager : "BLAKE", hiredate : "22-02-1981", sal : 1250, comm : 500, deptno : 30 }) db.employees.insert({empno:7566 , ename : "JONES", job : "MANAGER", manager : "KING", hiredate : "02-04-1981", sal : 2975, deptno : 20 }) db.employees.insert({empno:7654 , ename : "MARTIN", job : "SALESMAN", manager : "BLAKE", hiredate : "28-09-1981", sal : 1250, comm : 1400, deptno : 30 }) db.employees.insert({empno:7698 , ename : "BLAKE", job : "MANAGER", manager : "KING", hiredate : "01-05-1981", sal : 2850, deptno : 30 }) db.employees.insert({empno:7782 , ename : "CLARK", job : "MANAGER", manager : "KING", hiredate : "09-06-1981", sal : 2450, deptno : 10 }) db.employees.insert({empno:7788 , ename : "SCOTT", job : "ANALYST", manager : "JONES", hiredate : "13-06-1987", sal : 3000, deptno : 20 }) db.employees.insert({empno:7839 , ename : "KING", job : "CEO", manager : "", hiredate : "17-11-1981", sal : 5000, deptno : 10 }) db.employees.insert({empno:7844 , ename : "TURNER", job : "SALESMAN", manager : "BLAKE", hiredate : "08-09-1981", sal : 1500, deptno : 30 }) db.employees.insert({empno:7876 , ename : "ADAMS", job : "CLERK", manager : "SCOTT", hiredate : "13-06-1987", sal : 1100, deptno : 20 }) db.employees.insert({empno:7900 , ename : "JAMES", job : "CLERK", manager : "BLAKE", hiredate : "03-12-1981", sal : 950, deptno : 30 }) db.employees.insert({empno:7902 , ename : "FORD", job : "ANALYST", manager : "JONES", hiredate : "03-12-1981", sal : 3000, deptno : 20 }) db.employees.insert({empno:7934 , ename : "CLERK", job : "CLERK", manager : "KING", hiredate : "23-01-1982", sal : 1300, deptno : 10 }) > db.v_emp.drop() // 뷰 생성 db.createView ( "v_emp", "employees", [ { $lookup: { from : "department", localField : "deptno", foreignField : "deptno", as : "depart_Info" } }, { $project: { empno : 1, ename : 1, job : 1, deptno : 1, "depart_Info.dname" : 1} } ] ) // 조회 > db.v_emp.find().pretty() // 결과는 생략
Materialied view
On-Demand Materialized Views
MongoDB는 standard views와 On-Demand Materialized Views라는 두 가지 뷰 타입을 제공합니다.
On-Demand Materialized Views는 MongoDB 4.2 버전 부터 제공되는 기능입니다.
$merge 연산자를 이용하여 사용자의 의도대로 집계된 결과를 별도의 컬렉션에 저장할 수 있습니다.
집계 결과는 동일한 DB 또는 다른 DB에 생성됩니다.
기존의 $out 연산자와 유사한 기능이지만 다양한 기능이 추가되어 별도로 제공되는 기능입니다.
• 테스트 컬렉션 생성
// 기초 데이터 입력 > db.bakesales.insertMany( [ { date: new ISODate("2018-12-01"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") }, { date: new ISODate("2018-12-02"), item: "Cake - Peanut Butter", quantity: 5, amount: new NumberDecimal("90") }, { date: new ISODate("2018-12-02"), item: "Cake - Red Velvet", quantity: 10, amount: new NumberDecimal("200") }, { date: new ISODate("2018-12-04"), item: "Cookies - Chocolate Chip", quantity: 20, amount: new NumberDecimal("80") }, { date: new ISODate("2018-12-04"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") }, { date: new ISODate("2018-12-05"), item: "Pie - Key Lime", quantity: 3, amount: new NumberDecimal("60") }, { date: new ISODate("2019-01-25"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") }, { date: new ISODate("2019-01-25"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") }, { date: new ISODate("2019-01-26"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }, { date: new ISODate("2019-01-26"), item: "Cookies - Chocolate Chip", quantity: 12, amount: new NumberDecimal("48") }, { date: new ISODate("2019-01-26"), item: "Cake - Carrot", quantity: 2, amount: new NumberDecimal("36") }, { date: new ISODate("2019-01-26"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }, { date: new ISODate("2019-01-27"), item: "Pie - Chocolate Cream", quantity: 1, amount: new NumberDecimal("20") }, { date: new ISODate("2019-01-27"), item: "Cake - Peanut Butter", quantity: 5, amount: new NumberDecimal("80") }, { date: new ISODate("2019-01-27"), item: "Tarts - Apple", quantity: 3, amount: new NumberDecimal("12") }, { date: new ISODate("2019-01-27"), item: "Cookies - Chocolate Chip", quantity: 12, amount: new NumberDecimal("48") }, { date: new ISODate("2019-01-27"), item: "Cake - Carrot", quantity: 5, amount: new NumberDecimal("36") }, { date: new ISODate("2019-01-27"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }, { date: new ISODate("2019-01-28"), item: "Cookies - Chocolate Chip", quantity: 20, amount: new NumberDecimal("80") }, { date: new ISODate("2019-01-28"), item: "Pie - Key Lime", quantity: 3, amount: new NumberDecimal("60") }, { date: new ISODate("2019-01-28"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }, ] );
Materialized 정의
이 함수는 monthlybakesales 라는 Materialized 뷰를 정의합니다.
$merge 스테이지를 사용하여 집계 쿼리의 결과를 다른 컬렉션(monthlybakesales)에 저장합니다.
함수는 날짜 파라미터를 받아서 날짜부터 월별 판매 정보를 업데이트 합니다.
> updateMonthlySales = function(startDate) { db.bakesales.aggregate( [ { $match: { date: { $gte: startDate } } }, { $group: { _id: { $dateToString: { format: "%Y-%m", date: "$date" } }, sales_quantity: { $sum: "$quantity"}, sales_amount: { $sum: "$amount" } } }, { $merge: { into: "monthlybakesales", whenMatched: "replace" } } ] ); };
Materialized 뷰를 업데이트
정의한 함수를 호출하여 집계 쿼리의 결과를 컬렉션에 저장하거나 갱신 합니다.
> updateMonthlySales(new ISODate("1970-01-01"));
Materialized 뷰를 조회 합니다.
> db.monthlybakesales.find().sort( { _id: 1 } ) { "_id" : "2018-12", "sales_quantity" : 41, "sales_amount" : NumberDecimal("506") } { "_id" : "2019-01", "sales_quantity" : 86, "sales_amount" : NumberDecimal("896") }
위의 결과가 2건이고, 그 중에서 id: "2019-01" 의 sales_amount 가 896임을 확인하시기 바랍니다.
추가 테스트를 위해서 도큐먼트를 추가로 입력합니다.
> db.bakesales.insertMany( [ { date: new ISODate("2019-01-28"), item: "Cake - Chocolate", quantity: 3, amount: new NumberDecimal("90") }, { date: new ISODate("2019-01-28"), item: "Cake - Peanut Butter", quantity: 2, amount: new NumberDecimal("32") }, { date: new ISODate("2019-01-30"), item: "Cake - Red Velvet", quantity: 1, amount: new NumberDecimal("20") }, { date: new ISODate("2019-01-30"), item: "Cookies - Chocolate Chip", quantity: 6, amount: new NumberDecimal("24") }, { date: new ISODate("2019-01-31"), item: "Pie - Key Lime", quantity: 2, amount: new NumberDecimal("40") }, { date: new ISODate("2019-01-31"), item: "Pie - Banana Cream", quantity: 2, amount: new NumberDecimal("40") }, { date: new ISODate("2019-02-01"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") }, { date: new ISODate("2019-02-01"), item: "Tarts - Apple", quantity: 2, amount: new NumberDecimal("8") }, { date: new ISODate("2019-02-02"), item: "Cake - Chocolate", quantity: 2, amount: new NumberDecimal("60") }, { date: new ISODate("2019-02-02"), item: "Cake - Peanut Butter", quantity: 1, amount: new NumberDecimal("16") }, { date: new ISODate("2019-02-03"), item: "Cake - Red Velvet", quantity: 5, amount: new NumberDecimal("100") } ] )
Materialized 뷰를 업데이트
정의한 함수를 호출하여 집계 쿼리의 결과를 컬렉션에 저장하거나 갱신
> updateMonthlySales(new ISODate("2019-01-01"));
갱신된 Materialized 뷰인 monthlybakesales 를 조회합니다.
> db.monthlybakesales.find().sort( { _id: 1 } ) { "_id" : "2018-12", "sales_quantity" : 41, "sales_amount" : NumberDecimal("506") } { "_id" : "2019-01", "sales_quantity" : 102, "sales_amount" : NumberDecimal("1142") } { "_id" : "2019-02", "sales_quantity" : 15, "sales_amount" : NumberDecimal("284") }
이전에 조회 하였을 때와는 달리 1개의 결과가 더 늘어났고 id: "2019-01" 의 sales_amount 가 증가된(변경이 된) 것을 확인할 수 있습니다.
Reference
Reference URL
• mongodb.com/data-model-design
• mongodb.com/database-references
• mongodb.com/model-tree-structures
Reference Book
• 빅데이터 저장 및 분석을 위한 New NoSQL & mongoDB
연관된 다른 글
Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io