# Mongo DB 정리 ### embedded vs reference - 1:1, 1:N 관계를 가질 때 embedded 방식을 사용하면 좋다. - M:N 관계를 가질 때 reference 방식을 사용하면 좋다. ### embedded ``` { _id: "joe", name: "Joe Bookreader", address: { street: "123 Fake Street", city: "Faketon", state: "MA", zip: "12345" } } // Address { pataron_id: "joe", street: "123 Fake Street", city: "Faketon", state: "MA", zip: "12345" } ``` ### reference ``` // Publisher { _id: "oreilly", name: "O'Reilly Media", founded: 1980, location: "CA" } // Book { _id: 123456789, title: "MongoDB: The Definitive Guide", author: [ "Kristina Chodorow", "Mike Dirolf" ], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher_id: "oreilly" // <- Publisher._id } ``` ### 모델링 패턴 1. Attribute 패턴 ``` { title: "Star Wars", director: "George Lucas", ... release_US: ISODate("1977-05-20T01:00:00+01:00"), release_France: ISODate("1977-10-19T01:00:00+01:00"), release_Italy: ISODate("1977-10-20T01:00:00+01:00"), release_UK: ISODate("1977-12-27T01:00:00+01:00"), ... } { title: "Star Wars", director: "George Lucas", … releases: [ { location: "USA", date: ISODate("1977-05-20T01:00:00+01:00") }, { location: "France", date: ISODate("1977-10-19T01:00:00+01:00") }, { location: "Italy", date: ISODate("1977-10-20T01:00:00+01:00") }, { location: "UK", date: ISODate("1977-12-27T01:00:00+01:00") }, … ], … } ``` 위의 예에서 각 출시 날짜를 검색하려면 여러 필드를 검색해야 한다. 그러나 Attribute 패턴을 사용하여 배열로 관리하므로 배열에 대해 하나의 인덱스를 만들어 인덱싱을 쉽게 관리할 수 있다. 2. Extended Reference 패턴 여러개의 콜렉션으로 나누어 관리하면 성능적인 관점에서 join에 대해 성능적 문제가 발생한다. 자주 사용하는 필드만 복사하여 우선순위가 높거나 하는 필드를 포함시킨다 ``` ///customer { _id:1, name : "abc", street: "123 main st", city : "some where", country: "nation", ... } /// order { _id: 2 date : "2019-02-18", customer_id: 1, sipping_addr: { name: "abc", street: "123 main st", city : "some where", country: "nation", }, ... } ``` 데이터의 중복이 되는것을 신경 써야한다. 데이터가 자주 변경되지 않는 필드를 사용하면 좋다. 3. Subset 패턴 자주 사용되는 정보와 사용되지 않는 정보를 분리하여 작업 세트의 전체 크기를 줄인다. ``` user { name, id, level, address, ratings : [ { user_id, rating: "응답이 빨라요", // 평가 항목 8개 } ], image // url // 판매 목록 saleList : [ post_id, ], // 구매 목록 buyList: [ post_id, ], // 구매 요청 목록 } buyRequestList: { user_id, post_id } categories { name: string } ratings { rating : string } post { writer: user_id, title : string, content : string, images: [url: string], category: string, buyers : [ user_id ], state : string, // 구매가 되었는지를 나타내는 상태 bool으로 하면 예약 중 상태를 표현할 수 없어서 확장성을 고려해서 string으로 cost: number, uploadTime, // createdAt으로 자동으로 생성되면 안 써도 된다. } comment { user_id, content, post_id } ``` ### Replica Set - 안정성을 위해서 사용한다 - db 여러개를 생성후 Primary 와 Secondary 로 두어 Primary DB가 죽었을 경우 Secondary DB 가 PrimaryDB가 되면서 정상 작동 하는 법 - db 여러개를 띄운다 (아래와 같은 방법으로 3개의 DB를 만든다) ``` mongod --replSet replica --dbpath C:\Users\user\Desktop\test\db1 --port 30001 ``` - 그 후, 한 군데 접속하여 아래의 명령어를 통해 세 개의 DB를 replicaSet 설정을 해준다. ``` config = { _id : "replica" // option으로 넣은 태그명, members : [{_id:0,host:"localhost:30001"},{_id:1,host:"localhost:30002"},{_id:2,host:"localhost:30003"}] rs.initiate(config) 설정 으로 완료된다 ``` # 상황별 몽고디비 모델링 ## 1:1 관계 ### Embedded Document Pattern 하나의 document가 다른 하나를 포함하는 관계일 때 사용한다. ```javascript= { _id: "joe", name: "Joe Bookreader", address: { street: "123 Fake Street", city: "Faketon", state: "MA", zip: "12345" } } ``` ### Subset Pattern embedded document pattern의 잠재적인 문제점은 어플리케이션이 필요로 하지 않는 필드들을 포함해 document 크기가 커진다는 것이다. 대신에 subset pattern을 사용해서 한 데이터베이스의 call에서 가장 많이 접근 되는 subset을 가져오도록 하는 방법을 쓸 수 있다. 먄약 movie collection이 다음과 같을 때 ```javascript= { "_id": 1, "title": "The Arrival of a Train", "year": 1896, "runtime": 1, "released": ISODate("01-25-1896"), "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg", "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...", "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.", "lastupdated": ISODate("2015-08-15T10:06:53"), "type": "movie", "directors": [ "Auguste Lumière", "Louis Lumière" ], "imdb": { "rating": 7.3, "votes": 5043, "id": 12 }, "countries": [ "France" ], "genres": [ "Documentary", "Short" ], "tomatoes": { "viewer": { "rating": 3.7, "numReviews": 59 }, "lastUpdated": ISODate("2020-01-09T00:02:53") } } ``` 여기에는 simple overview에는 필요가 없는 정보들이 포함되어 있다. 이런 정보들을 하나의 collection에 다 모아놓기 보다는 collection을 나눠놓을 수 있다. ```javascript= // movie collection { "_id": 1, "title": "The Arrival of a Train", "year": 1896, "runtime": 1, "released": ISODate("1896-01-25"), "type": "movie", "directors": [ "Auguste Lumière", "Louis Lumière" ], "countries": [ "France" ], "genres": [ "Documentary", "Short" ], } // movie_details collection { "_id": 156, "movie_id": 1, // reference to the movie collection "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg", "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...", "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.", "lastupdated": ISODate("2015-08-15T10:06:53"), "imdb": { "rating": 7.3, "votes": 5043, "id": 12 }, "tomatoes": { "viewer": { "rating": 3.7, "numReviews": 59 }, "lastUpdated": ISODate("2020-01-29T00:02:53") } } ``` 이렇게 나누는 것이 read 성능을 높인다. 대부분의 request를 충족하는 data의 크기가 작아지기 때문이다. ### subset pattern의 trade off subset pattern을 사용하면 전체적인 working set의 크기를 줄인다. 또한 읽기 성능을 늘린다. 하지만 만약 적절하지 못하게 데이터를 여러개의 collection으로 나누면, 어플리케이션이 데이터를 가져올 때 multiple trip을 해야할 수도 있다. # 1:many 관계 [참고링크](https://docs.mongodb.com/manual/tutorial/model-embedded-one-to-many-relationships-between-documents/) 한명의 patron이 있고 여러개의 주소와 관계가 있을 때를 생각해보자. 하나의 context 속에서 다른 entity를 봐야할 때 embedding이 좋다. ```javascript= { "_id": "joe", "name": "Joe Bookreader", "addresses": [ { "street": "123 Fake Street", "city": "Faketon", "state": "MA", "zip": "12345" }, { "street": "1 Some Other Street", "city": "Boston", "state": "MA", "zip": "12345" } ] } ``` ## subset pattern e-commerce 사이트에서 상품에 대한 리뷰 데이터를 갖고 있다고 하자. ```javascript= { "_id": 1, "name": "Super Widget", "description": "This is the most useful item in your toolbox.", "price": { "value": NumberDecimal("119.99"), "currency": "USD" }, "reviews": [ { "review_id": 786, "review_author": "Kristina", "review_text": "This is indeed an amazing widget.", "published_date": ISODate("2019-02-18") }, { "review_id": 785, "review_author": "Trina", "review_text": "Nice product. Slow shipping.", "published_date": ISODate("2019-02-17") }, ... { "review_id": 1, "review_author": "Hans", "review_text": "Meh, it's okay.", "published_date": ISODate("2017-12-06") } ] } ``` 리뷰는 생성된 시간의 반대로 정렬되어 있다. 사용자가 상품 페이지에 접근했을 때 최근 10개의 리뷰를 가져온다. 모든 리뷰를 product와 함게 저장하지 않고 두 개의 collection으로 나눌 수 있다. product collection에는 10개의 최신 리뷰만 가지고 있다. ```javascript= { "_id": 1, "name": "Super Widget", "description": "This is the most useful item in your toolbox.", "price": { "value": NumberDecimal("119.99"), "currency": "USD" }, "reviews": [ { "review_id": 786, "review_author": "Kristina", "review_text": "This is indeed an amazing widget.", "published_date": ISODate("2019-02-18") } ... { "review_id": 776, "review_author": "Pablo", "review_text": "Amazing!", "published_date": ISODate("2019-02-16") } ] } ``` review collection에는 모든 리뷰가 저장되어 있다. ``` { "review_id": 786, "product_id": 1, "review_author": "Kristina", "review_text": "This is indeed an amazing widget.", "published_date": ISODate("2019-02-18") } { "review_id": 785, "product_id": 1, "review_author": "Trina", "review_text": "Nice product. Slow shipping.", "published_date": ISODate("2019-02-17") } ... { "review_id": 1, "product_id": 1, "review_author": "Hans", "review_text": "Meh, it's okay.", "published_date": ISODate("2017-12-06") } ``` ### subset pattern의 trade off data 복제가 생길 수 있다. 예를들어, 리뷰가 product collection과 reivews collection 모두에 있을 수 있다. 두 collection 사이에 데이터 일관성이 있도록 처리를 해주어야한다. 또한 product collection이 가장 최근의 collection이 되도록 유지해주어야한다. ## 1:many with references [참고링크](https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/) 출판사와 책의 관계를 생각해보자 publisher 정보의 반복을 피하기 위해서 referencing이 좋다. embedding 방식을 쓰면 다음과 같이 된다. ```javascript= { title: "MongoDB: The Definitive Guide", author: [ "Kristina Chodorow", "Mike Dirolf" ], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher: { name: "O'Reilly Media", founded: 1980, location: "CA" } } { title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English", publisher: { name: "O'Reilly Media", founded: 1980, location: "CA" } } ``` publisher 데이터의 중복을 줄이기 위해서 reference를 쓴다. reference 방식을 쓸 때 관계의 증가가 어디에 reference를 저장할지를 결정한다. 만약 한 출판사에 대한 책의 수가 작고 증가 폭이 제한되어 있다면 출판사 document 안에 책 reference를 넣는 것이 효율적이다. 하지만 출판사에 대한 책의 수가 크고 제한이 없다면 다음과 같이 mutable, growing array가 된다. ```javascript= { name: "O'Reilly Media", founded: 1980, location: "CA", books: [123456789, 234567890, ...] } { _id: 123456789, title: "MongoDB: The Definitive Guide", author: [ "Kristina Chodorow", "Mike Dirolf" ], published_date: ISODate("2010-09-24"), pages: 216, language: "English" } { _id: 234567890, title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English" } ``` > 숫자가 제한이 되어있지 않은 array는 왜 안 좋은가? document의 사이즈가 예상하지 못한 사이즈로 커질 수 있다. 배열이 계속해서 증가하면 array에 index를 지정하는 것이 점점 성능이 나빠질 수 있다. 나중에는 BSON document size 한계를 넘을 수 있다. [참고](https://docs.atlas.mongodb.com/schema-suggestions/avoid-unbounded-arrays/) [참고: blog와 growing comments](https://stackoverflow.com/questions/9306815/mongodb-performance-with-growing-data-structure) mutable하고 growing array를 피하기 위해 출판사 reference를 책 document 안에 저장한다. ```javascript= // publisher { _id: "oreilly", name: "O'Reilly Media", founded: 1980, location: "CA" } //book { _id: 123456789, title: "MongoDB: The Definitive Guide", author: [ "Kristina Chodorow", "Mike Dirolf" ], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher_id: "oreilly" } { _id: 234567890, title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English", publisher_id: "oreilly" } ``` ## keyword 검색이 가능한 데이터 모델 [참고링크](https://docs.mongodb.com/manual/tutorial/model-data-for-keyword-search/) keyword가 document에 저장이 되어있는 경우에 사용한다. keyword base query를 지원하기 위한 구조를 추가하기 위해서 array field를 만들고 문자열을 array에 넣어놓는다. 그리고 array에 multi-key index를 추가할 수 있다. ```javascript= { title : "Moby-Dick" , author : "Herman Melville" , published : 1851 , ISBN : 0451526996 , topics : [ "whaling" , "allegory" , "revenge" , "American" , "novel" , "nautical" , "voyage" , "Cape Cod" ] } ``` ```javascript= db.volumes.createIndex( { topics: 1 } ) db.volumes.findOne( { topics : "voyage" }, { title: 1 } ) ``` ``` users { name, id, level, address, image // url ratings: [1,23,4,5,3,2,65,3]; // 평 } ~~sale_list~~ { user_id, post_id } ~~buy_list~~ { user_id, post_id } // sale list와 buy list 를 하나의 거래완료 collection으로 나타낼 수 있다. buy_request_list { user_id, post_id } ~~user_ratings:~~ { user_id, //평가 당한 사람 user_id, // 평가 한 사람 post_id, } // 게시물당 하나의 평가만 할 수 있기 때문에 post안으로 평가를 했는지를 나타내는 것을 넣어도 될 것 같다. ~~user_rating_histories~~: { user_id, // 평가당한 사람 id histories: [1,23,4,5,3,2,65,3]; // 평 } // rating 정보를 유저 안으로 넣어도 될 것 같다. 크기가 크지 않고 고정 크기의 배열이라서. ===== user_post: { user_id, // 판매자 user_id, // 구매자 post_id: // 게시글 } // 요청을 따로 하는 경우도 많이 있어서 collection을 분리하는 것이 더 좋을 것 같다. // buyer request 리스트가 따로 있으면 writer, buyer, post가 1:1 관계가 되어서 따로 분리할 필요가 없을 것 같아서 post 안에 넣어주는 것이 좋을 것 같다. categories { name: string } post { writer: user_id, buyer_id: user_id title : string, content : string, images: [url: string], // 개수 제한 category: string, ~~recent_comments~~ : [] // 최근 10개. 최근 댓글로 업데이트 하는 문제. 여러 사람이 동시에 댓글을 작성할 때 lock이 걸려서 post 읽기가 느려지는 문제. ~~state~~ : string, // 구매가 되었는지를 나타내는 상태 bool으로 하면 예약 중 상태를 표현할 수 없어서 확장성을 고려해서 string으로 cost: number, uploadTime, // createdAt으로 자동으로 생성되면 안 써도 된다 } post_comments { post_id, writer_id, content } ``` ## 궁금한점 #### 1. buy list와 sale list를 users 안에 넣는 것이 좋을까? ``` users { name, id, level, address, image // url ratings: [1,23,4,5,3,2,65,3]; // 평 } sale_list { user_id, post_id } buy_list { user_id, post_id } ``` - 장점 join을 없애서 읽기 성능을 높일 수 있다. - 단점 sale list와 buy list의 크기가 계속해서 커져서 document 크기가 커질 수 있다. 답변 : 거래 완료내역만 추가되는 별개의 테이블이 있으면 어떨까요? no sql이 느리다곤 하지만 join을 없애서 읽기 성능이 얼마만큼 좋아지는지… 의문입니다 ㅎㅎ - sale list, buy list를 합쳐서 collection을 만들고 index를 해놓으면 읽기를 하는 속도가 빨라질 수 있을 것 같다. - 그런데 buy request list는 삭제를 많이 해야하는데 이 때 index가 있으면 삭제 성능이 나빠지기 때문에 buy request list는 따로 두는 것이 좋을 것 같다. #### 2. recent comments를 post 안에 넣는 것이 좋을까? ``` post { writer: user_id, title : string, content : string, images: [url: string], // 개수 제한 category: string, recent_comments : [] // 최근 10개. 최근 댓글로 업데이트 하는 문제. 여러 사람이 동시에 댓글을 작성할 때 lock이 걸려서 post 읽기가 느려지는 문제. state : string, // 구매가 되었는지를 나타내는 상태 bool으로 하면 예약 중 상태를 표현할 수 없어서 확장성을 고려해서 string으로 cost: number, uploadTime, // createdAt으로 자동으로 생성되면 안 써도 된다. } post_comments { post_id, writer_id, content } ``` - 장점 post를 읽을 때 최근 댓글을 빠르게 읽을 수 있다. - 단점 댓글을 여러명이 한번에 작성하면 document에 lock이 걸려서 해당 document의 읽기 성능이 나빠진다. 검색했을 때 가져오는 post data에 댓글 데이터는 포함이 안돼도 된다. 최신 댓글 10개를 유지하는 문제가 있다. ###### tags: `tech sharing`