php - 如何创建一个对大型动态数据集中的标签进行计数的性能系统

标签 php mongodb design-patterns database-design architecture

概述

我有一个 iOS 应用程序,人们可以通过标签进行搜索,其中一些标签是预定义的,一些标签是用户定义的。

当用户编写他/她想要搜索的标签时,我想显示一行,显示这些标签可用的结果数量(请参阅示例搜索图片)。

注意: #Exercise#Routine 是父标签,意味着该人始终会使用其中第一个。

我正在使用 PHPMongoDB 服务器端。 我想创建一个文件,其中标签每小时计数一次,以便所有客户端都可以获取它并最大限度地减少资源消耗。

鉴于所操纵的信息是用户控制的标签,随着时间的推移,该列表将大大扩展。

挑战

  • 考虑到以下因素,我对最好的方法是什么感到困惑 创建、操作和存储此类的性能和开销 列表。

我的第一个想法是创建一个二维数组(见图)来存储我的所有值。然后,该数据将被转换为 JSON,以便可以存储到 MongoDB 中。

但是这种方法将使我获取所有标签并将它们加载到内存中以执行任何 +1 或 -1 操作。因此,我认为这可能不是最好的。

所有操作都将发生在每个元素的插入、更新和删除上。因此,将会有相当大的 RAM 开销。

我的第二个想法是创建一个文档,在其中存储所有使用过的标签,并每小时进行计数查询以生成列表由客户使用。

这意味着每次删除、更新和插入时都会检查该文档上是否存在该标记,并根据条件创建或删除或不执行任何操作。

每小时,获取所有标签,生成包含所有标签组合的数组。针对所有标签组合查询数据库并计算返回结果的数量并创建文件。

鉴于我使用 MongoDB 而不是 MySQL,我认为这种方法可能是更好的方法。但我仍然不确定它的性能。

有人制作过类似的系统并可以提供更好方法的建议吗?

搜索示例图片

二维数组

最佳答案

使用 mongodb 时要考虑的最重要的事情之一是,在决定数据库设计时必须考虑应用程序的访问模式。我们将尝试通过使用示例来解决您的问题,并看看它是如何工作的。

假设您的收藏中有以下文档,让我们在下面看看如何使这种简单的数据格式创造奇迹:

> db.performant.find()
{ "_id" : ObjectId("522bf7166094a4e72db22827"), "name" : "abc", "tags" : [  "chest",  "bicep",  "tricep" ] }
{ "_id" : ObjectId("522bf7406094a4e72db22828"), "name" : "def", "tags" : [  "routine",  "trufala",  "tricep" ] }
{ "_id" : ObjectId("522bf75f6094a4e72db22829"), "name" : "xyz", "tags" : [  "routine",  "myTag",  "tricep",  "myTag2" ] }
{ "_id" : ObjectId("522bf7876094a4e72db2282a"), "name" : "mno", "tags" : [  "exercise",  "myTag",  "tricep",  "myTag2",  "biceps" ] }

首先,您绝对必须在标签上创建索引。 (如果需要,您可以创建复合索引)

> db.performant.ensureIndex({tags:1})
> db.performant.getIndexes()
[
    {
            "v" : 1,
            "key" : {
                    "_id" : 1
            },
            "ns" : "test.performant",
            "name" : "_id_"
    },
    {
            "v" : 1,
            "key" : {
                    "tags" : 1
            },
            "ns" : "test.performant",
            "name" : "tags_1"
    }
]

要从上述集合中查询标签数据,通常会使用 db.performant.find({tags:{$in:["bicep"]}}),但这不是一个好主意。让我告诉你原因:

> db.performant.find({tags:{$in:["bicep","chest","trufala"]}}).explain()
{
    "cursor" : "BtreeCursor tags_1 multi",
    "isMultiKey" : true,
    "n" : 2,
    "nscannedObjects" : 3,
    "nscanned" : 5,
    "nscannedObjectsAllPlans" : 3,
    "nscannedAllPlans" : 5,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
            "tags" : [
                    [
                            "bicep",
                            "bicep"
                    ],
                    [
                            "chest",
                            "chest"
                    ],
                    [
                            "trufala",
                            "trufala"
                    ]
            ]
    },
    "server" : "none-6674b8f4f2:27017"
}

您可能已经注意到,此查询正在执行整个集合扫描。这可能会让您想知道,如果我们没有使用该索引,为什么我们要添加该索引,我也想知道这一点。但不幸的是,这是一个 mongoDB 尚未解决的问题(至少据我所知)

幸运的是,我们可以解决这个问题,并且仍然使用我们在标签集合上创建的索引。方法如下:

