web-dev-qa-db-ja.com

pyODBCのfast_executemanyを使用してpandas.DataFrame.to_sqlを高速化する

MS SQLを実行しているリモートサーバーに大きなpandas.DataFrameを送信したいと思います。私が今やる方法は、data_frameオブジェクトをタプルのリストに変換し、pyODBCのexecutemany()関数で送信することです。次のようになります。

 import pyodbc as pdb

 list_of_tuples = convert_df(data_frame)

 connection = pdb.connect(cnxn_str)

 cursor = connection.cursor()
 cursor.fast_executemany = True
 cursor.executemany(sql_statement, list_of_tuples)
 connection.commit()

 cursor.close()
 connection.close()

それから、data_frame.to_sql()メソッドを使用することで、物事を高速化できるか(少なくとももっと読みやすいか)疑問に思い始めました。私は次の解決策を思いつきました:

 import sqlalchemy as sa

 engine = sa.create_engine("mssql+pyodbc:///?odbc_connect=%s" % cnxn_str)
 data_frame.to_sql(table_name, engine, index=False)

これでコードは読みやすくなりましたが、アップロードは少なくとも150倍遅い ...です。

SQLAlchemyを使用するときにfast_executemanyを反転する方法はありますか?

私はpandas-0.20.3、pyODBC-4.0.21およびsqlalchemy-1.1.13を使用しています。

34
J.K.

SQLAlchemyの開発者に連絡した後、この問題を解決する方法が登場しました。すばらしい仕事に感謝します!

カーソル実行イベントを使用して、executemanyフラグが立てられているかどうかを確認する必要があります。その場合は、fast_executemanyオプションをオンにしてください。例えば:

from sqlalchemy import event

@event.listens_for(engine, 'before_cursor_execute')
def receive_before_cursor_execute(conn, cursor, statement, params, context, executemany):
    if executemany:
        cursor.fast_executemany = True

実行イベントの詳細については、 こちら をご覧ください。


UPDATE:pyodbcfast_executemanyのサポートが SQLAlchemy 1.3. に追加されたため、ハックはもはや必要ありません。

19
J.K.

EDIT(2019-03-08):Gord Thompsonは、sqlalchemyの更新ログからの良いニュースを以下にコメントしました:SQLAlchemy以降1.3.0、リリース2019-03-04、sqlalchemyはmssql+pyodbc方言のengine = create_engine(sqlalchemy_url, fast_executemany=True)をサポートするようになりました。つまり、関数を定義して@event.listens_for(engine, 'before_cursor_execute')を使用する必要がなくなりました。以下の関数は削除でき、フラグのみをcreate_engineステートメントで設定する必要があります。 。

元の投稿:

これを投稿するためのアカウントを作成しました。既に提供された回答のフォローアップであるため、上記のスレッドの下にコメントしたかったです。上記のソリューションは、Ubuntuベースのインストールから書き込みを行うMicrosft SQLストレージ上のバージョン17 SQLドライバーで機能しました。

物事を大幅に高速化するために使用した完全なコード(100倍以上の高速化)は以下のとおりです。これは、関連する詳細で接続文字列を変更することを条件に、ターンキースニペットです。上記のポスターに、私はすでにかなりの時間を探していたので、解決策に感謝します。

import pandas as pd
import numpy as np
import time
from sqlalchemy import create_engine, event
from urllib.parse import quote_plus


conn =  "DRIVER={ODBC Driver 17 for SQL Server};SERVER=IP_ADDRESS;DATABASE=DataLake;UID=USER;PWD=PASS"
quoted = quote_plus(conn)
new_con = 'mssql+pyodbc:///?odbc_connect={}'.format(quoted)
engine = create_engine(new_con)


@event.listens_for(engine, 'before_cursor_execute')
def receive_before_cursor_execute(conn, cursor, statement, params, context, executemany):
    print("FUNC call")
    if executemany:
        cursor.fast_executemany = True


table_name = 'fast_executemany_test'
df = pd.DataFrame(np.random.random((10**4, 100)))


s = time.time()
df.to_sql(table_name, engine, if_exists = 'replace', chunksize = None)
print(time.time() - s)

以下のコメントに基づいて、pandas to_sql実装とクエリの処理方法に関する制限を説明するために、少し時間をかけたいと思いました。 MemoryErrorが引き起こされる原因となる可能性がある2つのことがあります。

