使用桶模式对数据进行分组
存储桶模式将长系列数据分成不同的对象。将大型数据序列分成较小的群组可以改善查询访问模式并简化应用程序逻辑。当您拥有与中心实体相关的类似对象(例如单个用户进行的股票交易)时,存储桶很有用。
您可以使用桶模式进行分页,根据应用程序每页显示的元素数量对数据进行分组。此方法使用 MongoDB 的灵活数据模型,根据应用程序所需的数据存储数据。
提示
时间序列集合会自动应用存储桶模式,适用于大多数涉及对时间序列数据进行分桶的应用程序。
关于此任务
考虑以下追踪股票交易的模式。初始模式未使用存储桶模式,并将每笔交易存储在单个文档中。
db.trades.insertMany( [ { "ticker" : "MDB", "customerId": 123, "type" : "buy", "quantity" : 419, "date" : ISODate("2023-10-26T15:47:03.434Z") }, { "ticker" : "MDB", "customerId": 123, "type" : "sell", "quantity" : 29, "date" : ISODate("2023-10-30T09:32:57.765Z") }, { "ticker" : "GOOG", "customerId": 456, "type" : "buy", "quantity" : 50, "date" : ISODate("2023-10-31T11:16:02.120Z") } ] )
该应用程序一次显示单个客户进行的股票交易,每页显示 10 笔交易。 为了简化应用程序逻辑,使用存储桶模式按 customerId
对交易进行分组,每组 10 个。
步骤
按 customerId 对数据进行分组
重新组织模式,使每个 customerId
都有一个文档:
{ "customerId": 123, "history": [ { "type": "buy", "ticker": "MDB", "qty": 419, "date": ISODate("2023-10-26T15:47:03.434Z") }, { "type": "sell", "ticker": "MDB", "qty": 29, "date": ISODate("2023-10-30T09:32:57.765Z") } ] }, { "customerId": 456, "history": [ { "type" : "buy", "ticker" : "GOOG", "quantity" : 50, "date" : ISODate("2023-10-31T11:16:02.120Z") } ] }
使用桶模式:
具有通用
customerId
值的文档将压缩到单个文档中,其中customerId
为顶级字段。该客户的交易被分组到一个名为
history
的嵌入式数组字段中。
为每个存储桶添加标识符和计数
1 db.trades.drop() 2 3 db.trades.insertMany( 4 [ 5 { 6 "_id": "123_1698349623", 7 "customerId": 123, 8 "count": 2, 9 "history": [ 10 { 11 "type": "buy", 12 "ticker": "MDB", 13 "qty": 419, 14 "date": ISODate("2023-10-26T15:47:03.434Z") 15 }, 16 { 17 "type": "sell", 18 "ticker": "MDB", 19 "qty": 29, 20 "date": ISODate("2023-10-30T09:32:57.765Z") 21 } 22 ] 23 }, 24 { 25 "_id": "456_1698765362", 26 "customerId": 456, 27 "count": 1, 28 "history": [ 29 { 30 "type" : "buy", 31 "ticker" : "GOOG", 32 "quantity" : 50, 33 "date" : ISODate("2023-10-31T11:16:02.120Z") 34 } 35 ] 36 }, 37 ] 38 )
_id
字段值是 customerId
和 history
字段中首次交易时间(以秒为单位,自 Unix 纪元以来)的组合。
count
字段指示该文档的history
大量中有多少个元素。 count
字段用于实现分页逻辑。
后续步骤
更新模式以使用桶模式后,请更新用于读取和写入数据的应用程序逻辑。请参阅以下部分:
使用桶模式查询数据
在更新的模式中,每个文档都包含应用程序中单个页面的数据。您可以使用 _id
和 count
字段来确定如何返回和更新数据。
要查询相应页面的数据,可使用 regex 查询返回指定 customerId
的数据,并使用 skip
返回正确页面的数据。对 _id
的 regex 查询使用默认 _id 索引,因此无需额外索引即可实现高性能查询。
以下查询返回客户 123
第一页交易的数据:
db.trades.find( { "_id": /^123_/ } ).sort( { _id: 1 } ).limit(1)
要返回后续页面的数据,请指定一个 skip
值,该值比要显示其数据的页面小 1。例如,要显示页面 10 的数据,请运行以下查询:
db.trades.find( { "_id": /^123_/ } ).sort( { _id: 1 } ).skip(9).limit(1)
注意
前面的查询没有返回结果,因为示例数据仅包含第一页的文档。
使用桶模式插入数据
既然该模式使用了桶模式,请更新您的应用程序逻辑以将新的交易插入到正确的桶中。使用更新命令,通过适当的 customerId
和桶将交易插入到桶中。
以下命令为 customerId: 123
插入一项新交易:
db.trades.updateOne( { "_id": /^123_/, "count": { $lt: 10 } }, { "$push": { "history": { "type": "buy", "ticker": "MSFT", "qty": 42, "date": ISODate("2023-11-02T11:43:10") } }, "$inc": { "count": 1 }, "$setOnInsert": { "_id": "123_1698939791", "customerId": 123 } }, { upsert: true } )
应用程序每页显示10笔交易。 更新过滤搜索文档中的customerId: 123
,其中count
小于10 ,即存储桶不包含整页数据。
如果存在与
"_id": /^123_/
匹配的文档,且其count
小于10 ,则更新命令会将新交易推送到匹配文档的history
大量中。如果没有匹配的文档,更新命令将插入包含新交易的新文档(因为
upsert
是true
)。 新文档的_id
字段是customerId
和自交易的Unix纪元以来的时间(以秒为单位)的串联。
更新命令的逻辑通过确保没有 history
数组包含超过 10 个文档来避免无界数组。
运行更新操作后,trades
集合会包含以下文档:
[ { _id: '123_1698349623', customerId: 123, count: 3, history: [ { type: 'buy', ticker: 'MDB', qty: 419, date: ISODate("2023-10-26T15:47:03.434Z") }, { type: 'sell', ticker: 'MDB', qty: 29, date: ISODate("2023-10-30T09:32:57.765Z") }, { type: 'buy', ticker: 'MSFT', qty: 42, date: ISODate("2023-11-02T11:43:10.000Z") } ] }, { _id: '456_1698765362', customerId: 456, count: 1, history: [ { type: 'buy', ticker: 'GOOG', quantity: 50, date: ISODate("2023-10-31T11:16:02.120Z") } ] } ]
结果
实现存储桶模式后,您无需纳入分页逻辑即可在应用程序中返回结果。 数据的存储方式与其在应用程序中的使用方式相匹配。