web-dev-qa-db-fra.com

Comment accélérer l'insertion en masse sur MS SQL Server à partir de CSV en utilisant pyodbc

Voici mon code avec lequel j'aimerais de l'aide. Je dois l'exécuter sur 1 300 000 lignes, ce qui signifie qu'il faut jusqu'à 40 minutes pour insérer ~ 300 000 lignes.

Je pense que l'insertion en vrac est la voie à suivre pour l'accélérer? Ou est-ce parce que j'itère sur les lignes via for data in reader: portion?

#Opens the prepped csv file
with open (os.path.join(newpath,outfile), 'r') as f:
    #hooks csv reader to file
    reader = csv.reader(f)
    #pulls out the columns (which match the SQL table)
    columns = next(reader)
    #trims any extra spaces
    columns = [x.strip(' ') for x in columns]
    #starts SQL statement
    query = 'bulk insert into SpikeData123({0}) values ({1})'
    #puts column names in SQL query 'query'
    query = query.format(','.join(columns), ','.join('?' * len(columns)))

    print 'Query is: %s' % query
    #starts curser from cnxn (which works)
    cursor = cnxn.cursor()
    #uploads everything by row
    for data in reader:
        cursor.execute(query, data)
        cursor.commit()

Je sélectionne dynamiquement mes en-têtes de colonne exprès (car je voudrais créer le code le plus Pythonic possible).

SpikeData123 est le nom de la table.

26
TangoAlee

Mise à jour: comme indiqué dans le commentaire de @SimonLang, BULK INSERT sous SQL Server 2017 et versions ultérieures prennent apparemment en charge les qualificateurs de texte dans les fichiers CSV (ref: ici ).


BULK INSERT sera presque certainement beaucoup plus rapide que de lire le fichier source ligne par ligne et de faire un INSERT régulier pour chaque ligne. Cependant, BULK INSERT et BCP ont une limitation importante concernant les fichiers CSV en ce qu'ils ne peuvent pas gérer les qualificateurs de texte (ref: ici ). Autrement dit, si votre fichier CSV ne contient pas de chaînes de texte qualifiées ...

1,Gord Thompson,2015-04-15
2,Bob Loblaw,2015-04-07

... alors vous pouvez l'insérer en bloc, mais s'il contient des qualificatifs de texte (car certaines valeurs de texte contiennent des virgules) ...

1,"Thompson, Gord",2015-04-15
2,"Loblaw, Bob",2015-04-07

... alors BULK INSERT ne peut pas le gérer. Pourtant, il pourrait être plus rapide dans l'ensemble de prétraiter un tel fichier CSV dans un fichier délimité par des tuyaux ...

1|Thompson, Gord|2015-04-15
2|Loblaw, Bob|2015-04-07

... ou un fichier délimité par des tabulations (où représente le caractère de tabulation) ...

1→Thompson, Gord→2015-04-15
2→Loblaw, Bob→2015-04-07

... et ensuite BULK INSERT ce fichier. Pour ce dernier fichier (délimité par des tabulations), le code BULK INSERT ressemblerait à ceci:

import pypyodbc
conn_str = "DSN=myDb_SQLEXPRESS;"
cnxn = pypyodbc.connect(conn_str)
crsr = cnxn.cursor()
sql = """
BULK INSERT myDb.dbo.SpikeData123
FROM 'C:\\__tmp\\biTest.txt' WITH (
    FIELDTERMINATOR='\\t',
    ROWTERMINATOR='\\n'
    );
"""
crsr.execute(sql)
cnxn.commit()
crsr.close()
cnxn.close()

Remarque: Comme mentionné dans un commentaire, l'exécution d'un BULK INSERT n'est applicable que si l'instance SQL Server peut lire directement le fichier source. Pour les cas où le fichier source se trouve sur un client distant, voir cette réponse .

33
Gord Thompson

Comme indiqué dans un commentaire à une autre réponse, la commande T-SQL BULK INSERT Ne fonctionnera que si le fichier à importer se trouve sur la même machine que l'instance SQL Server ou se trouve dans un emplacement réseau SMB/CIFS que le L'instance SQL Server peut lire. Il peut donc ne pas être applicable dans le cas où le fichier source se trouve sur un client distant.

pyodbc 4.0.19 a ajouté une fonctionnalité Cursor # fast_executemany qui peut être utile dans ce cas. fast_executemany Est "off" par défaut, et le code de test suivant ...

cnxn = pyodbc.connect(conn_str, autocommit=True)
crsr = cnxn.cursor()
crsr.execute("TRUNCATE TABLE fast_executemany_test")

sql = "INSERT INTO fast_executemany_test (txtcol) VALUES (?)"
params = [(f'txt{i:06d}',) for i in range(1000)]
t0 = time.time()
crsr.executemany(sql, params)
print(f'{time.time() - t0:.1f} seconds')

... a pris environ 22 secondes à exécuter sur ma machine de test. Ajouter simplement crsr.fast_executemany = True ...

cnxn = pyodbc.connect(conn_str, autocommit=True)
crsr = cnxn.cursor()
crsr.execute("TRUNCATE TABLE fast_executemany_test")

crsr.fast_executemany = True  # new in pyodbc 4.0.19

sql = "INSERT INTO fast_executemany_test (txtcol) VALUES (?)"
params = [(f'txt{i:06d}',) for i in range(1000)]
t0 = time.time()
crsr.executemany(sql, params)
print(f'{time.time() - t0:.1f} seconds')

... réduit le temps d'exécution à un peu plus d'une seconde.

40
Gord Thompson

oui l'insertion en bloc est le bon chemin pour charger de gros fichiers dans une base de données. En un coup d'œil, je dirais que la raison pour laquelle cela prend autant de temps est que vous avez mentionné que vous parcourez chaque ligne de données du fichier, ce qui signifie en fait supprimer les avantages de l'utilisation d'un insert en bloc et le rendre comme un insert normal. N'oubliez pas que son nom implique qu'il est utilisé pour insérer des mandrins de données. Je supprimerais la boucle et réessayerais.

Je vérifierais également votre syntaxe pour l'insertion en bloc car elle ne me semble pas correcte. vérifiez le sql qui est généré par pyodbc car j'ai le sentiment qu'il pourrait seulement exécuter une insertion normale

Sinon, s'il est encore lent, j'essaierais d'utiliser l'insertion en bloc directement à partir de SQL et de charger le fichier entier dans une table temporaire avec insertion en bloc, puis d'insérer la colonne appropriée dans les bonnes tables. ou utilisez un mélange d'insert en vrac et de bcp pour obtenir les colonnes spécifiques insérées ou OPENROWSET.

1
Michael Moura