1)リモートSQLストレージに書き込んでいると仮定します。 to_sqlメソッドを使用して大きなpandas DataFrameを書き込もうとすると、データフレーム全体が値のリストに変換されます。この変換は、元のDataFrameよりも多くのRAMを使用します(その上に、古いDataFrameがまだRAMに残っているため)。このリストは、ODBCコネクタの最後のexecutemany呼び出しに提供されます。 ODBCコネクタには、このような大きなクエリの処理に問題があると思います。これを解決する方法は、to_sqlメソッドにチャンクサイズ引数を提供することです(10 ** 5は、Azureの2 CPU 7GB ram MSSQLストレージアプリケーションで約600メガビット/秒(!)の書き込み速度を与える最適なサイズのようです) -Azureをお勧めできませんbtw)。したがって、クエリサイズである最初の制限は、chunksize引数を指定することで回避できます。ただし、これにより、10 ** 7以上のサイズのデータ​​フレーム(少なくとも作業中のVMで〜55 GBのRAMを含む)を作成できなくなり、nr 2が発行されます。

これは、np.split(10 ** 6サイズのDataFrameチャンク)でDataFrameを分割することで回避できます。これらは反復的に書き出すことができます。 pandas自体のコアにto_sqlメソッドの準備ができているときにプルリクエストを作成しようとするため、毎回事前に分割する必要はありません。とにかく、次のような(ターンキーではない)関数を書くことになりました。

import pandas as pd
import numpy as np

def write_df_to_sql(df, **kwargs):
    chunks = np.split(df, df.shape()[0] / 10**6)
    for chunk in chunks:
        chunk.to_sql(**kwargs)
    return True

上記のスニペットのより完全な例は、ここで見ることができます: https://gitlab.com/timelord/timelord/blob/master/timelord/utils/connector.py

これは、パッチを組み込み、SQLとの接続のセットアップに伴う必要なオーバーヘッドの一部を軽減する、私が書いたクラスです。まだいくつかのドキュメントを書く必要があります。また、pandas自体にパッチを提供することを計画していましたが、その方法についてはまだ良い方法を見つけていません。

これがお役に立てば幸いです。

45
hetspookjee

新しいturbodbcライブラリを使用できる人のための追加の高性能オプションとして、この完全な例を投稿したかっただけです。 http://turbodbc.readthedocs.io/en/latest/

pandas .to_sql()の間に流動的な多くのオプションがあり、sqlalchemyを介してfast_executemanyをトリガーし、pyodbcをtuples/lists/etcで直接使用するか、フラットファイルでBULK UPLOADを試みます。

うまくいけば、現在のpandasプロジェクトで機能が進化したり、将来のturbodbc統合のようなものが含まれたりするので、次のことが少し楽しくなるかもしれません。

import pandas as pd
import numpy as np
from turbodbc import connect, make_options
from io import StringIO

test_data = '''id,transaction_dt,units,measures
               1,2018-01-01,4,30.5
               1,2018-01-03,4,26.3
               2,2018-01-01,3,12.7
               2,2018-01-03,3,8.8'''

df_test = pd.read_csv(StringIO(test_data), sep=',')
df_test['transaction_dt'] = pd.to_datetime(df_test['transaction_dt'])

options = make_options(parameter_sets_to_buffer=1000)
conn = connect(driver='{SQL Server}', server='server_nm', database='db_nm', turbodbc_options=options)

test_query = '''DROP TABLE IF EXISTS [db_name].[schema].[test]

                CREATE TABLE [db_name].[schema].[test]
                (
                    id int NULL,
                    transaction_dt datetime NULL,
                    units int NULL,
                    measures float NULL
                )

                INSERT INTO [db_name].[schema].[test] (id,transaction_dt,units,measures)
                VALUES (?,?,?,?) '''

