zoukankan      html  css  js  c++  java
  • Mongo的应用程序设计

      本章介绍如何设计应用程序,以便更好地使用MongoDB , 内容包括:

      • 内嵌数据和引用数据之间的权衡;
      • 优化技巧,
      • 数据一致性;
      • 模式迁移;
      • 不适合使用 MongoDB 作为数据存储的场景。

     

    1.范式化与反范式化

      数据表示的方式有很多种,其中最重要的问题之一就是在多大程度上对数据进行范式化。 范式化( normalization ) 是将数据分散到多个不同的集合,不同集合之间可以相互引用数据。虽然很多文档可以引用某一块数据,但是这块数据只存储在一个集合中。所以,如果要修改这块数据,只需修改保存这块数据的那一个文档就行了。但是,MongoDB 没有提供连接(join ) 工具,所以在不同集合之间执行连接査询需要进行多次査询。

      反范式化( denormalization) 与范式化相反:将每个文档所需的数据都嵌入在文档内部。每个文档都拥有自己的数据副本,而不是所以文档共同引用同一个数据副本。这意味着,如果信息发生了变化,那么所有相关文档都需要进行更新,但是在执行查询时,只需要一次査询,就可以得到所有数据。

      决定何时采用范式化何时采用反范式化是比较困难的。范式化能够提高数据写入速度,反范式化能够提高数据读取速度。需要根据自己应用程序的实际需要仔细权衡。

     

    1.1 数据表示的例子

      假设要保存学生和课程信息。一种表示方式是使用一个students 集合 (每个学生是一个文档)和 一个classes 集合(每门课程是一个文档)。然后用第三个集合studentClasses 保存学生和课程之间的联系。

    > db.studentClasses.findOne({"studentId" : id})
    {
        "_id" : ObjectId("512512c1d86041c7dca81915"),
        "studentId" : ObjectId("512512a5d86041c7dca81914"),
        "classes" : [
            ObjectId("512512ced86041c7dca81916"),
            ObjectId("512512dcd86041c7dca81917"),
            ObjectId("512512e6d86041c7dca81918"),
            ObjectId("512512f0d86041c7dca81919")
        ]
    }

      如果比较熟悉关系型数据库,可能你之前见过这种类型的表连接,虽然你的每个结果文档中可能只有一个学生和一门课程(而不是一个课程"__id"列表)。将课程放在数组中,这有点儿 MongoDB 的风格,不过实际上通常不会这么保存数据,因为要经历很多次查询才能得到真实信息。

      假设要找到一个学生所选的课程。需要先查找 students 集合找到学生信息,然后查询studentClasses 找到课程"__id",最后再查询 classes 集合才能得到想要的信息。为了找出课程信息,需要向服务器请求三次查询。很可能你并不想在MongoDB中用这种数据组织方式,除非学生信息和课程信息经常发生变化,而且对数据读取速度也没有要求。

      如果将课程引用嵌入在学生文档中,就可以节省一次査询:

    {
        "_id" : ObjectId("512512a5d86041c7dca81914"),
        "name" : "John Doe",
        "classes" : [
            ObjectId("512512ced86041c7dca81916"),
            ObjectId("512512dcd86041c7dca81917"),
            ObjectId("512512e6d86041c7dca81918"),
            ObjectId("512512f0d86041c7dca81919")
        ]
    }

      "classes"字段是一个数组,其中保存了 John Doe需要上的课程"_id"。需要找出这些课程的信息时,就可以使用这些查询classes集合。这个过程只需要两次查询。如果数据不需要随时访问也不会随时发生变化(“随时”比“经常”要求更高),那么这种数据组织方式是非常好的。

      如果需要进一步优化读取速度,可以将数据完全反范式化,将课程信息作为内嵌文档保存到学生文档的"classes"字段中,这样只需要一次查询就可以得到学生的课程信息了:

    {
        "_id" : ObjectId("512512a5d86041c7dca81914"),
        "name" : "John Doe",
        "classes" : [
            {
                "class" : "Trigonometry",
                "credits" : 3,
                "room" : "204"
            },
            {
                "class" : "Physics",
                "credits" : 3,
                "room" : "159"
            },
            {
                "class" : "Women in Literature",
                "credits" : 3,
                "room" : "14b"
            },
            {
                "class" : "AP European History",
                "credits" : 4,
                "room" : "321"
            }
        ]
    }

      上面这种方式的优点是只需要一次查询就可以得到学生的课程信息,缺点是会占用更多的存储空间,而且数据同步更困难。例如,如果物理学的学分变成了4分(不再是3分),那么选修了物理学课程的每个学生文档都需要更新,而不只是更新“Physics”文档。

      最后,也可以混合使用内嵌数据和引用数据:创建一个子文档数组用于保存常用信息,需要査询更详细信息时通过引用找到实际的文档:

    {
        "_id" : ObjectId("512512a5d86041c7dca81914"),
        "name" : "John Doe",
        "classes" : [
            {
                "_id" : ObjectId("512512ced86041c7dca81916"),
                "class" : "Trigonometry"
            },
            {
                "_id" : ObjectId("512512dcd86041c7dca81917"),
                "class" : "Physics"
            },
            {
                "_id" : ObjectId("512512e6d86041c7dca81918"),
                "class" : "Women in Literature"
            },
            {
                "_id" : ObjectId("512512f0d86041c7dca81919"),
                "class" : "AP European History"
            }
        ]
    }

      这种方式也是不错的选择,因为内嵌的信息可以随着需求的变化进行修改:如果希望在一个页面中包含更多(或者更少)的信息,就可以将更多(或者更少)的信息放在内嵌文档中。

      需要考虑的另一个重要问题是,信息更新更频繁还是信息读取更频繁?如果这些数据会定期更新,那么范式化是比较好的选择。如果数据变化不频繁,为了优化更新效率而牺牲读取效率就不值得了。

      例如,教科书上介绍范式化的一个例子可能是将用户和用户地址保存在不同时集合中。但是,人们几乎不会改变住址,所以不应该为了这种概率极小的情况(某人改变了住址)而牺牲毎一次査询的效率。在这种情景下,应该将地址内嵌在用户文档中。

      如果决定使用内嵌文档,更新文档时,需要设置一个定时任务(cron job),以确保所做的每次更新都成功更新了所有文档。例如,我们试图将更新扩散到多个文档,在更新完所有文档之前,服务器崩溃了。需要能够检测到这种问题,并且重新进行未完的更新。

      一般来说,数据生成越频繁,就越不应该将这些数据内嵌到其他文档中。如果内嵌字段或者内嵌字段数量是无限增长的,那么应该将这些内容保存在单独的集合中,使用引用的方式进行访问,而不是内嵌到其他文档中。评论列表或者活动列表等信息应该保存在单独的集合中,不应该内嵌到其他文档中。

      最后,如果某些字段是文档数据的一部分,那么需要将这些字段内嵌到文档中。如果在査询文档时经常需要将某个字段排除,那么这个字段应该放在另外的集合中,而不是内嵌在当前的文档中。表8-1给出了一些指导原则。

    表8-1内嵌数据与引用数据的比较

    更适合内嵌

    更适合引用

    子文档较小

    子文档较大

    数据不会定期改

    数据经常改变

    最终数据一致即可

    中间阶段的数据必须一致

    文档数据小幅增加

    文档数据大幅增加

    数据通常需要执行二次査询才能获得

    数据通常不包含在结果中

    快速读取

    快速写入


      假如我们有一个用户集合。下面是一些可能需要的字段,以及它们是否应该内嵌到用户文档中。

    • 用户首选项(account preferences )

      用户首选项只与特定用户相关,而且很可能需要与用户文档内的其他用户信息一起査询。所以用户首选项应该内嵌到用户文档中。

    • 最近活动(recent activity )

      这个字段取决于最近活动增长和变化的频繁程度。如果这是个固定长度的字段 (比如最近的10次活动),那么应该将这个字段内嵌到用户文档中。

    • 好友(friends)

      通常不应该将好友信息内嵌到用户文档中,至少不应该将好友信息完全内嵌到用户文档中。下节会介绍社交网络应用的相关内容。

    • 所有由用户产生的内容

      不应该内嵌在用户文档中。

     

    1.2 基数

      一个集合中包含的对其他集合的引用数量叫做基数(cardinality)。常见的关系有一对一、一对多、多对多。假如有一个博客应用程序。每篇博客文章(post)都有一个标题(title),这是-个一对一的关系。每个作者(author)可以有多篇文章,这是一个一对多的关系。每篇文章可以有多个标签(tag),每个标签可以在多篇文章中使用,所以这是一个多对多的关系。

      在MongoDB中,many (多)可以被分拆为两个子分类:many (多)和few (少)。假如,作者和文章之间可能是一对少的关系:每个作者只发表了为数不多的几篇文章。博客文章和标签可能是多对少的关系:文章数量实际上很可能比标签数量多。博客文章和评论之间是一对多的关系:每篇文章都可以拥有很多条评论。

      只要确定了少与多的关系,就可以比较容易地在内嵌数据和引用数据之间进行权衡。通常来说,“少”的关系使用内嵌的方式会比较好,“多”的关系使用引用的方式比较好。

     

    1.3 好友、粉丝,以及其他的麻烦事项

      很多社交类的应用程序都需要链接人、内容、粉丝、好友,以及其他一些事物。对于这些高度关联的数据使用内嵌的形式还是引用的形式不容易权衡。这一节会介绍社交图谱数据相关的注意事项。通常,关注、好友或者收藏可以简化为一个发布-订阅系统:一个用户可以订阅另一个用户相关的通知。这样,有两个基本操作需要比较高效:如何保存订阅者,如何将一个事件通知给所有订阅者。

      比较常见的订阅实现方式有三种。第一种方式是将内容生产者内嵌在订阅者文档中:

    {
        "_id" : ObjectId("51250a5cd86041c7dca8190f"),
        "username" : "batman",
        "email" : "batman@waynetech.com"
        "following" : [
            ObjectId("51250a72d86041c7dca81910"), 
            ObjectId("51250a7ed86041c7dca81936")
        ]
    }

      现在,对于一个给定的用户文档,可以使用形如db.activities.find({"user" :{"$in" : user["following"]}})的方式查询该用户感兴趣的所有活动信息。但是,对于一条刚刚发布的活动信息,如果要找出对这条活动信息感兴趣的所有用户,就不得不査询所有用户的"following"字段了。

      另一种方式是将订阅者内嵌到生产生文档中:

    {
        "_id" : ObjectId("51250a7ed86041c7dca81936"),
        "username" : "joker",
        "email" : "joker@mailinator.com"
        "followers" : [
            ObjectId("512510e8d86041c7dca81912"),
            ObjectId("51250a5cd86041c7dca8190f"),
            ObjectId("512510ffd86041c7dca81910")
        ]
    }

      当这个生产者新发布一条信息时,我们立即就可以知道需要给哪些用户发送通知。这样做的缺点是,如果需要找到一个用户关注的用户列表,就必须査询整个用户集合。这种方式的优缺点与第一种方式的优缺点正好相反。

      同时,这两种方式都存在另一个问题:它们会使用户文档变得越来越大,改变也越来越频繁。通常,"following"和"followers"字段甚至不需要返回:査询粉丝列表有多频繁?如果用户比较频繁地关注某些人或者对一些人取消关注,也会导致大量的碎片。因此,最后的方案对数据进一步范式化,将订阅信息保存在单独的集合中,以避免这些缺点。进行这种程度的范式化可能有点儿过了,但是对于经常发生变化而且不需要与文档其他字段一起返回的字段,这非常有用。对"followers"字段做这种范式化是有意义的。

      用一个集合来保存发布者和订阅者的关系,其中的文档结构可能如下所示:

    {
        "_id" : ObjectId("51250a7ed86041c7dca81936"), // followee's "_id"
        "followers" : [
            ObjectId("512510e8d86041c7dca81912"),
            ObjectId("51250a5cd86041c7dca8190f"),
            ObjectId("512510ffd86041c7dca81910")
        ]
    }

      这样可以使用户文档比较精简,但是需要额外的査询才能得到粉丝列表。由 于"followers"数组的大小会经常发生变化,所以可以在这个集合上启用"usePower0f2Sizes",以保证users集合尽可能小。如果将followers集合保存在另一个数据库中,也可以在不过多影响users集合的前提下对其进行压缩。

     

      应对威尔•惠顿效应

      不管使用什么样的策略,内嵌字段只能在子文档或者引用数量不是特别大的情况下有效发挥作用。对于比较有名的用户,可能会导致用于保存粉丝列表的文档溢出。对于这种情况的一种解决方案是在必要时使用“连续的”文档。例如:

    > db.users.find({"username" : "wil"})
    {
        "_id" : ObjectId("51252871d86041c7dca8191a"),
        "username" : "wil",
        "email" : "wil@example.com",
        "tbc" : [
            ObjectId("512528ced86041c7dca8191e"),
            ObjectId("5126510dd86041c7dca81924")
        ]
        "followers" : [
            ObjectId("512528a0d86041c7dca8191b"),
            ObjectId("512528a2d86041c7dca8191c"),
            ObjectId("512528a3d86041c7dca8191d"),
            ...
        ]
    }
    {
        "_id" : ObjectId("512528ced86041c7dca8191e"),
        "followers" : [
            ObjectId("512528f1d86041c7dca8191f"),
            ObjectId("512528f6d86041c7dca81920"),
            ObjectId("512528f8d86041c7dca81921"),
            ...
        ]
    }
    {
        "_id" : ObjectId("5126510dd86041c7dca81924"),
        "followers" : [
            ObjectId("512673e1d86041c7dca81925"),
            ObjectId("512650efd86041c7dca81922"),
            ObjectId("512650fdd86041c7dca81923"),
            ...
        ]
    }

      对于这种情况,需要在应用程序中添加从"tbc" (to be continued)数组中取数据的相关逻辑。

     

    2.优化数据操作

      如果要优化应用程序,首先必须知道对读写性能进行评估以便找到性能瓶颈。对读取操作的优化通常包括正确使用索引,以及尽可能将所需信息放在单个文档中返回。对写入操作的优化通常包括减少索引数量以及尽可能提高更新效率。

      经常需要在写入效率更高的模式与读取效率更髙的模式之间权衡,所以必须要知道哪种操作对你的应用程序更重要。这里的影响因素并不只是读取和写入的重要性,也包括读取和写入操作的频繁程度:如果对你的应用程序来说写入操作更加重要,但是为了执行一次写入操作需要进行1000次读取操作,那么还是应该首先优化读取速度。

     

    2.1 优化文档增长

      更新数据时,需要明确更新是否会导致文件体积增长,以及增长程度。如果增长程度是可预知的,可以为文档预留足够的增长空间,这样可以避免文档移动,可以提髙写入速度。检査一下填充因子:如果它大约是1.2或者更大,可以考虑手动填充。 

      如果要对文档进行手动填充,可以在创建文档时创建一个占空间比较大的字段,文件创建成功之后再将这个字段移除。这样就提前为文档分配了足够的空间供后续使用。假设有一个餐馆评论的集合,其中的文档如下所示:

    {
        "_id" : ObjectId(),
        "restaurant" : "Le Cirque",
        "review" : "Hamburgers were overpriced."
        "userId" : ObjectId(),
        "tags" : []
    }

      "tags"字段会随着用户不断添加标签而增长,应用程序可能经常需要执行这样的更新操作:

    > db.reviews.update({"_id" : id}, 
    ... {"$push" : {"tags" : {"$each" : ["French", "fine dining", "hamburgers"]}}}})

      如果知道"tags"通常不会超过100字节,可以手工为文档留出足够的填充空间,这样可以避免更新文档时发生文档移动。如果不为文档预留增长空间,那么每当"tags"字段增长时,文档就会被移动。可以在文档最后添加一个大字段(随便用什么名字)进行手工填充,如下所示:

    {
        "_id" : ObjectId(),
        "restaurant" : "Le Cirque",
        "review" : "Hamburgers were overpriced."
        "userId" : ObjectId(),
        "tags" : [],
        "garbage" : "........................................................"+
            "................................................................"+
            "................................................................"
    }

      可以在第一次插入文档时这么做,也可以在upsert时使用"$set0nInsert"创建这个字段。

      更新文档时,总是用“$unset"移除"garbage"字段。

    > db.reviews.update({"_id" : id}, 
    ... {"$push" : {"tags" : {"$each" : ["French", "fine dining", "hamburgers"]}}},
    ...  "$unset" : {"garbage" : true}})

      如果"garbage"字段存在,"$unset"操作符可以将其移除,如果这个字段不存在,"$unset"操作符什么也不做。

      如果文档中有一个字段需要增长,应该尽可能将这个字段放在文档最后的位置 ("garbage"之前)。这样可以稍微提高一点点的性能,因为如果"tags"字段发生了增长,MongoDB不需要重写"tags"后面的字段。

     

    2.2 删除旧数据

      有些数据只在特定时间内有用:几周或者几个月之后,保留这些数据只是在浪费存储空间。有三种常见的方式用于删除旧数据:使用固定集合,使用TTL集合,或者定期删除集合。

      最简单的方式是使用固定集合:将集合大小设为一个比较大的值,当集合被填满时,将旧数据从固定集合中挤出。但是,固定集合会对操作造成一些限制,而且在密集插入数据时会大大降低数据在固定集合内的存活期。

      第二种方式是使用TTL集合,TTL集合可以更精确地控制删除文档的时机。但是,对于写入量非常大的集合来说这种方式可能不够快:它通过遍历TTL索引来删除文档。如果TTL集合能够承受足够的写入量,使用TTL集合删除旧数据可能是最简单的方式了。

      最后一种方法是使用多个集合:例如,每个月的文档单独使用一个集合。每当月份变更时,应用程序就开始使用新月份的集合(初始是个空集合),査询时要对当前月份和之前月份的集合都进行査询。对于6个月之前创建的集合,可以直接将其删除。这种方式可以应对任意的操作量,但是对于应用程序来说会比较复杂,因为需要使用动态的集合名称(或者数据库名称),也要动态处理对多个数据库的査询。

     

    3.数据库和集合的设计

      确定了文档结构之后,接下来就要确定使用什么样的集合或者数据库来保存文档。通常这个过程很简单,但是有一些指导原则需要注意。

      通常,具有相近模式的文档应该放在相同的集合中。MongoDB通常不允许使用多个集合进行数据组合,如果有些文档需要进行集中査询或者聚合,那么这些文档应该放在同一个大集合里。例如,可能有一些结构非常不同的文档,但是如果要对它们进行聚合,就需要让它们位于同一个集合内。

      对于数据库来说,最大的问题是锁机制(每个数据库上都有一个读/写锁)和存储。每一个数据库,在磁盘上都位于自己的文件中(通常也在单独的文件夹中),这意味着,可以让不同的数据库位于不同的磁盘分卷。所以,你可能希望数据库内的所有项目都拥有相近的“质量”、相近的访问模式,或者相近的访问量。假设我们有一个拥有多个组件的应用程序:日志组件会创建大量的日志数据(日志数据不是很重要),还要有一个用户集合,以及几个用于保存用户生成数据的集合。用户集合是最有价值的:保证用户数据安全是非常重要的。社交活动数据需要放在一个大流量集合中,它不如用户集合重要,但是比日志集合重要。这个集合主要用于用户通知,所以几乎是一个只插入不更新的集合。

      按照重要性进行拆分,最后可能得到三个数据库:logs (日志)、activities (活动)、 users (用户)。这样做的好处是,最重要的数据集合的数据量可能最小(例如,用户集合内的数据通常不如日志集合多)。将所有数据集都存储在SSD上你可能负担不起,但是也许可以只将用户集合存储在SSD上。或者对用户集合使用RAID10,而对日志和活动集合使用RAIDO。

      注意,使用多个数据库时有一些限制:MongoDB通常不允许直接将数据从一个数据库移到另一个数据库。例如,无法将在A数据库上执行MapReduce的结果保存到B数据库中,也无法使用renameCollection命令将集合从一个数据库移动到另一个数据库(比如,可以将foo.bar重命名为foo.baz,但是不能将foo.bar重命名 foo2.baz)。

     

    4.—致性管理

      必须要明确知道应用程序的读取对数据一致性的要求有多高。MongoDB支持多种不同的一致性级别,从每次都读到完全正确的最新数据到读取不确定新旧程度的数据。如果要得到最近一年内的活动信息报表,可能只要求最近这些天的数据完全准确。相反,如果要做实时交易,可能需要即时读到最新的数据。

      要理解如何获得这些不同级别的一致性,首先要了解MongoDB的内部机制。服务器为毎个数据库连接维护一个请求队列。客户端每次发来的新请求都会添加到队列的末尾。入队之后,这个连接上的请求会依次得到处理。一个连接拥有一个一致的数据库视图,可以总是读取到这个连接最新写入的数据。

      注意,每个列队只对应一个连接:如果打开两个shell,连接到相同的数据库,这时就存在两个不同的连接。如果在其中一个shell中执行插入操作,紧接着在另一个shell中执行查询操作,新插入的数据可能不会出现在查询结果中。但是,如果是在同一个shell中,插入一个文档然后执行査询,一定能够查询到刚插入的文档。想手动重现这种问题是很困难的,但是在一个频繁执行插入和査询的服务器上很可能会发生。经常会有一些开发者使用一个线程插入数据,然后使用另一个线程检査数据是否成功插入。片刻之后,刚刚的数据看上去好像并没有成功插入,但是这些数据忽然就出现了。

      使用Ruby、Python和Java驱动程序时尤其要注意这个问题,因为这三种语言的驱动程序都使用了连接池(connection pool)。为了提高效率,这些驱动程序会建立多个与服务器之间的连接(也就是一个连接池),将请求通过不同的连接发送到服务器。但是它们都有各自的机制来保证一系列相关的请求会被同一个连接处理。关于不同语言连接池的详细文档,可以査看MongoDB Wiki (http://dochub.mongodb.org/drivers/connections) 。

      当向副本集备份节点发送读取请求时,就更麻烦了。副本集的数据可能不是最新的,这会导致读取到的数据是一秒钟之前或者一分钟之前的,甚至是几个小时之前的。处理这个问题的方式有好几种,最简单的一种是将所有读取请求都发送到主数据库,这样便可以每次都得到最新最准确的数据。也可以设置一个脚本自动检测副本集是否落后于主数据库,如果落后,就将副本集设为维护状态。如果你的副本集比较小,可以使用"w" : setSize执行安全写入,如果getLastError没能成功返回,可将后续的读取请求发送到主数据库。

     

    5.模式迁移

      随着应用程序使用时间的增长和需求变化,数据库模式可能也需要相应地增长和改变。有几种方式可以实现这个需求,不管使用哪种方法,都要小心保存该程序使用过的毎一个模式。

      最简单的方式就是在应用程序需要时改进数据库模式,以确保应用程序能够支持所有旧版的模式(比如,要能够从容处理某些字段的缺失,或者是某些字段在不同版本中的不同类型)。这种方式可能会导致混乱,尤其是不同版本的模式之间有冲突时。例如,版本A要求有"mobile"字段,但版本B没有"mobile"字段,却需要有另外一个不同字段,同时还有个版本C认为"mobile"字段是可选的。为了满足这样的需求可能会逐步把代码变得一团糟。

      另一种稍微结构化一点儿的解决方案是在每个文档中包含一个"version"字段(或者"v"),使用这个字段来决定应用程序能够接受的文档结构。这种方式对模式的要求更加严格:文档必须对多个版本都有效。这仍然需要支持各种旧版本。

      最后一种方式是,当模式发生变化时,将数据进行迁移。通常来说这并不是个好主意:MongoDB允许使用动态模式,以避免执行迁移,因为执行迁移会对系统造成很大的压力。但是,如果决定改变每一个文档,需要确保所有文档都被成功更新。MongoDB中的多文档更新并不是原子的(原子是指要么所有文档都成功更新,要么一个也不更新)。如果MongoDB在迁移过程中崩溃,最终的结果可能会是只有一部分文档被更新,还有一部分没有更新。

     

    6.不适合使用MongoDB的场景

      尽管MongoDB是一个通用型数据库,可以用在大部分应用程序中,但它并非万能的。MongoDB不支持下面这些应用场景。

      • MongoDB不支持事务(transaction),对事务性有要求的应用程序不建议使用MongoDB。可以用几种方式实现简单的类事务(transaction-like)语义,尤其是操作单个文档时,但是数据库并不能强制要求用户这么做。因此,你可以让所有客户端都遵守你设定的某种语义规范(比如,执行任何操作之前都要先检査锁),但是无法阻挡不知情的用户或恶意用户把事情变成一团糟。(4.0版本以后增加事务功能)
      • 在多个不同维度上对不同类型的数据进行连接,这是关系型数据库善长的事情。 MongoDB不支持这么做,以后也很可能不支持。
      • 最后,如果你使用的工具不支持MongoDB,那可能你应该选择一个关系型数据库,而不是MongoDB。有很多工具并不支持MongoDB,从SQLAl chemy到Wordpress。支持MongoDB的工具已经越来越多了,但是目前来说仍然不如关系型数据库多。

    作者:小家电维修

    相见有时,后会无期。

  • 相关阅读:
    LightOj 1016
    uva 127 "Accordian" Patience 简单模拟
    hdu 1180 诡异的楼梯 BFS + 优先队列
    UVALive 3907 Puzzle AC自动机+DP
    HDU 4001 To Miss Our Children Time DP
    HDU 4000 Fruit Ninja 树状数组
    hdu_1021_Fibonacci Again_201310232237
    hdu_1005_Number Sequence_201310222120
    hdu_1029-Ignatius and the Princess IV_201310180916
    hdu_1020_Encoding_201310172120
  • 原文地址:https://www.cnblogs.com/lizexiong/p/15377246.html
Copyright © 2011-2022 走看看