循环查询数据的性能问题及优化

2020-06-06 16:20:44 浏览数 (1)

糟糕的代码,对代码维护、性能、团队协作都会造成负面影响,所以,先设计再实现,谋而后动。

这里的循环查询,指的是在一个for循环中,不断访问数据库来查询数据。在刚接手公司数据报表系统时,发现有很多每日报告跑的出奇的慢,通过一番诊断后,发现主要来自两个方面的因素:一是需要对数据库的某些字段建立和优化索引,二是存在了很多糟糕的代码,这些代码在一个循环中不断的访问数据库,查询数据。

本文将摘取其中的三个例子来说明如何避免循环查询带来的性能问题,涉及常用的三种数据存储:MySQL,MongoDB和Redis。

1. 使用IN查询替换for循环

  • 优化前代码(简化版,以MySQL为例):
代码语言:javascript复制
sql = 'SELECT A.real_name, A.phone, A.gender FROM tb_user AS A INNER JOIN tb_trade AS B on A.id=B.user_id WHERE B.id=%s;'for id in trade_ids:    user = db_mysql.find(sql, [id])
    # TODO: do some work

这个代码的本意是要查询每个交易的购买用户的信息,和其他信息拼接起来,然后写入EXCEL文件。这种写法,简单明了,只要按照逻辑来写就好了,然而随着交易越来越多,这段代码会跑的越来越慢,即使建立了索引,但是却无法避免每次的数据库访问开销。

优化后代码:

代码语言:javascript复制
sql = 'SELECT A.real_name, A.phone, A.gender, B.id FROM tb_user AS A INNER JOIN tb_trade AS B on A.id=B.user_id WHERE B.id IN (%s);'
place_holders = ','.join(map(lambda x: '%s', id_list))  users = db_mysql.findAll(sql % place_holders, [trade_ids])
for id in users:    # TODO: do some work

使用IN替换掉for循环,一次查询拿到所有的数据,然后在for循环中取处理业务逻辑。该方法在MySQL与Mongo中均可以使用,只是语法不同而已。

2. 使用聚合查询替换for循环

优化前代码(简化版,以MongoDB为例):

代码语言:javascript复制
avaliable_companies = []condition = {  'is_active': True,  'create_time': {'$lt': datatime.now()},  'suspended': False}
for company in companies:    condition['company'] = company['_id']  job = db_mongo.job.find_one(condition)  if job:      avaliable_companies.append(job)
count = len(avaliable_companies)

这段代码的本意是要查询截止到当前时间,生成的job记录是来自哪几家company。同样的,随着数据量的增加,这段代码会跑的越来越慢。

代码语言:javascript复制
pipeline = [  {'$match': {    'is_active': True,    'create_time': {'$lt': datatime.now()},    'suspended': False,    'company': {'$in': map(lambda x: x['_id'], companies)}  }},  {'$group': {'_id': 'company'}}]
agg_result = db_mongo.job.aggregate(pipeline)
count = len(list(agg_result))

使用聚合可以一次查询出结果,当然,这里也可以通过IN查询来做,同样可以提高性能。

3. 使用pipeline来查询redis

Redis通常用来做数据缓存,降低数据库的命中率,从而提供并发性能。然而,如果使用不当,你会发现虽然使用了缓存,但是时间查询效率并没特别大的提升。

  • 优化前代码(简化版):
代码语言:javascript复制
redis_cli = get_redis()for id in user_ids:    result = redis_cli.get('user_last_active_time:%d' % id)

这个代码本意是要查询一组用户的最近一次活跃时间,这些活跃时间都缓存在Redis中,但是这个代码,如果user_ids的列表很长,就会发现这个缓存查询很慢,因为每次访问redis都需要建立一次IO请求。

  • 优化后代码:
代码语言:javascript复制
redis_cli = get_redis()pipeline = redis_cli.pipeline(transaction=False)for d in user_ids:    pipeline.get('user_last_active_time:%d' % id)active_time_list = pipeline.execute()

使用Redis的pipeline来一次获取所有的数据,这么做会比上面的快几十倍,在数据量大的情况下。

上面通过三个实例来阐述循环查询对性能的影响和优化的方法,写这篇博客的目的并不仅仅要介绍这些技巧方法,因为技巧方法远不止这些,而是想借此传达一个观点:编程,应该设计先于写代码。虽然都是实现同样的逻辑功能,但是如果没有进行一番设计和思考,必然会写出一些糟糕的代码,其会对代码维护、性能、团队协作都会造成负面影响。

0 人点赞