Supposons que vous ayez une table SQL existante appelée person_age
, où id
est la clé primaire :
age
id
1 18
2 42
et vous avez également de nouvelles données dans un DataFrame
appelé extra_data
age
id
2 44
3 95
alors il serait utile d'avoir une option sur extra_data.to_sql()
qui permet de passer le DataFrame à SQL avec une option INSERT
ou UPDATE
sur les lignes, basée sur le primary key
.
Dans ce cas, la ligne id=2
serait mise à jour en age=44
et la ligne id=3
serait ajoutée
age
id
1 18
2 44
3 95
merge
de SQLAlchemy ?J'ai regardé le code source de pandas
sql.py
pour trouver une solution, mais je n'ai pas pu suivre.
(Toutes mes excuses pour avoir mélangé sqlalchemy
et sqlite
import pandas as pd
from sqlalchemy import create_engine
import sqlite3
conn = sqlite3.connect('example.db')
c = conn.cursor()
c.execute('''DROP TABLE IF EXISTS person_age;''')
c.execute('''
CREATE TABLE person_age
(id INTEGER PRIMARY KEY ASC, age INTEGER NOT NULL)
''')
conn.commit()
conn.close()
##### Create original table
engine = create_engine("sqlite:///example.db")
sql_df = pd.DataFrame({'id' : [1, 2], 'age' : [18, 42]})
sql_df.to_sql('person_age', engine, if_exists='append', index=False)
#### Extra data to insert/update
extra_data = pd.DataFrame({'id' : [2, 3], 'age' : [44, 95]})
extra_data.set_index('id', inplace=True)
#### extra_data.to_sql() with row update or insert option
expected_df = pd.DataFrame({'id': [1, 2, 3], 'age': [18, 44, 95]})
expected_df.set_index('id', inplace=True)
Ce serait une fonctionnalité intéressante, mais le principal problème est que nous voulons qu'elle soit indépendante de la saveur de la base de données et basée sur le noyau sqlalchemy (donc pas sur l'ORM sqlalchemy) pour être incluse dans les pandas lui-même.
Ce qui rendra cela difficile à mettre en œuvre..
Ouais, je pense que c'est hors de portée pour les pandas car les upserts ne sont pas pris en charge par tous les moteurs de base de données.
Alors qu'un INSERT OR UPDATE
n'est pas pris en charge par tous les moteurs, un INSERT OR REPLACE
peut être rendu indépendant du moteur en supprimant des lignes de la table cible pour l'ensemble de clés primaires dans l'index DataFrame suivi d'une insertion de toutes les lignes du DataFrame. Vous voudriez le faire dans une transaction.
@TomAugspurger Pourrions-nous ajouter l'option upsert pour les moteurs de base de données pris en charge et générer une erreur pour les moteurs de base de données non pris en charge ?
J'aimerais voir ça aussi. Je suis coincé entre l'utilisation de SQL pur et SQL Alchemy (je n'ai pas encore réussi à le faire fonctionner, je pense que cela a quelque chose à voir avec la façon dont je passe les dicts). J'utilise psycopg2 COPY pour l'insertion en masse, mais j'aimerais utiliser pd.to_sql pour les tables où les valeurs peuvent changer avec le temps et cela ne me dérange pas que l'insertion soit un peu plus lente.
insert_values = df.to_dict(orient='records')
insert_statement = sqlalchemy.dialects.postgresql.insert(table).values(insert_values)
upsert_statement = insert_statement.on_conflict_do_update(
constraint='fact_case_pkey',
set_= df.to_dict(orient='dict')
)
Et du SQL pur :
def create_update_query(df, table=FACT_TABLE):
"""This function takes the Airflow execution date passes it to other functions"""
columns = ', '.join([f'{col}' for col in DATABASE_COLUMNS])
constraint = ', '.join([f'{col}' for col in PRIMARY_KEY])
placeholder = ', '.join([f'%({col})s' for col in DATABASE_COLUMNS])
values = placeholder
updates = ', '.join([f'{col} = EXCLUDED.{col}' for col in DATABASE_COLUMNS])
query = f"""INSERT INTO {table} ({columns})
VALUES ({placeholder})
ON CONFLICT ({constraint})
DO UPDATE SET {updates};"""
query.split()
query = ' '.join(query.split())
return query
def load_updates(df, connection=DATABASE):
"""Uses COPY from STDIN to load to Postgres
:param df: The dataframe which is writing to StringIO, then loaded to the the database
:param connection: Refers to a PostgresHook
"""
conn = connection.get_conn()
cursor = conn.cursor()
df1 = df.where((pd.notnull(df)), None)
insert_values = df1.to_dict(orient='records')
for row in insert_values:
cursor.execute(create_update_query(df), row)
conn.commit()
cursor.close()
del cursor
conn.close()
@ldacey ce style a fonctionné pour moi (insert_statement.excluded est un alias de la ligne de données qui a violé la contrainte):
insert_values = merged_transactions_channels.to_dict(orient='records')
insert_statement = sqlalchemy.dialects.postgresql.insert(orders_to_channels).values(insert_values)
upsert_statement = insert_statement.on_conflict_do_update(
constraint='orders_to_channels_pkey',
set_={'channel_owner': insert_statement.excluded.channel_owner}
)
@cdagnino Cet extrait peut ne pas fonctionner dans le cas de clés composites, ce scénario doit également être pris en compte. je vais essayer de trouver un moyen de faire pareil
Une façon de résoudre ce problème de mise à jour consiste à utiliser bulk_update_mappings de sqlachemy . Cette fonction prend une liste de valeurs de dictionnaire et met à jour chaque ligne en fonction de la clé primaire des tables.
session.bulk_update_mappings(
Table,
pandas_df.to_dict(orient='records)
)
Je suis d'accord avec @neilfrndes , cela ne devrait pas permettre à une fonctionnalité intéressante comme celle-ci d'être implémentée car certaines bases de données ne le prennent pas en charge. Y a-t-il une chance que cette fonctionnalité se produise?
Probablement. si quelqu'un fait un PR. Après réflexion, je ne pense pas m'y opposer sur le principe que certaines bases de données ne le supportent pas. Cependant, je ne connais pas trop le code SQL, donc je ne sais pas quelle est la meilleure approche.
Une possibilité est de fournir quelques exemples d'upserts utilisant l'appelable method
si ce PR est introduit : https://github.com/pandas-dev/pandas/pull/21401
Pour postgres qui ressemblerait à quelque chose comme (non testé):
from sqlalchemy.dialects import postgresql
def pg_upsert(table, conn, keys, data_iter):
for row in data:
row_dict = dict(zip(keys, row))
stmt = postgresql.insert(table).values(**row_dict)
upsert_stmt = stmt.on_conflict_do_update(
index_elements=table.index,
set_=row_dict)
conn.execute(upsert_stmt)
Quelque chose de similaire pourrait être fait pour mysql .
Pour postgres, j'utilise execute_values. Dans mon cas, ma requête est un modèle jinja2 pour indiquer si je dois mettre à jour le jeu ou ne rien faire . Cela a été assez rapide et flexible. Pas aussi rapide que d'utiliser COPY ou copy_expert mais cela fonctionne bien.
from psycopg2.extras import execute_values
df = df.where((pd.notnull(df)), None)
tuples = [tuple(x) for x in df.values]
``
with pg_conn:
with pg_conn.cursor() as cur:
execute_values(cur=cur,
sql=insert_query,
argslist=tuples,
template=None,
)
@danich1 pouvez-vous, s'il vous plaît, donner un exemple de la façon dont cela fonctionnerait ?
J'ai essayé de jeter un œil à bulk_update_mappings mais je me suis vraiment perdu et je n'ai pas pu le faire fonctionner.
@cristianionescu92 Un exemple serait celui-ci :
J'ai une table appelée User avec les champs suivants : id et name.
| identifiant | nom |
| --- | --- |
| 0 | Jean |
| 1 | Joe |
| 2 | Harry |
J'ai un cadre de données pandas avec les mêmes colonnes mais des valeurs mises à jour :
| identifiant | nom |
| --- | --- |
| 0 | Chris |
| 1 | Jacques |
Supposons également que nous ayons une variable de session ouverte pour accéder à la base de données. En appelant cette méthode :
session.bulk_update_mappings(
User,
<pandas dataframe above>.to_dict(orient='records')
)
Pandas convertira la table en une liste de dictionnaires [{id: 0, name: "chris"}, {id: 1, name:"james"}] que sql utilisera pour mettre à jour les lignes de la table. La table finale ressemblera donc à :
| identifiant | nom |
| --- | --- |
| 0 | Chris |
| 1 | Jacques |
| 2 | Harry |
Bonjour @danich1 et merci beaucoup pour votre réponse. J'ai découvert moi-même les mécanismes de fonctionnement de la mise à jour. Malheureusement je ne sais pas travailler comment travailler avec une session, je suis assez débutant.
Laissez-moi vous montrer ce que je fais :
` importer pypyodbc
de to_sql_newrows import clean_df_db_dups, to_sql_newrows #ce sont 2 fonctions que j'ai trouvées sur GitHub, malheureusement je ne me souviens plus du lien. Clean_df_db_dups exclut d'un dataframe les lignes qui existent déjà dans une table SQL en vérifiant plusieurs colonnes clés et to_sql_newrows est une fonction qui insère dans sql les nouvelles lignes.
from sqlalchemy import create_engine
engine = create_engine("engine_connection_string")
#Write data to SQL
Tablename = 'Dummy_Table_Name'
Tablekeys = Tablekeys_string
dftoupdateorinsertinSQL= random_dummy_dataframe
#Connect to sql server db using pypyodbc
cnxn = pypyodbc.connect("Driver={SQL Server};"
"Server=ServerName;"
"Database=DatabaseName;"
"uid=userid;pwd=password")
newrowsdf= clean_df_db_dups(dftoupdateorinsertinSQL, Tablename, engine, dup_cols=Tablekeys)
newrowsdf.to_sql(Tablename, engine, if_exists='append', index=False, chunksize = 140)
end=timer()
tablesize = (len(newrowsdf.index))
print('inserted %r rows '%(tablesize))`
Le code ci-dessus exclut essentiellement d'une trame de données les lignes que j'ai déjà dans SQL et insère uniquement les nouvelles lignes. Ce dont j'ai besoin, c'est de mettre à jour les lignes qui existent. Pouvez-vous, s'il vous plaît, m'aider à comprendre ce que je dois faire ensuite ?
Motivation pour un meilleur TO_SQL
to_sql
mieux s'intégrer aux pratiques de base de données est de plus en plus précieux à mesure que la science des données se développe et se mélange à l'ingénierie des données.
upsert
fait partie, en particulier parce que de nombreuses personnes trouvent que le contournement consiste à utiliser replace
place, ce qui supprime la table, et avec elle toutes les vues et contraintes.
L'alternative que j'ai vue chez les utilisateurs plus expérimentés est d'arrêter d'utiliser les pandas à ce stade, et cela a tendance à se propager en amont et à rendre le paquet pandas plus difficile à retenir parmi les utilisateurs expérimentés. Est-ce la direction que Pandas veut prendre ?
Je comprends que nous voulons que to_sql reste autant que possible agnostique de la base de données et utilise l'alchimie de base de SQL. Une méthode qui tronque ou supprime au lieu d'un véritable upsert ajouterait quand même beaucoup de valeur.
Intégration avec la vision du produit Pandas
Une grande partie du débat ci-dessus a eu lieu avant l'introduction de l'argument method
(comme mentionné par @kjford avec psql_insert_copy
) et la possibilité de transmettre un callable.
Je serais ravi de contribuer soit à la fonctionnalité de base de pandas, soit à défaut, à la documentation sur la solution / les meilleures pratiques sur la façon d'obtenir une fonctionnalité d'upsert dans Pandas, comme ci-dessous :
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method
Quelle est la voie à suivre préférée pour les principaux développeurs/chefs de produit de Pandas ?
Je pense que nous sommes ouverts à une implémentation spécifique au moteur. La proposition d'utiliser method='upsert'
semble raisonnable, mais à ce stade, je pense que nous avons besoin de quelqu'un pour faire une proposition de conception claire.
J'ai une exigence similaire où je souhaite mettre à jour les données existantes dans une table MySQL à partir de plusieurs CSV au fil du temps.
J'ai pensé que je pouvais df.to_sql() pour insérer les nouvelles données dans une table temporaire nouvellement créée, puis exécuter une requête MySQL pour contrôler comment ajouter/mettre à jour les données dans la table existante .
Référence MySQL : https://stackoverflow.com/questions/2472229/insert-into-select-from-on-duplicate-key-update?answertab=active#tab -top
Avis de non-responsabilité : j'ai commencé à utiliser Python et Pandas il y a quelques jours seulement.
Salut les pandas : j'ai eu le même problème, j'ai dû fréquemment mettre à jour ma base de données locale avec des enregistrements que je charge et manipule finalement dans les pandas. J'ai construit une bibliothèque simple pour ce faire - il s'agit essentiellement d'un remplaçant pour df.to_sql et pd.read_sql_table qui utilise l'index DataFrame comme clé primaire par défaut. Utilise uniquement le noyau sqlalchemy.
https://pypi.org/project/pandabase/0.2.1/
https://github.com/notsambeck/pandabase
Cet outil est assez avisé, il n'est probablement pas approprié d'être inclus dans Pandas tel quel. Mais pour mon cas d'utilisation spécifique, cela résout le problème... s'il y a un intérêt à masser cela pour l'adapter aux Pandas, je suis heureux de vous aider.
Pour l'instant, les travaux suivants (dans le cas limité des pandas et sqlalchemy actuels, nommés index comme clé primaire, back-end SQLite ou Postgres et types de données pris en charge) :
pip install pandabase / pandabase.to_sql(df, table_name, con_string, how='upsert')
Travailler sur une solution générale à ce problème avec cvonsteg. Prévoyons de revenir avec un design proposé en octobre.
@TomAugspurger comme suggéré, @rugg2 et moi avons proposé la proposition de conception suivante pour une option upsert
dans to_sql()
.
2 nouvelles variables à ajouter comme argument possible method
dans la méthode to_sql()
:
1) upsert_update
- lors de la correspondance des lignes, mettre à jour la ligne dans la base de données (pour mettre à jour sciemment les enregistrements - représente la plupart des cas d'utilisation)
2) upsert_ignore
- lors de la correspondance des lignes, ne mettez pas à jour la ligne dans la base de données (pour les cas où les ensembles de données se chevauchent et que vous ne souhaitez pas remplacer les données dans les tables)
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine("connection string")
df = pd.DataFrame(...)
df.to_sql(
name='table_name',
con=engine,
if_exists='append',
method='upsert_update' # (or upsert_ignore)
)
Pour implémenter cela, la classe SQLTable
recevrait 2 nouvelles méthodes privées contenant la logique upsert, qui seraient appelées à partir de la méthode SQLTable.insert() :
def insert(self, chunksize=None, method=None):
#set insert method
if method is None:
exec_insert = self._execute_insert
elif method == "multi":
exec_insert = self.execute_insert_multi
#new upsert methods <<<
elif method == "upsert_update":
exec_insert = self.execute_upsert_update
elif method == "upsert_ignore":
exec_insert = self.execute_upsert_ignore
# >>>
elif callable(method):
exec_inset = partial(method, self)
else:
raise ValueError("Invalid parameter 'method': {}".format(method))
...
Nous proposons la mise en œuvre suivante, avec une justification détaillée ci-dessous (tous les points sont ouverts à la discussion) :
DELETE
et INSERT
upsert
, et les implémentations peuvent varier selon les saveursupsert_ignore
ces opérations seraient évidemment ignorées sur les enregistrements correspondantsUNIQUE
contrainte @TomAugspurger , si la proposition upsert
conçue avec @cvonsteg vous convient, nous procéderons à l'implémentation dans le code (y compris les tests) et lancerons une pull request.
Faites-nous savoir si vous souhaitez procéder différemment.
La lecture de la proposition est sur ma liste de choses à faire. je suis un peu en retard sur mon
e-mail dès maintenant.
Le mercredi 9 octobre 2019 à 9h18, Romain [email protected] a écrit :
@TomAugspurger https://github.com/TomAugspurger , si la conception nous
conçu avec @cvonsteg https://github.com/cvonsteg vous convient, nous allons
procéder à l'implémentation dans le code (y compris les tests) et lever un pull
demander.Faites-nous savoir si vous souhaitez procéder différemment.
-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/pandas-dev/pandas/issues/14553?email_source=notifications&email_token=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXH7A
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.
Personnellement, je n'ai rien contre, alors pensez qu'un RP est le bienvenu. Une implémentation sur tous les DBM utilisant le noyau SQLAlchemy est certainement la façon dont cela devrait commencer si je lis correctement vos points, et de même avec uniquement les clés primaires.
Toujours plus facile de commencer petit et concentré et de se développer à partir de là
besoin de cette fonctionnalité mal.
Les relations publiques que nous avons écrites avec cvonsteg devraient maintenant donner la fonctionnalité : les critiques maintenant !
Cette fonctionnalité serait absolument glorieuse ! Je ne suis pas trop versé dans le vocabulaire de github ; est-ce que le commentaire de @rugg2 selon lequel la fonctionnalité est « à revoir maintenant » signifie-t-il que c'est à l'équipe des pandas de l'examiner ? Et s'il est approuvé, cela signifie-t-il qu'il deviendra disponible via une nouvelle version de pandas que nous pourrons installer, ou serions-nous obligés d'appliquer le commit manuellement nous-mêmes via git ? (J'ai eu des problèmes avec cela via conda, donc si c'est le cas, j'aimerais me mettre au courant d'ici que cette fonctionnalité soit prête). Merci!!
@ pmgh2345 -
Les relations publiques que nous avons écrites avec cvonsteg devraient maintenant donner la fonctionnalité : les critiques maintenant !
Cela pourrait valoir la peine d'ajouter un nouveau paramètre à la méthode to_sql
, plutôt que d'utiliser if_exists
. La raison en est que if_exists
vérifie l'existence de la table, pas de la ligne.
@cvonsteg a initialement proposé d'utiliser method=
, ce qui éviterait l'ambiguïté d'avoir deux significations pour if_exists
.
df.to_sql(
name='table_name',
con=engine,
if_exists='append',
method='upsert_update' # (or upsert_ignore)
)
@brylie, nous pourrions ajouter un nouveau paramètre qui est vrai, mais comme vous le savez, chaque nouveau paramètre rend une API plus maladroite. Il y a un compromis.
Si nous devons choisir parmi les paramètres actuels, comme vous l'avez dit, nous avons d'abord pensé à utiliser l'argument method
, mais après plus de réflexions, nous avons réalisé que (1) l'utilisation et (2) la logique correspondent mieux au if_exists
argument.
1) du point de vue de l'utilisation de l'API
L'utilisateur voudra choisir à la fois method="multi" ou None d'une part, et "upsert" d'autre part. Cependant, il n'y a pas de cas d'utilisation aussi forts avec l'utilisation de la fonctionnalité "upsert" en même temps qu'un if_exists="append" ou "replace", le cas échéant.
2) d'un point de vue logique
Faites-moi savoir si j'ai bien compris votre point, et s'il vous plaît criez si vous pensez que la mise en œuvre actuelle en cours d'examen (PR #29636) serait un net négatif !
Oui, vous comprenez mon propos. L'implémentation actuelle est un net positif mais légèrement diminué par une sémantique ambiguë.
Je maintiens toujours que le if_exists
devrait continuer à se référer à une seule chose, l'existence d'une table. Une ambiguïté dans les paramètres a un impact négatif sur la lisibilité et peut conduire à une logique interne alambiquée. Alors que l'ajout d'un nouveau paramètre, comme upsert=True
est clair et explicite.
Salut!
Si vous voulez voir une implémentation non agnostique pour faire des upserts, j'ai un exemple avec ma bibliothèque pangres . Il gère PostgreSQL et MySQL à l'aide de fonctions sqlalchemy spécifiques à ces types de bases de données. Quant à SQlite (et d'autres types de bases de données permettant une syntaxe upsert similaire), il utilise un insert sqlalchemy régulier compilé.
Je partage cette réflexion en pensant que cela pourrait donner quelques idées aux collaborateurs (je suis cependant conscient que nous voulons que ce soit agnostique de type SQL, ce qui a beaucoup de sens). Aussi peut-être qu'une comparaison de vitesse serait également intéressante lorsque le PR de @cvonsteg passera .
Veuillez noter que je ne suis pas un expert de longue date en sqlalchemy ou autre !
Je veux vraiment cette fonctionnalité. Je suis d'accord qu'un method='upsert_update'
est une bonne idée.
Est-ce encore prévu ? Les pandas ont vraiment besoin de cette fonctionnalité
Oui c'est encore prévu, et on y est presque !
Le code est écrit, mais il y a un test qui ne passe pas. Aide bienvenue !
https://github.com/pandas-dev/pandas/pull/29636
Le mar. 5 mai 2020, 19:18 Leonel Atencio [email protected] a écrit :
Est-ce encore prévu ? Les pandas ont vraiment besoin de cette fonctionnalité
-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
ou se désinscrire
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.
Salut! La fonctionnalité est-elle prête ou manque-t-il encore quelque chose ? S'il manque encore quelque chose, n'hésitez pas à me dire si je peux vous aider !
Des nouvelles?))
Venant du monde Java, je n'aurais jamais pensé que cette fonctionnalité simple pourrait bouleverser ma base de code.
Salut à tous,
J'ai examiné comment les upserts sont implémentés dans SQL dans tous les dialectes et j'ai trouvé un certain nombre de techniques qui peuvent éclairer les décisions de conception ici. Mais d'abord, je veux mettre en garde contre l'utilisation de la logique DELETE ... INSERT. S'il existe des clés étrangères ou des déclencheurs, d'autres enregistrements de la base de données finiront par être supprimés ou endommagés. Dans MySQL, REPLACE fait les mêmes dégâts. J'ai en fait créé des heures de travail pour moi-même pour réparer des données parce que j'ai utilisé REPLACE. Donc, ceci dit, voici les techniques implémentées en SQL :
dialecte | Technique
-- | --
MySQL | INSÉRER ... SUR MISE À JOUR DE LA CLÉ EN DOUBLE
PostgreSQL | INSÉRER ... SUR CONFLIT
SQLite | INSÉRER ... SUR CONFLIT
DB2 | FUSIONNER
Serveur SQL | FUSIONNER
Oracle | FUSIONNER
SQL : 2016 | FUSIONNER
Avec une syntaxe très variable, je comprends la tentation d'utiliser DELETE ... INSERT pour rendre le dialecte d'implémentation agnostique. Mais il existe un autre moyen : nous pouvons imiter la logique de l'instruction MERGE en utilisant une table temporaire et des instructions INSERT et UPDATE de base. La syntaxe SQL:2016 MERGE est la suivante :
MERGE INTO target_table
USING source_table
ON search_condition
WHEN MATCHED THEN
UPDATE SET col1 = value1, col2 = value2,...
WHEN NOT MATCHED THEN
INSERT (col1,col2,...)
VALUES (value1,value2,...);
Emprunté à partir du didacticiel Oracle
et ajusté pour se conformer à SQL Wikibook
Étant donné que chaque dialecte pris en charge par SQLAlchemy prend en charge les tables temporaires, une approche plus sûre et indépendante du dialecte pour effectuer un upsert serait, en une seule transaction :
En plus d'être une technique indépendante du dialecte, elle a également l'avantage d'être étendue en permettant à l'utilisateur final de choisir comment insérer ou comment mettre à jour les données ainsi que sur quelle clé joindre les données.
Bien que la syntaxe des tables temporaires et des jointures de mise à jour puissent différer légèrement d'un dialecte à l'autre, elles doivent être prises en charge partout.
Vous trouverez ci-dessous une preuve de concept que j'ai écrite pour MySQL :
import uuid
import pandas as pd
from sqlalchemy import create_engine
# This proof of concept uses this sample database
# https://downloads.mysql.com/docs/world.sql.zip
# Arbitrary, unique temp table name to avoid possible collision
source = str(uuid.uuid4()).split('-')[-1]
# Table we're doing our upsert against
target = 'countrylanguage'
db_url = 'mysql://<{user: }>:<{passwd: }>.@<{host: }>/<{db: }>'
df = pd.read_sql(
f'SELECT * FROM `{target}`;',
db_url
)
# Change for UPDATE, 5.3->5.4
df.at[0,'Percentage'] = 5.4
# Change for INSERT
df = df.append(
{'CountryCode': 'ABW','Language': 'Arabic','IsOfficial': 'F','Percentage':0.0},
ignore_index=True
)
# List of PRIMARY or UNIQUE keys
key = ['CountryCode','Language']
# Do all of this in a single transaction
engine = create_engine(db_url)
with engine.begin() as con:
# Create temp table like target table to stage data for upsert
con.execute(f'CREATE TEMPORARY TABLE `{source}` LIKE `{target}`;')
# Insert dataframe into temp table
df.to_sql(source,con,if_exists='append',index=False,method='multi')
# INSERT where the key doesn't match (new rows)
con.execute(f'''
INSERT INTO `{target}`
SELECT
*
FROM
`{source}`
WHERE
(`{'`, `'.join(key)}`) NOT IN (SELECT `{'`, `'.join(key)}` FROM `{target}`);
''')
# Create a doubled list of tuples of non-key columns to template the update statement
non_key_columns = [(i,i) for i in df.columns if i not in key]
# Whitespace for aesthetics
whitespace = '\n\t\t\t'
# Do an UPDATE ... JOIN to set all non-key columns of target to equal source
con.execute(f'''
UPDATE
`{target}` `t`
JOIN
`{source}` `s` ON `t`.`{"` AND `t`.`".join(["`=`s`.`".join(i) for i in zip(key,key)])}`
SET
`t`.`{f"`,{whitespace}`t`.`".join(["`=`s`.`".join(i) for i in non_key_columns])}`;
''')
# Drop our temp table.
con.execute(f'DROP TABLE `{source}`;')
Ici, je fais les hypothèses suivantes :
Malgré les hypothèses, j'espère que ma technique inspirée de MERGE informera les efforts visant à créer une option d'upsert flexible et robuste.
Je pense qu'il s'agit d'une fonctionnalité utile, mais cela semble hors de portée, car il est intuitif d'avoir une fonctionnalité aussi commune lors de l'ajout de lignes à une table.
Veuillez réfléchir à nouveau pour ajouter cette fonction : il est très utile d'ajouter des lignes à une table existante.
Hélas Pangres est limité à Python 3.7+. Comme dans mon cas (je suis obligé d'utiliser un vieux Python 3.4), ce n'est pas toujours une solution viable.
Merci, @GoldstHa - c'est une contribution vraiment utile. Je vais essayer de créer un POC pour l'implémentation de type MERGE
Compte tenu des problèmes liés à l'approche DELETE/INSERT
et au bloqueur potentiel de l'approche @GoldstHa MERGE
sur les bases de données MySQL, j'ai creusé un peu plus. J'ai rassemblé une preuve de concept en utilisant la fonctionnalité de mise à jour de sqlalchemy , qui semble prometteuse. Je vais essayer de l'implémenter correctement cette semaine dans la base de code Pandas, en m'assurant que cette approche fonctionne dans toutes les versions de la base de données.
Il y a eu de bonnes discussions autour de l'API et de la façon dont un upsert devrait être appelé (c'est-à-dire via l'argument if_exists
, ou via un argument explicite upsert
). Cela sera précisé prochainement. Pour l'instant, voici la proposition de pseudocode sur le fonctionnement de la fonctionnalité en utilisant l'instruction SqlAlchemy
upsert
:
Identify primary key(s) and existing pkey values from DB table (if no primary key constraints identified, but upsert is called, return an error)
Make a temp copy of the incoming DataFrame
Identify records in incoming DataFrame with matching primary keys
Split temp DataFrame into records which have a primary key match, and records which don't
if upsert:
Update the DB table using `update` for only the rows which match
else:
Ignore rows from DataFrame with matching primary key values
finally:
Append remaining DataFrame rows with non-matching values in the primary key column to the DB table
Commentaire le plus utile
Alors qu'un
INSERT OR UPDATE
n'est pas pris en charge par tous les moteurs, unINSERT OR REPLACE
peut être rendu indépendant du moteur en supprimant des lignes de la table cible pour l'ensemble de clés primaires dans l'index DataFrame suivi d'une insertion de toutes les lignes du DataFrame. Vous voudriez le faire dans une transaction.