记项目Python-MySQL访问类的优化

背景

接手一些Python项目的后续开发与维护,发现这些项目都用同一个数据库访问类,而生成的结果行竟然是用list存的,一个简单的row[‘id’]访问需要遍历整行去找,遂优化之

改写成dict

一般访问数据行的字段都是使用字段名访问,显然应该使用dict。但也有可能使用数字下标访问,例如对于select count(*)的结果集,可能就使用rs[0][0]访问了。因此还需要一个list去存结果集的字段名顺序。

其实MySQLdb Driver有DictCursor可直接获得dict结果。当结果集的列有重复字段,第二个重名字段会加上表别名前缀。例如select * FROM tableA as A, tableB as B,两个表都有字段id,tableB的id会使用B.id这个key。
对于可以用下标访问的需求,也需要额外加个list来存字段名顺序。这个list是从cursor的description生成的。问题在于cursor的description里,tableB的id的字段名仍然是id,当想用下标访问tableB的id时,查找list获得id这个字段名,得到的却是tableA的结果。暂时没有想到解决这个问题的方法。因此还是使用默认cursor,手动生成结果集。

结果测试

改写成dict后,测试耗时只有原来的50%。对于列很多的结果集,从原来遍历整行O(n)才拿到结果,变成O(1),改善还是很可观的。

使用Cython进一步加速

在纯Python的基础上,已经很难再有提高了。考虑使用Cython将其编译成C extension。对于数据库访问类这种“底层”的类,平时很少会改动,但使用非常频繁,进一步优化还是值得的。

对于能确定静态类型的地方,例如for循环中的i,自定义的Python class,都用cdef指定类型。对于参数或者返回值中有能确定静态类型的Python函数,也可以使用cpdef修饰。

编译后测试,耗时只有改成dict后版本的1/7左右,是原始未优化版本的6%左右,速度提升有15倍!

项目实测

以上的测试结果只是纯跑分的测试,实际效果还是要放到本来的项目体现。找了一个报表的调用,数据库查询结果是5k行。

$ time -p curl -s http://app08.gz/fd/data/pilfered_time_txt/?btime=2011-07-04\&etime=2011-07-11\&product=mh\&get_state= | wc -l
5737
real 4.48
user 0.01
sys 0.22
$ time -p curl -s http://localhost:8004/fd/data/pilfered_time_txt/?btime=2011-07-04\&etime=2011-07-11\&product=mh\&get_state= | wc -l
5737
real 2.42
user 0.02
sys 0.13

上面的是未优化版本的结果
下面的是使用c extension版本的结果,大约有45%的提升。

PS:
在import cython生成的so时,曾经遇到ImportError

>>> import db
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: dynamic module does not define init function (initdb)

解决方法是注意生成cython扩展的setup.py,ext_modules里面的Extension的第一个参数必须是你将来要import的名字。例如我要import db,那么就要写成:

ext_modules = [Extension("db", ["db.pyx"])]

对于生成的so,可以使用

strings db.so | grep db

来查看是否正确生成了initdb这个入口。