美文网首页python和大数据研究
pymongo 亿级数据查询技术总结之一

pymongo 亿级数据查询技术总结之一

作者: 小肥爬爬 | 来源:发表于2020-08-04 19:32 被阅读0次

    序言

    这么多年来做过好几个使用mongodb的项目, 这里主要记录下大数据使用上的一些技巧和要点. 在公司项目我用java, 个人总结一般用python. (反正都是调用mongodb 本身驱动提供的api, 语言本身的影响可以忽略不计) 这里用的是pymongo, java的话用spring框架提供的api, 原理和用法是相通的.

    大数据很大, 有多大呢? 我最大处理过的数据量级只是TB级别. 不过mongodb在亿级记录的情况下就必须上索引和使用各种技巧, 否则性能就会显著下降. 之后所有的招数都在拆分表结构和怎么扩容机器用空间换时间上. 再讨论下去就要结合具体业务, 不适合作为入门案例讨论了. 所以这里就只讨论下单机亿级数据量的处理.

    亿级数据下find的性能

    不多说理论, 先上代码再bb. 先插入一亿条数据, 新建一个 one_hundred_million_data_in_pymongo_1.py 文件, 敲入代码如下:

    #! /usr/bin/python
    # -*- coding: UTF-8 -*-
    
    """
    
        作者: 小肥巴巴
        简书: https://www.jianshu.com/u/db796a501972
        邮箱: imyunshi@163.com
        github: https://github.com/xiaofeipapa/python_example
    
        您可以任意转载, 恳请保留我作为原作者, 谢谢.
    
    
    """
    from helper import random_name, CalTime, user_coll
    
    
    @CalTime()
    def insert_example_data():
    
        # data_count = 10000            # 插入1万条数据是 139.33 ms , 0.1 秒量级
        data_count = 100000000          # 插入1亿条数据是 1391.1216 秒
    
        batch_list = []
        count = 0
        for index in range(0, data_count):
    
            name, sex = random_name()
    
            doc = {
                'name': name,
                'sex': sex,
                'isUse': 1
            }
    
            batch_list.append(doc)
    
            # 单线城状态下, 批量数据设高点效率更高
            if len(batch_list) >= 10000:
                count += len(batch_list)
                user_coll.insert_many(batch_list)
                batch_list.clear()
                print('---- 已插入%d万条数据' % (count / 10000))
    
        # ---- end for
    
        if len(batch_list) >= 10000:
            user_coll.insert_many(user_coll, batch_list)
    
        print('---- finished')
    
    
    @CalTime()
    def test_count_no_index():
        """
        测试从亿级数据中find and count的性能
    
        :return:
        """
    
        # ---- 无索引情况
        user_coll.drop_indexes()
        keyword = '慕容'
        query_filter = {
            'name': {'$regex': keyword, '$options': 'i'}
        }
        count = user_coll.find(query_filter).count()        # 方法1
        # count = user_coll.count(query_filter)               # 方法2
        # count = user_coll.count_documents(query_filter).count()        # 方法3
    
        print('=== 找到用户的数量: %d ' % count)
    
    
    if __name__ == '__main__':
        # insert_example_data()
        test_count_no_index()
    
    

    一些工具型方法放在helper.py 文件下, 代码如下:

    #! /usr/bin/python
    # -*- coding: UTF-8 -*-
    
    """
    
        作者: 小肥巴巴
        简书: https://www.jianshu.com/u/db796a501972
        邮箱: imyunshi@163.com
        github: https://github.com/xiaofeipapa/python_example
    
        您可以任意转载, 恳请保留我作为原作者, 谢谢.
    
        随机生成名字, 参考了这篇: https://blog.csdn.net/qq_41426326/article/details/91975774
        非常感谢原作者.
    
    """
    
    import random
    from timeit import default_timer
    import pymongo
    
    """
        在本地使用 py_bd 数据库
    
    
        设计表结构:
    
        Users:
            name: 姓名, 随机生成
            sex: 1: 女性 2:男性
            isUse: 1: 未使用 2: 已使用, 留作测试. 默认为1
    
    
    """
    db_url = 'mongodb://localhost:27017/py_bd'
    g_mc = pymongo.MongoClient(db_url)
    g_mc = g_mc[db_url.split('/')[-1]]  # 自动找最后一个字符串作为数据库名称
    
    # 如果没有这个collection, mongodb 会自动创建
    user_coll = g_mc['UserData']
    
    
    # 计算时间的类
    class CalTime(object):
    
        # 带参数版本
        # def __init__(self, unit=TIME_UNIT_MS):
        #     self.unit = unit
    
        def __init__(self, label='总时间'):
            self.label = label
    
        def __call__(self, func):
            def wrapper(*args, **kwargs):
                t_begin = default_timer()
                result = func(*args, **kwargs)
                diff = (default_timer() - t_begin)
    
                print("==== " + self.label + ": %0.4f 秒 " % diff)
    
                return result
    
            return wrapper
    
    
    def random_name():
        # 删减部分,比较大众化姓氏
        firstName = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻水云苏潘葛奚范彭郎鲁韦昌马苗凤花方俞任袁柳鲍史唐费岑薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅卞齐康伍余元卜顾孟平" \
                    "黄和穆萧尹姚邵湛汪祁毛禹狄米贝明臧计成戴宋茅庞熊纪舒屈项祝董粱杜阮席季麻强贾路娄危江童颜郭梅盛林刁钟徐邱骆高夏蔡田胡凌霍万柯卢莫房缪干解应宗丁宣邓郁单杭洪包诸左石崔吉" \
                    "龚程邢滑裴陆荣翁荀羊甄家封芮储靳邴松井富乌焦巴弓牧隗山谷车侯伊宁仇祖武符刘景詹束龙叶幸司韶黎乔苍双闻莘劳逄姬冉宰桂牛寿通边燕冀尚农温庄晏瞿茹习鱼容向古戈终居衡步都耿满弘国文东殴沃曾关红游盖益桓公晋楚闫"
        # 百家姓全部姓氏
        # firstName = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻柏水窦章云苏潘葛奚范彭郎鲁韦昌马苗凤花方俞任袁柳酆鲍史唐费廉岑薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅皮卞齐康伍余元卜顾孟平" \
        #             "黄和穆萧尹姚邵湛汪祁毛禹狄米贝明臧计伏成戴谈宋茅庞熊纪舒屈项祝董粱杜阮蓝闵席季麻强贾路娄危江童颜郭梅盛林刁钟徐邱骆高夏蔡田樊胡凌霍虞万支柯昝管卢莫经房裘缪干解应宗丁宣贲邓郁单杭洪包诸左石崔吉钮" \
        #             "龚程嵇邢滑裴陆荣翁荀羊於惠甄麴家封芮羿储靳汲邴糜松井段富巫乌焦巴弓牧隗山谷车侯宓蓬全郗班仰秋仲伊宫宁仇栾暴甘钭厉戎祖武符刘景詹束龙叶幸司韶郜黎蓟薄印宿白怀蒲邰从鄂索咸籍赖卓蔺屠蒙池乔阴欎胥能苍" \
        #             "双闻莘党翟谭贡劳逄姬申扶堵冉宰郦雍舄璩桑桂濮牛寿通边扈燕冀郏浦尚农温别庄晏柴瞿阎充慕连茹习宦艾鱼容向古易慎戈廖庾终暨居衡步都耿满弘匡国文寇广禄阙东殴殳沃利蔚越夔隆师巩厍聂晁勾敖融冷訾辛阚那简饶空" \
        #             "曾毋沙乜养鞠须丰巢关蒯相查後荆红游竺权逯盖益桓公晋楚闫法汝鄢涂钦归海帅缑亢况后有琴梁丘左丘商牟佘佴伯赏南宫墨哈谯笪年爱阳佟言福百家姓终"
        # 百家姓中双姓氏
        firstName2 = "万俟司马上官欧阳夏侯诸葛闻人东方赫连皇甫尉迟公羊澹台公冶宗政濮阳淳于单于太叔申屠公孙仲孙轩辕令狐钟离宇文长孙慕容鲜于闾丘司徒司空亓官司寇仉督子颛孙端木巫马公西漆雕乐正壤驷公良拓跋夹谷宰父谷梁段干百里东郭南门呼延羊舌微生梁丘左丘东门西门南宫南宫"
        # 女孩名字
        girl = '秀娟英华慧巧美娜静淑惠珠翠雅芝玉萍红娥玲芬芳燕彩春菊兰凤洁梅琳素云莲真环雪荣爱妹霞香月莺媛艳瑞凡佳嘉琼勤珍贞莉桂娣叶璧璐娅琦晶妍茜秋珊莎锦黛青倩婷姣婉娴瑾颖露瑶怡婵雁蓓纨仪荷丹蓉眉君琴蕊薇菁梦岚苑婕馨瑗琰韵融园艺咏卿聪澜纯毓悦昭冰爽琬茗羽希宁欣飘育滢馥筠柔竹霭凝晓欢霄枫芸菲寒伊亚宜可姬舒影荔枝思丽'
        # 男孩名字
        boy = '伟刚勇毅俊峰强军平保东文辉力明永健世广志义兴良海山仁波宁贵福生龙元全国胜学祥才发武新利清飞彬富顺信子杰涛昌成康星光天达安岩中茂进林有坚和彪博诚先敬震振壮会思群豪心邦承乐绍功松善厚庆磊民友裕河哲江超浩亮政谦亨奇固之轮翰朗伯宏言若鸣朋斌梁栋维启克伦翔旭鹏泽晨辰士以建家致树炎德行时泰盛雄琛钧冠策腾楠榕风航弘'
        # 名
        name = '中笑贝凯歌易仁器义礼智信友上都卡被好无九加电金马钰玉忠孝'
        # 10%的机遇生成双数姓氏
        if random.choice(range(100)) > 10:
            firstName_name = firstName[random.choice(range(len(firstName)))]
        else:
            i = random.choice(range(len(firstName2)))
            firstName_name = firstName2[i:i + 2]
    
        sex = random.choice(range(2))
        name_1 = ""
        # 生成并返回一个名字
        if sex > 0:
            girl_name = girl[random.choice(range(len(girl)))]
            if random.choice(range(2)) > 0:
                name_1 = name[random.choice(range(len(name)))]
            return firstName_name + name_1 + girl_name, 1
        else:
            boy_name = boy[random.choice(range(len(boy)))]
            if random.choice(range(2)) > 0:
                name_1 = name[random.choice(range(len(name)))]
            return firstName_name + name_1 + boy_name, 2
    
    
    @CalTime('创建索引')
    def add_index_if_not_exists(index_list):
        """
        如果索引不存在, 创建索引
        :return:
        """
    
        # 循环查找已存在的
        add_list = []
        for data in index_list:
            # print(data)
            name, sort = data
    
            has = False
            for exist_index in user_coll.list_indexes():
                exist_name = exist_index['name'].split('_')[0]
                if exist_name == name:
                    print('---- 索引%s 已经存在' % name)
                    has = True
    
            if not has:
                add_list.append(data)
    
        if len(add_list) > 0:
            # print(add_list)
            user_coll.ensure_index(add_list)
            print('=== 已创建索引: ', add_list)
    
        # print(data)
        # print(data['name'])
        # for detail in data:
        #     print(detail)
    
    
    def _test_create_index():
    
        index_list = [
            ('name', pymongo.ASCENDING),
            ('sex', pymongo.ASCENDING),
        ]
    
        add_index_if_not_exists(index_list)
    
    
    if __name__ == '__main__':
        # print(random_name())
        _test_create_index()
    
    
    

    这两大个文件的代码思路如下:

    1. 插入1亿条数据, 字段包括 name(姓名), sex(性别), isUse(是否使用, 在讲事务锁的时候会用到).
    2. 名字用随机生成的名字. 网上找的一段程序, 感谢原作者, 也在代码里贴了引用链接.
    3. 分别测试pymongo的两个api: find 和 count . 前者查找名字包含"慕容"的用户, 后者统计这些用户的数量. 其中count 有3种方式可以用, 其实结果都是一样的.

    count 的测试结果

    以上代码模仿了真实的案例. 假设你有一个老板, 要你统计在这些用户里姓"慕容"的人有多少. (你当然可以换成张/李/陈/王), 新人大概就会随手用上以上API, 这些都是在官方文档里记录的API. 但是结果很遗憾, 当你实际一跑的时候会发现, 这个程序用时很久, 在我的电脑上大概是 43 秒左右. 如果你以 mongodb count with poor performance 为关键字在google 搜索, 搜不出太多有用的信息. (这也是我写这个案例的初衷).

    数据库优化首要策略: 加索引

    运用算法知识来简单分析, 数据库的表结构类似二维表, 查找某个列的内容时就需要遍历 row 行 * column 列, 最终的时间是 O(row * column ). 而创建索引之后, 索引会保存到单独的空间, 类似一维表的结构, 算法的度量是 O(row) , 是一种用空间(内存)换取时间的策略. 任何上了数据量的数据库应用, 肯定要先加索引.
    (其实在mongodb 里加索引是有严格先后顺序的, 后面会说到)

    新建一个 one_hundred_million_data_in_pymongo_2.py , 敲入以下代码:

    #! /usr/bin/python
    # -*- coding: UTF-8 -*-
    
    """
    
        作者: 小肥巴巴
        简书: https://www.jianshu.com/u/db796a501972
        邮箱: imyunshi@163.com
        github: https://github.com/xiaofeipapa/python_example
    
        您可以任意转载, 恳请保留我作为原作者, 谢谢.
    
    
    """
    from helper import random_name, CalTime, user_coll, add_index_if_not_exists
    import pymongo
    
    
    @CalTime()
    def test_count_with_index():
        """
        测试从亿级数据中find and count的性能, 带索引
    
        :return:
        """
    
        # ---- 将name 加上索引
        index_list = [
            ('name', pymongo.ASCENDING)
        ]
        add_index_if_not_exists(index_list)
    
        # 继续查询
        keyword = '慕容'
        query_filter = {
            'name': {'$regex': keyword, '$options': 'i'}
        }
        count = user_coll.find(query_filter).count()            # 方法1
        print('=== 找到用户的数量: %d ' % count)
    
    
    if __name__ == '__main__':
        test_count_with_index()
    
    

    运行这段程序, 结果会非常出乎你的意料. 加索引会非常慢, 在我的机器用时 236 秒左右, 这是在预计之中的. 然而 count 的查询, 加了索引和没加没有区别? 还是要花费50多秒? 问题究竟出在哪里呢? 欲知后事如何, 留待下回填坑.

    相关文章

      网友评论

        本文标题:pymongo 亿级数据查询技术总结之一

        本文链接:https://www.haomeiwen.com/subject/vpwwrktx.html