前文
同事反馈执行下载sql时返回500异常,上日志查看报了MemoryError,报错如下图:
赶紧查了下系统的内存可用,发现只有200M可用,当下载行数超过5w行就GG了,查看报错发现在cursor.fetchall()出错,google了才知道cursor.fetchall()和cursor.fetchone()都是先从数据库读取到内存中,然后再返回,结果就造成MemoryError。
解决cursor.fetchall()
首先解释下fetchall()取得数据是为了什么,实际上一开始只是为了取affected rows,也就是执行数据库所影响的行数,因为这个需求是先取得影响行数,然后跑工单,审批通过后才能下载。一开始也没想到后面需要下载几万条或者以上的数据,所以并没有针对取影响行数进行优化,另一方面python连接数据库用的是mysql.connector,这个是mysql官方推介的连接python库,结果并没有能直接获取affected rows的方法,查了官网,解释如下:
标红框的说明如果不进行fetch操作,则无法获取rowcount,这个就比较尴尬了,而且网上普遍对pymysql的支持偏多,并且pymysql提供了affected_rows=cursor.execute(sql)就能返回影响行数,简直不要太友好,最后重写了mysql.connector的类,当然报了一系列错,不过跟着提示都能完成,这边就提个,mysql的连接游标分为两种形式,默认是取出的数据为元祖,另一种是取出的数据为字典,那一般为了项目取数据方便,用的是字典的方式,下面是mysql.connector和pymysql的写法:
pymysql:
conn.cursor(cursor=pymysql.cursors.DictCursor)
mysql.connector:
conn.cursor(dictionary=True)
ok,affected rows用execute就能拿到,就避免了fetchall来取得,但是真正要下载的时候还是绕不过去。查找网上的解决方法,普遍推介用流式查询避免MemoryError,那基本代码如下:
import pymysql
#config 即是host、port、user、pass、database
conn = pymysql.connect(**config)
cur = conn.cursor(cursor=pymysql.cursors.DictCursor)
cur.execute("sql");
row = cur.fetchone()
while row is not None:
do something...
row = cur.fetchone()
cur.close()
conn.close(
标准代码如上,那流式查询的好处是:
非流式查询:内存会随着查询记录的增长而近乎直线增长。
流式查询:内存会保持稳定,不会随着记录的增长而增长。其内存大小取决于批处理大小
最后我也是用了该查询来完成下载,但是因为下载本身用到了StreamingHttpResponse和yield来搭配下载,导致上述代码中的do something很不好处理,不过万幸还是处理好了,这个待下次分享用python来下载数据库的数据并转化成txt、csv、excel时再一并分享,这边就暂时说到这,谢谢观看。