使用SqlAlchemy和cx_Oracle将Pandas DataFrame写入Oracle数据库时加速to_sql()

时间:2017-03-10 21:28:06

标签: oracle performance pandas dataframe sqlalchemy

使用pandas dataframe的to_sql方法,我可以很容易地在oracle数据库中的表中写入少量行:

from sqlalchemy import create_engine
import cx_Oracle
dsn_tns = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=<host>)(PORT=1521))\
       (CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=<servicename>)))"
pwd = input('Please type in password:')
engine = create_engine('oracle+cx_oracle://myusername:' + pwd + '@%s' % dsn_tns)
df.to_sql('test_table', engine.connect(), if_exists='replace')

但是对于任何常规大小的数据帧(我的有60k行,不是那么大),代码变得无法使用,因为它在我愿意等待(绝对超过10分钟)的时间内从未完成。我已经搜索了很多次搜索和搜索,最接近的解决方案是ansonwthis question中给出的答案。但那个是关于mysql,而不是oracle。正如Ziggy Eunicien指出的那样,它对甲骨文不起作用。有什么想法吗?

修改

这是数据框中的行样本:

id          name            premium     created_date    init_p  term_number uprate  value   score   group   action_reason
160442353   LDP: Review     1295.619617 2014-01-20  1130.75     1           7       -42 236.328243  6       pass
164623435   TRU: Referral   453.224880  2014-05-20  0.00        11          NaN     -55 38.783290   1       suppress

这里是df的数据类型:

id               int64
name             object
premium          float64
created_date     object
init_p           float64
term_number      float64
uprate           float64
value            float64
score            float64
group            int64
action_reason    object

4 个答案:

答案 0 :(得分:14)

Pandas + SQLAlchemy默认情况下将所有object(字符串)列保存为Oracle DB中的 CLOB ,这使得插入非常缓慢。

以下是一些测试:

import pandas as pd
import cx_Oracle
from sqlalchemy import types, create_engine

#######################################################
### DB connection strings config
#######################################################
tns = """
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = my-db-scan)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = my_service_name)
    )
  )
"""

usr = "test"
pwd = "my_oracle_password"

engine = create_engine('oracle+cx_oracle://%s:%s@%s' % (usr, pwd, tns))

# sample DF [shape: `(2000, 11)`]
# i took your 2 rows DF and replicated it: `df = pd.concat([df]* 10**3, ignore_index=True)`
df = pd.read_csv('/path/to/file.csv')

DF信息:

In [61]: df.shape
Out[61]: (2000, 11)

In [62]: df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 11 columns):
id               2000 non-null int64
name             2000 non-null object
premium          2000 non-null float64
created_date     2000 non-null datetime64[ns]
init_p           2000 non-null float64
term_number      2000 non-null int64
uprate           1000 non-null float64
value            2000 non-null int64
score            2000 non-null float64
group            2000 non-null int64
action_reason    2000 non-null object
dtypes: datetime64[ns](1), float64(4), int64(4), object(2)
memory usage: 172.0+ KB

让我们检查将它存储到Oracle DB需要多长时间:

In [57]: df.shape
Out[57]: (2000, 11)

In [58]: %timeit -n 1 -r 1 df.to_sql('test_table', engine, index=False, if_exists='replace')
1 loop, best of 1: 16 s per loop

在Oracle DB中(注意CLOB):

AAA> desc test.test_table
 Name                            Null?    Type
 ------------------------------- -------- ------------------
 ID                                       NUMBER(19)
 NAME                                     CLOB        #  !!!
 PREMIUM                                  FLOAT(126)
 CREATED_DATE                             DATE
 INIT_P                                   FLOAT(126)
 TERM_NUMBER                              NUMBER(19)
 UPRATE                                   FLOAT(126)
 VALUE                                    NUMBER(19)
 SCORE                                    FLOAT(126)
 group                                    NUMBER(19)
 ACTION_REASON                            CLOB        #  !!!

现在让我们指示pandas将所有object列保存为VARCHAR数据类型:

In [59]: dtyp = {c:types.VARCHAR(df[c].str.len().max())
    ...:         for c in df.columns[df.dtypes == 'object'].tolist()}
    ...:

In [60]: %timeit -n 1 -r 1 df.to_sql('test_table', engine, index=False, if_exists='replace', dtype=dtyp)
1 loop, best of 1: 335 ms per loop

这次是约。快了48倍

签入Oracle DB:

 AAA> desc test.test_table
 Name                          Null?    Type
 ----------------------------- -------- ---------------------
 ID                                     NUMBER(19)
 NAME                                   VARCHAR2(13 CHAR)        #  !!!
 PREMIUM                                FLOAT(126)
 CREATED_DATE                           DATE
 INIT_P                                 FLOAT(126)
 TERM_NUMBER                            NUMBER(19)
 UPRATE                                 FLOAT(126)
 VALUE                                  NUMBER(19)
 SCORE                                  FLOAT(126)
 group                                  NUMBER(19)
 ACTION_REASON                          VARCHAR2(8 CHAR)        #  !!!

让我们用200.000行DF测试它:

In [69]: df.shape
Out[69]: (200000, 11)

In [70]: %timeit -n 1 -r 1 df.to_sql('test_table', engine, index=False, if_exists='replace', dtype=dtyp, chunksize=10**4)
1 loop, best of 1: 4.68 s per loop

在我的测试(不是最快的)环境中,200K行DF需要大约5秒钟。

结论:在将DataFrames保存到Oracle DB时,使用以下技巧为dtype dtype的所有DF列显式指定object。否则它将被保存为CLOB数据类型,这需要特殊处理并使其非常慢

dtyp = {c:types.VARCHAR(df[c].str.len().max())
        for c in df.columns[df.dtypes == 'object'].tolist()}

df.to_sql(..., dtype=dtyp)

答案 1 :(得分:1)

您可以只使用 method='multi',这将提高您的数据插入速度。

您也可以根据需要调整块大小,具体取决于您的数据。

当我尝试编写一个能够将数据从 csv 文件/excel 加载到数据框中的谷歌云函数时发现了这一点,我想将该数据框保存到谷歌云 sql 中的 postgresql 数据库中。

如果您可以在数据框中创建与数据库表中类似的结构,那么这是一个非常方便的工具。

df.to_sql('table_name', con=engine, if_exists='append', index=False, chunksize=2000, method='multi')

答案 2 :(得分:0)

请在此处评论后代。我在python 3.6.8,pandas 1.1.3,sqlalchemy 1.3.20上。当我尝试从MaxU实施解决方案时,最初遇到错误:

raise ValueError(f"{col} ({my_type}) not a string")

老实说,我不知道为什么。经过几个小时的调试,这终于对我有用。就我而言,我试图从CSV读取并插入Oracle:

import cx_Oracle
import numpy as np
import pandas as pd
import sqlalchemy as sa
from sqlalchemy import create_engine

conn = create_engine('oracle://{}:{}@{}'.format(USERNAME, PASSWORD, DATABASE))

df = pd.read_csv(...)
object_columns = [c for c in df.columns[df.dtypes == 'object'].tolist()]
dtyp = {c:sa.types.VARCHAR(df[c].str.len().max()) for c in object_columns}

df.to_sql(..., dtype=dtyp)

说实话,我并没有做太大改变,所以不能100%知道为什么我得到了原始错误,只是在这里发帖以防它有所帮助。

答案 3 :(得分:0)

使用此功能:
此函数将采用数据帧名称和长度(可选)。 它将转换后的数据类型(对象类型)返回到 Varchar(length) 默认 lenght =250 (这个乐趣只处理对象类型)

dtest[, sequence := rep(seq_len(floor(.N/5)),length.out=.N), by = places]
dtest[sequence!=1,sequence:=NA]

调用方法示例:

def dtyp(df_name, length=250):
    cols = df_name.select_dtypes(include='object')
    dtyps = {col: VARCHAR2(length) for col in cols}
    return dtypsenter code here