迴圈查詢資料的效能問題及優化
阿新 • • 發佈:2018-12-22
寫在前言:糟糕的程式碼,對程式碼維護、效能、團隊協作都會造成負面影響,所以,先設計再實現,謀而後動。
這裡的迴圈查詢,指的是在一個for迴圈中,不斷訪問資料庫來查詢資料。在剛接手公司資料報表系統時,發現有很多每日報告跑的出奇的慢,通過一番診斷後,發現主要來自兩個方面的因素:一是需要對資料庫的某些欄位建立和優化索引,二是存在了很多糟糕的程式碼,這些程式碼在一個迴圈中不斷的訪問資料庫,查詢資料。本文接下來將摘取其中的三個例子來說明如何避免迴圈查詢帶來的效能問題,涉及常用的三種資料儲存:MySQL,MongoDB和Redis。
1. 使用IN查詢替換for迴圈
- 優化前程式碼(簡化版,以MySQL為例):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檔案。這種寫法,簡單明瞭,只要按照邏輯來寫就好了,然而隨著交易越來越多,這段程式碼會跑的越來越慢,即使建立了索引,但是卻無法避免每次的資料庫訪問開銷。
- 優化後代碼: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為例):
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。同樣的,隨著資料量的增加,這段程式碼會跑的越來越慢。
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通常用來做資料快取,降低資料庫的命中率,從而提供併發效能。然而,如果使用不當,你會發現雖然使用了快取,但是時間查詢效率並沒特別大的提升。 - 優化前程式碼(簡化版):redis_cli = get_redis()
for id in user_ids:
result = redis_cli.get('user_last_active_time:%d' % id)
這個程式碼本意是要查詢一組使用者的最近一次活躍時間,這些活躍時間都快取在Redis中,但是這個程式碼,如果user_ids的列表很長,就會發現這個快取查詢很慢,因為每次訪問redis都需要建立一次IO請求。
-- 優化後代碼:
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來一次獲取所有的資料,這麼做會比上面的快幾十倍,在資料量大的情況下。
上面通過三個例項來闡述迴圈查詢對效能的影響和優化的方法,寫這篇部落格的目的並不僅僅要介紹這些技巧方法,因為技巧方法遠不止這些,而是想借此傳達一個觀點:程式設計,應該設計先於寫程式碼。雖然都是實現同樣的邏輯功能,但是如果沒有進行一番設計和思考,必然會寫出一些糟糕的程式碼,其會對程式碼維護、效能、團隊協作都會造成負面影響。