> db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).explain()
{
    "clauses" : [
            {
                    "cursor" : "BtreeCursor tags_1",
                    "isMultiKey" : true,
                    "n" : 1,
                    "nscannedObjects" : 1,
                    "nscanned" : 1,
                    "nscannedObjectsAllPlans" : 1,
                    "nscannedAllPlans" : 1,
                    "scanAndOrder" : false,
                    "indexOnly" : false,
                    "nYields" : 0,
                    "nChunkSkips" : 0,
                    "millis" : 10,
                    "indexBounds" : {
                            "tags" : [
                                    [
                                            "bicep",
                                            "bicep"
                                    ]
                            ]
                    }
            },
            {
                    "cursor" : "BtreeCursor tags_1",
                    "isMultiKey" : true,
                    "n" : 0,
                    "nscannedObjects" : 1,
                    "nscanned" : 1,
                    "nscannedObjectsAllPlans" : 1,
                    "nscannedAllPlans" : 1,
                    "scanAndOrder" : false,
                    "indexOnly" : false,
                    "nYields" : 0,
                    "nChunkSkips" : 0,
                    "millis" : 0,
                    "indexBounds" : {
                            "tags" : [
                                    [
                                            "chest",
                                            "chest"
                                    ]
                            ]
                    }
            },
            {
                    "cursor" : "BtreeCursor tags_1",
                    "isMultiKey" : true,
                    "n" : 1,
                    "nscannedObjects" : 1,
                    "nscanned" : 1,
                    "nscannedObjectsAllPlans" : 1,
                    "nscannedAllPlans" : 1,
                    "scanAndOrder" : false,
                    "indexOnly" : false,
                    "nYields" : 0,
                    "nChunkSkips" : 0,
                    "millis" : 0,
                    "indexBounds" : {
                            "tags" : [
                                    [
                                            "trufala",
                                            "trufala"
                                    ]
                            ]
                    }
            }
    ],
    "n" : 2,
    "nscannedObjects" : 3,
    "nscanned" : 3,
    "nscannedObjectsAllPlans" : 3,
    "nscannedAllPlans" : 3,
    "millis" : 10,
    "server" : "none-6674b8f4f2:27017"
}

如您所见,n 与 nscanned 非常接近。扫描了 3 条记录 1,分别对应“bicep”、“chest”、“trufala”。由于“bicep”和“chest”属于同一个文档,因此只返回1个与其对应的结果。一般来说,count() 和 find() 都会进行有限的扫描,并且非常高效。此外,您永远不必为用户提供过时的数据。您还可以完全避免运行任何类型的批处理作业!

因此,通过使用这种方法,我们可以得出以下结论:如果您按 n 个标签进行搜索,并且每个标 checkout 现 m 次,则扫描的文档总数将是 n * m。现在考虑到您有大量标签和大量文档,并且您通过几个标签进行扫描(这些标签又对应于几个文档 - 尽管不是 1:1),结果将始终非常快,因为发生 1 个文档扫描每个标签和文档组合。

注意:这种方法永远无法覆盖索引,因为数组上有一个索引,即“isMultiKey”:true。您可以阅读有关覆盖索引的更多信息 here

局限性:每种方法都有局限性,这个也有! 对结果进行排序将产生极差的性能,因为它将扫描整个集合的次数等于传递给此查询的标签的次数,再加上它扫描与 $or 的每个参数相对应的附加记录。

> db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).sort({tags:1}).explain()
{
    "cursor" : "BtreeCursor tags_1",
    "isMultiKey" : true,
    "n" : 2,
    "nscannedObjects" : 15,
    "nscanned" : 15,
    "nscannedObjectsAllPlans" : 15,
    "nscannedAllPlans" : 15,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
            "tags" : [
                    [
                            {
                                    "$minElement" : 1
                            },
                            {
                                    "$maxElement" : 1
                            }
                    ]
            ]
    },
    "server" : "none-6674b8f4f2:27017"
} 

在本例中,它扫描 15 次,等于 3 次完整收集扫描(每次扫描 4 条记录)加上每个参数扫描的 3 条记录 $or

最终结论:如果您可以接受未排序的结果或者愿意在前端付出额外的努力来自己对它们进行排序,则使用此方法可以获得非常有效的结果。

关于php - 如何创建一个对大型动态数据集中的标签进行计数的性能系统,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18611531/

相关文章:

java - 从 php exec 或 shell_exec 运行 java jar 不适用于蜡染

php - 如何让 PHP 与 ADOdb 和 MySQL 一起工作?

php - 文本区域数据未完全插入

Node.js/MongoDB - 查询日期

c++ - 在初始化列表中初始化许多对象成员变量

php - 无法从 Android 连接到 PHP 中的数据库

python - 如何将 Django 连接到 MongoDB 数据库?

javascript - 不断检查日期是否已过期

Angular 设计模式 : MVC, MVVM 还是 MV*?

Delphi: View <-> 模型同步的良好模式/策略