為什麼PyMySQL獲取一條數據會讓記憶體爆炸
- 2020 年 3 月 13 日
- 筆記
當Python 有讀寫 MySQL 數據的需求時,我們經常使用PyMySQL這個第三方庫來完成。
有時候如果一張表裡面的數據非常大,但是我們只需要讀取一條數據,此時我們可能會想當然地使用cursor.fetchone()這個方法,以為這樣就真的可以只讀取一條數據:
import pymysql connection = pymysql.connect(host='localhost', user='user', password='passwd', db='db', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) with connection.cursor() as cursor: db = 'select * from users where age > 10' cursor.execute(db) one_user = cursor.fetchone()
但實際上,上面這段程式碼,與下面這段程式碼沒有任何區別:
... with connection.cursor() as cursor: sql = 'select * from users where age > 10' cursor.execute(sql) all_users = cursor.fetchall() one_user = all_users[0]
這是因為,當我們執行到cursor.execute(sql)的時候,PyMySQL就已經把表裡面所有的數據讀取到記憶體中了。而後面的cursor.fetchall()或者cursor.fetchone()只不過是從記憶體中返回全部數據還是返回1條數據而已。
我們來看PyMySQL的源程式碼[1]。在cursor.execute()方法程式碼如下圖所示:

其中第163行調用了self._query方法。我們再去到這個方法裡面:

看到程式碼第322行,調用了self._do_get_result()方法。我們再去這個方法裡面看看:

注意程式碼第342行,此時已經把所有數據存放到了self._rows列表中。
現在我們來看cursor.fetchone()方法:

可以看到,這裡不過是從列表裡面根據下標讀取一條數據出來而已。
再看cursor.fetchall()方法:

如果之前先多次調用過cursor.fetchone(),那麼self.rownumber會持續增加。而調用cursor.fetchall()時,跳過之前已經返回過的數據,直接返回剩下的全部數據即可。如果之前沒有調用過cursor.fetchone(),那麼直接返回全部數據。
所以,單純使用cursor.fetchone()並不能節省記憶體,如果表裡面的數據非常大,還是會有記憶體爆炸的危險。
那麼真正的解決辦法是什麼呢?真正的解決辦法在創建資料庫連接的時候指定游標類型。pymysql.connect有一個參數叫做cursorclass,把它的值設定為pymysql.SSDictCursor即可解決問題。
我們來看一下如何正確使用它:
import pymysql connection = pymysql.connect(host='localhost', user='user', password='passwd', db='db', charset='utf8mb4', cursorclass=pymysql.cursors.SSDictCursor) with connection.cursor() as cursor: db = 'select * from users where age > 10' cursor.execute(db) for row in cursor: print('對 cursor 直接進行迭代,每循環一次,從資料庫讀取一條數據。不會提前把所有數據讀取到記憶體中。') print(row['name'])
參考資料
[1]源程式碼: https://github.com/PyMySQL/PyMySQL/blob/master/pymysql/cursors.py


