通过sqlalchemy重复插入sqlite数据库会导致内存泄漏吗?

时间:2019-06-02 20:43:52

标签: python pandas sqlite memory-leaks sqlalchemy

当通过sqlalchemy和pandas to_sql和指定的chucksize将巨大的pandas数据帧插入sqlite时,会出现内存错误。

起初,我认为这是to_sql的问题,但是我尝试了一种变通方法,其中我不使用块大小而是使用for i in range(100): df.iloc[i * 100000:(i+1):100000].to_sql(...),但仍然导致错误。

在某些情况下,似乎存在内存泄漏,并且通过sqlalchemy重复插入sqlite。

通过一个最小的示例,我很难尝试复制在转换数据时发生的内存泄漏。但这非常接近。

import string
import numpy as np
import pandas as pd
from random import randint
import random

def make_random_str_array(size=10, num_rows=100, chars=string.ascii_uppercase + string.digits):
    return (np.random.choice(list(chars), num_rows*size)
            .view('|U{}'.format(size)))

def alt(size, num_rows):
    data = make_random_str_array(size, num_rows=2*num_rows).reshape(-1, 2)
    dfAll = pd.DataFrame(data)
    return dfAll

dfAll = alt(randint(1000, 2000), 10000)

for i in range(330):
    print('step ', i)
    data = alt(randint(1000, 2000), 10000)
    df = pd.DataFrame(data)
    dfAll = pd.concat([ df,  dfAll ])

import sqlalchemy

from sqlalchemy import create_engine
engine = sqlalchemy.create_engine('sqlite:///testtt.db')

for i in range(500):
    print('step', i)
    dfAll.iloc[(i%330)*10000:((i%330)+1)*10000].to_sql('test_table22', engine, index = False, if_exists= 'append')

这是在Google Colab CPU环境上运行的。

数据库本身并不会引起内存泄漏,因为我可以重新启动环境,并且先前插入的数据仍然存在,并且连接到该数据库不会导致内存增加。问题似乎是在某些情况下,通过循环to_sql或指定了chucksize的一个to_sql重复插入。

有没有一种方法可以运行此代码而不会导致内存使用量的最终增加?

编辑:

要完全重现错误,请运行此笔记本

https://drive.google.com/open?id=1ZijvI1jU66xOHkcmERO4wMwe-9HpT5OS

笔记本需要您将此文件夹导入Google云端硬盘的主目录

https://drive.google.com/open?id=1m6JfoIEIcX74CFSIQArZmSd0A8d0IRG8

笔记本还将安装您的Google驱动器,您需要授予其访问Google驱动器的权限。由于数据托管在我的Google驱动器上,因此导入数据不会占用您分配的任何数据。

1 个答案:

答案 0 :(得分:5)

Google Colab实例从大约12.72GB的可用RAM开始。 创建数据帧theBigList后,已使用约9.99GB的RAM。 这已经是一种相当不舒服的情况,因为对于 熊猫操作需要与其所操作的DataFrame一样多的额外空间。 因此,我们应该努力避免使用尽可能多的RAM,幸运的是,有一种简单的方法可以做到这一点:只需加载每个.npy文件,并将其数据一次存储在sqlite数据库中,一次曾经创建theBigList (请参见下文)。

但是,如果我们使用您发布的代码,则可以看到RAM使用率缓慢增加 theBigList的大块迭代地存储在数据库中。

theBigList DataFrame将字符串存储在NumPy数组中。但是在过程中 将字符串传输到sqlite数据库的过程中,NumPy字符串是 转换成Python字符串。这会占用更多内存。

this Theano tutoral讨论了Python内部内存管理,

  

为了加快内存分配(和重用)的速度,Python使用了许多列表   小物件。每个列表将包含大小相似的对象:   列出对象的大小,范围为1到8个字节,一个为9到16个字节,以此类推。   需要创建,或者我们重复使用列表中的空闲块,或者分配一个   新的。

     

...重要的是这些列表永远不会缩小。

     

确实:如果某项(尺寸为x)被释放(由于缺乏参考而释放),则其   位置不会返回到Python的全局内存池(甚至更少)   系统),但仅标记为免费,并添加到大小免费的商品列表中   X。如果另一个兼容的对象,则该死对象的位置将被重用   需要尺寸。如果没有可用的失效对象,则会创建新的失效对象。

     

如果从不释放小对象的内存,那么不可避免的结论是,   像金鱼一样,这些小的对象列表只会持续增长,不会缩小,   并且应用程序的内存占用量最大   在任何给定点分配的小对象的数量。

我相信这可以准确地描述您在执行此循环时看到的行为:

for i in range(0, 588):
    theBigList.iloc[i*10000:(i+1)*10000].to_sql(
        'CS_table', engine, index=False, if_exists='append')

即使许多死对象的位置都被重新用于新字符串, 对于本质上随机的字符串(例如theBigList中的字符串)来说,这是不令人难以置信的,多余的空间有时会 需要,因此内存占用量不断增长。

该过程最终达到Google Colab的12.72GB RAM限制,并且内核因内存错误而被终止。


在这种情况下,避免使用大量内存的最简单方法是永远不要实例化整个DataFrame-而是一次加载和处理DataFrame的一小块:

import numpy as np
import pandas as pd
import matplotlib.cbook as mc
import sqlalchemy as SA

def load_and_store(dbpath):
    engine = SA.create_engine("sqlite:///{}".format(dbpath))    
    for i in range(0, 47):
        print('step {}: {}'.format(i, mc.report_memory()))                
        for letter in list('ABCDEF'):
            path = '/content/gdrive/My Drive/SummarizationTempData/CS2Part{}{:02}.npy'.format(letter, i)
            comb = np.load(path, allow_pickle=True)
            toPD = pd.DataFrame(comb).drop([0, 2, 3], 1).astype(str)
            toPD.columns = ['title', 'abstract']
            toPD = toPD.loc[toPD['abstract'] != '']
            toPD.to_sql('CS_table', engine, index=False, if_exists='append')

dbpath = '/content/gdrive/My Drive/dbfile/CSSummaries.db'
load_and_store(dbpath)

可打印

step 0: 132545
step 1: 176983
step 2: 178967
step 3: 181527
...         
step 43: 190551
step 44: 190423
step 45: 190103
step 46: 190551

每行的最后一个数字是该进程报告的进程消耗的内存量 matplotlib.cbook.report_memory。有许多不同的内存使用量度。在Linux上,mc.report_memory()正在报告 the size of the physical pages of the core image的流程(包括文本,数据和堆栈空间)。


顺便说一句,您可以使用的另一个管理内存的基本技巧是使用函数。 函数终止时,将释放函数内部的局部变量。 这样可以减轻您手动调用delgc.collect()的负担。