cursor.executemanycolumns(test_query, [df_test['id'].values, df_test['transaction_dt'].values, df_test['units'].values, df_test['measures'].values]

turbodbcは、多くのユースケースで非常に高速になります(特にnumpy配列の場合)。基になるnumpy配列をデータフレーム列からパラメーターとしてクエリに直接渡すことがいかに簡単かを観察してください。また、これはメモリ消費を過度に増加させる中間オブジェクトの作成を防ぐのに役立つと考えています。これが役立つことを願っています!

9
Pylander

同じ問題に遭遇しましたが、PostgreSQLを使用していました。彼らは今リリースしますpandasバージョン0.24.そして、私の問題を解決したmethodと呼ばれるto_sql関数に新しいパラメーターがあります。

from sqlalchemy import create_engine

engine = create_engine(your_options)
data_frame.to_sql(table_name, engine, method="multi")

アップロード速度は100倍高速です。また、大量のデータを送信する場合は、chunksizeパラメーターを設定することをお勧めします。

9
Emmanuel

Pandas 0.23.0および0.24.0 複数値の挿入を使用 PyODBCを使用しているため、高速executemanyの助けになりません。単一のINSERT ... VALUES ...ステートメントがチャンクごとに発行されます。複数値の挿入チャンクは、古い低速のexecutemanyのデフォルトよりも改善されていますが、少なくとも単純なテストでは、複数の値の挿入に必要な手動のchunksize計算の必要性は言うまでもありませんが、高速のexecutemanyメソッドが依然として有効です。将来構成オプションが提供されない場合は、monkeypatchingによって古い動作を強制できます。

import pandas.io.sql

def insert_statement(self, data, conn):
    return self.table.insert(), data

pandas.io.sql.SQLTable.insert_statement = insert_statement

将来はここにあり、少なくともmasterブランチでは、 to_sql() のキーワード引数method=を使用して挿入メソッドを制御できます。デフォルトはNoneで、executemanyメソッドを強制します。 method='multi'を渡すと、複数値の挿入が使用されます。 Postgresql COPYなど、DBMS固有のアプローチの実装にも使用できます。

4
Ilja Everilä

@Pylanderが指摘したように

Turbodbcは、データの取り込みに最適です。

とても興奮して、githubとmediumで「ブログ」を書きました: https://medium.com/@erickfis/etl-process-with-turbodbc-1d19ed71510e を確認してください

作業例とpandas.to_sqlとの比較

簡単に言えば、

turbodbcを使用すると、3秒で10000行(77列)が得られます

pandas.to_sqlを使用すると、198秒で同じ10000行(77列)が得られます...

そして、ここに私が詳細にやっていることがあります

インポート:

import sqlalchemy
import pandas as pd
import numpy as np
import turbodbc
import time

いくつかのデータをロードして処理します-私のsample.pklをあなたのものに置き換えてください:

df = pd.read_pickle('sample.pkl')

df.columns = df.columns.str.strip()  # remove white spaces around column names
df = df.applymap(str.strip) # remove white spaces around values
df = df.replace('', np.nan)  # map nans, to drop NAs rows and columns later
df = df.dropna(how='all', axis=0)  # remove rows containing only NAs
df = df.dropna(how='all', axis=1)  # remove columns containing only NAs
df = df.replace(np.nan, 'NA')  # turbodbc hates null values...

SqlAlchemyを使用してテーブルを作成する

残念ながら、turbodbcには、テーブルの作成とデータの挿入のために、多くのSQLの手作業による多大なオーバーヘッドが必要です。

幸いなことに、Pythonは純粋な喜びであり、SQLコードを記述するこのプロセスを自動化できます。

最初のステップは、データを受け取るテーブルを作成することです。ただし、テーブルに複数の列がある場合、SQLコードを手動で作成してテーブルを作成すると問題が発生する可能性があります。私の場合、非常に多くの場合、テーブルには240列があります!

SqlAlchemyとpandasはまだ役立ちます:pandasは、多数の行(この例では10000)を書き込むのに適していますが、6行だけで、テーブル?このようにして、テーブルを作成するプロセスを自動化します。

SqlAlchemy接続を作成します。

mydb = 'someDB'

def make_con(db):
    """Connect to a specified db."""
    database_connection = sqlalchemy.create_engine(
        'mssql+pymssql://{0}:{1}@{2}/{3}'.format(
            myuser, mypassword,
            myhost, db
            )
        )
    return database_connection

pd_connection = make_con(mydb)

SQL Serverでテーブルを作成する

pandas + sqlAlchemyを使用しますが、前述のturbodbc用のスペースを準備するためだけです。ここでdf.head()に注意してください。データの6行のみを挿入するためにpandas + sqlAlchemyを使用しています。これは非常に高速に実行され、テーブルの作成を自動化するために行われています。

table = 'testing'
df.head().to_sql(table, con=pd_connection, index=False)

テーブルがすでに配置されているので、ここで真剣に話しましょう。

Turbodbc接続:

def turbo_conn(mydb):
    """Connect to a specified db - turbo."""
    database_connection = turbodbc.connect(
                                            driver='ODBC Driver 17 for SQL Server',
                                            server=myhost,
                                            database=mydb,
                                            uid=myuser,
                                            pwd=mypassword
                                        )
    return database_connection

Turbodbc用のSQLコマンドとデータの準備。創造的であるこのコード作成を自動化しましょう:

def turbo_write(mydb, df, table):
    """Use turbodbc to insert data into sql."""
    start = time.time()
    # preparing columns
    colunas = '('
    colunas += ', '.join(df.columns)
    colunas += ')'

    # preparing value place holders
    val_place_holder = ['?' for col in df.columns]
    sql_val = '('
    sql_val += ', '.join(val_place_holder)
    sql_val += ')'

    # writing sql query for turbodbc
    sql = f"""
    INSERT INTO {mydb}.dbo.{table} {colunas}
    VALUES {sql_val}
    """

    # writing array of values for turbodbc
    valores_df = [df[col].values for col in df.columns]

    # cleans the previous head insert
    with connection.cursor() as cursor:
        cursor.execute(f"delete from {mydb}.dbo.{table}")
        connection.commit()

    # inserts data, for real
    with connection.cursor() as cursor:
        try:
            cursor.executemanycolumns(sql, valores_df)
            connection.commit()
        except Exception:
            connection.rollback()
            print('something went wrong')

    stop = time.time() - start
    return print(f'finished in {stop} seconds')

Turbodbcを使用してデータを書き込む-3秒で10000行(77列)を取得しました。

turbo_write(mydb, df.sample(10000), table)

パンダ方式の比較-198秒で同じ10000行(77列)を取得しました…

table = 'pd_testing'

def pandas_comparisson(df, table):
    """Load data using pandas."""
    start = time.time()
    df.to_sql(table, con=pd_connection, index=False)
    stop = time.time() - start
    return print(f'finished in {stop} seconds')

pandas_comparisson(df.sample(10000), table)

環境と条件

Python 3.6.7 :: Anaconda, Inc.
TURBODBC version ‘3.0.0’
sqlAlchemy version ‘1.2.12’
pandas version ‘0.23.4’
Microsoft SQL Server 2014
user with bulk operations privileges

このコードの更新については、 https://erickfis.github.io/loose-code/ を確認してください!

3
erickfis

SQL Server INSERTのパフォーマンス:pyodbcとturbodbc

to_sqlを使用してpandas DataFrameをSQL Serverにアップロードすると、turbodbcはfast_executemanyなしのpyodbcよりも確実に高速になります。ただし、fast_executemanyをpyodbcに対して有効にすると、どちらのアプローチでも基本的に同じパフォーマンスが得られます。

テスト環境:

[venv1_pyodbc]
pyodbc 2.0.25

[venv2_turbodbc]
turbodbc 3.0.0
sqlalchemy-turbodbc 0.1.0

[両方に共通]
Windows上のPython 3.6.4 64ビット
SQLAlchemy 1.3.0b1
パンダ0.23.4
numpy 1.15.4

テストコード:

# for pyodbc
engine = create_engine('mssql+pyodbc://sa:whatever@SQL_panorama', fast_executemany=True)
# for turbodbc
# engine = create_engine('mssql+turbodbc://sa:whatever@SQL_panorama')

# test data
num_rows = 10000
num_cols = 100
df = pd.DataFrame(
    [[f'row{x:04}col{y:03}' for y in range(num_cols)] for x in range(num_rows)],
    columns=[f'col{y:03}' for y in range(num_cols)]
)

t0 = time.time()
df.to_sql("sqlalchemy_test", engine, if_exists='replace', index=None)
print(f"pandas wrote {num_rows} rows in {(time.time() - t0):0.1f} seconds")

テストは、各環境で12回実行され、それぞれの単一のベストタイムとワーストタイムは破棄されました。結果(秒単位):

   rank  pyodbc  turbodbc
   ----  ------  --------
      1    22.8      27.5
      2    23.4      28.1
      3    24.6      28.2
      4    25.2      28.5
      5    25.7      29.3
      6    26.9      29.9
      7    27.0      31.4
      8    30.1      32.1
      9    33.6      32.5
     10    39.8      32.9
   ----  ------  --------
average    27.9      30.0
3
Gord Thompson

@ J.K。の回答に追加したかっただけです。

このアプローチを使用している場合:

@event.listens_for(engine, 'before_cursor_execute')
def receive_before_cursor_execute(conn, cursor, statement, params, context, executemany):
    if executemany:
        cursor.fast_executemany = True

そして、あなたはこのエラーを受け取っています:

"sqlalchemy.exc.DBAPIError:(pyodbc.Error)( 'HY010'、 '[HY010] [Microsoft] [SQL Server Native Client 11.0]関数シーケンスエラー(0)(SQLParamData)')[SQL: 'INSERT INTO .. 。(...)VALUES(?、?) '] [パラメータ:((...、...)、(...、...)](このエラーの背景: http ://sqlalche.me/e/dbapi ) "

次のように文字列値をエンコードします:'yourStringValue'.encode('ascii')

これで問題が解決します。

2