為什麼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