Pandas: Ajout de l'option (Insérer ou mettre à jour si la clé existe) à `.to_sql`

Créé le 1 nov. 2016  ·  42Commentaires  ·  Source: pandas-dev/pandas

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

Production attendue

    age
id  
1   18
2   44
3   95

(Peut-être) références de code utiles

J'ai regardé le code source de pandas sql.py pour trouver une solution, mais je n'ai pas pu suivre.

Code pour reproduire l'exemple ci-dessus

(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)
Enhancement IO SQL

Commentaire le plus utile

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.

Tous les 42 commentaires

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() .

Proposition d'interface

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)
)

Proposition de mise en œuvre

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) :

(1) Moteur agnostique utilisant le noyau SQLAlchemy, via une séquence atomique de DELETE et INSERT

  • Seuls certains dbms prennent en charge nativement upsert , et les implémentations peuvent varier selon les saveurs
  • En tant que première implémentation, nous pensons qu'il serait plus facile de tester et de maintenir une implémentation sur tous les dbms. À l'avenir, si la demande existe, des implémentations spécifiques au moteur pourront être ajoutées.
  • Pour upsert_ignore ces opérations seraient évidemment ignorées sur les enregistrements correspondants
  • Il vaudra la peine de comparer une implémentation indépendante du moteur aux implémentations spécifiques au moteur en termes de performances.

(2) Upsert sur la clé primaire uniquement

  • Les upserts sont par défaut les conflits de clés primaires, sauf indication contraire
  • Certains SGBD permettent aux utilisateurs de spécifier des colonnes de clé non primaire, par rapport auxquelles vérifier l'unicité. Bien que cela donne plus de flexibilité à l'utilisateur, cela présente des pièges potentiels. Si ces colonnes n'ont pas UNIQUE contrainte
  • Dans une future amélioration, si la communauté le demande, nous pourrions ajouter la fonctionnalité pour étendre upsert non seulement pour travailler sur la clé primaire, mais aussi sur les champs spécifiés par l'utilisateur. C'est une question à plus long terme pour l'équipe core-dev, à savoir si Pandas doit rester simple pour protéger les utilisateurs qui ont une base de données mal conçue, ou qui ont plus de fonctionnalités.

@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

  • la méthode fonctionne actuellement sur _comment_ les données sont insérées : ligne par ligne ou "multi"
  • if_exists capture la logique métier de la façon dont nous gérons nos enregistrements : "replace", "append", "upsert_update" (upsert lorsque la clé existe, ajouter lorsqu'elle est nouvelle), "upsert_ignore" (ignorer lorsque la clé existe, ajouter lorsqu'elle est nouvelle). Bien que remplacer et ajouter examinent l'existence de la table, cela peut également être compris dans son impact au niveau de l'enregistrement.

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 :

  1. Créez une table temporaire.
  2. Insérez les données dans cette table temporaire.
  3. Faites une MISE À JOUR... REJOIGNEZ.
  4. INSÉRER où la clé (PRIMAIRE ou UNIQUE) ne correspond pas.
  5. Supprimez la table temporaire.

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 :

  1. La structure de votre source et de votre destination est la même.
  2. Que vous vouliez faire des insertions simples en utilisant les données de votre dataframe.
  3. Que vous souhaitiez simplement mettre à jour toutes les colonnes non clés avec les données de votre dataframe.
  4. Que vous ne voulez pas apporter de modifications aux données dans les colonnes clés.

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.

Proposition d'approche modifiée

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
Cette page vous a été utile?
0 / 5 - 0 notes