Pandas: Hinzufügen (Einfügen oder aktualisieren, wenn Schlüssel vorhanden) Option zu `.to_sql`

Erstellt am 1. Nov. 2016  ·  42Kommentare  ·  Quelle: pandas-dev/pandas

Angenommen, Sie haben eine vorhandene SQL-Tabelle namens person_age , wobei id der Primärschlüssel ist:

    age
id  
1   18
2   42

und Sie haben auch neue Daten in einem DataFrame namens extra_data

    age
id  
2   44
3   95

Dann wäre es nützlich, eine Option für extra_data.to_sql() , die es ermöglicht, den DataFrame mit einer INSERT oder UPDATE Option für die Zeilen an SQL zu übergeben, basierend auf primary key .

In diesem Fall würde die Zeile id=2 auf age=44 aktualisiert und die Zeile id=3 hinzugefügt

Erwartete Ausgabe

    age
id  
1   18
2   44
3   95

(Vielleicht) hilfreiche Codereferenzen

Ich habe mir pandas sql.py Quellcode angesehen, um eine Lösung zu finden, aber ich konnte nicht folgen.

Code zum Replizieren des obigen Beispiels

(Entschuldigung für die Vermischung von sqlalchemy und 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

Hilfreichster Kommentar

Während ein INSERT OR UPDATE nicht von allen Engines unterstützt wird, kann ein INSERT OR REPLACE Engine-unabhängig gemacht werden, indem Zeilen aus der Zieltabelle für den Satz von Primärschlüsseln im DataFrame-Index gelöscht werden, gefolgt von einer Einfügung von alle Zeilen im DataFrame. Sie möchten dies in einer Transaktion tun.

Alle 42 Kommentare

Das wäre eine nette Funktionalität, aber das Hauptproblem ist, dass wir wollen, dass es vom Datenbank-Geschmack unabhängig ist und auf dem sqlalchemy-Kern (also nicht dem sqlalchemy-ORM) für die Aufnahme in Pandas selbst basiert.
Was die Umsetzung erschwert..

Ja, ich denke, dies ist für Pandas nicht möglich, da Upserts nicht von allen DB-Engines unterstützt werden.

Während ein INSERT OR UPDATE nicht von allen Engines unterstützt wird, kann ein INSERT OR REPLACE Engine-unabhängig gemacht werden, indem Zeilen aus der Zieltabelle für den Satz von Primärschlüsseln im DataFrame-Index gelöscht werden, gefolgt von einer Einfügung von alle Zeilen im DataFrame. Sie möchten dies in einer Transaktion tun.

@TomAugspurger Könnten wir die Upsert-Option für unterstützte DB-Engines hinzufügen und einen Fehler für nicht unterstützte DB-Engines auslösen?

Das würde ich auch gerne sehen. Ich bin zwischen der Verwendung von reinem SQL und SQL Alchemy gefangen (habe das noch nicht zum Laufen gebracht, ich denke, es hat etwas damit zu tun, wie ich die Diktate übergebe). Ich verwende psycopg2 COPY zum Masseneinfügen, aber ich würde gerne pd.to_sql für Tabellen verwenden, bei denen sich Werte im Laufe der Zeit ändern können und es mir nichts ausmacht, dass es etwas langsamer eingefügt wird.

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

Und reines SQL:

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 dieser Stil hat für mich funktioniert (insert_statement.excluded ist ein Alias ​​für die Datenzeile, die gegen die Einschränkung verstoßen hat):

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 Dieses Snippet funktioniert bei zusammengesetzten Schlüsseln möglicherweise nicht, dieses Szenario muss ebenfalls

Eine Möglichkeit, dieses Update-Problem zu lösen, besteht darin, Bulk_update_mappings von sqlachemy zu verwenden . Diese Funktion übernimmt eine Liste von Wörterbuchwerten und aktualisiert jede Zeile basierend auf dem Primärschlüssel der Tabelle.

session.bulk_update_mappings(
  Table,
  pandas_df.to_dict(orient='records)
)

Ich stimme @neilfrndes zu , sollte nicht zulassen, dass ein nettes Feature wie dieses nicht implementiert wird, da einige DBs dies nicht unterstützen. Besteht die Möglichkeit, dass diese Funktion auftritt?

Wahrscheinlich. wenn jemand eine PR macht. Bei weiterer Betrachtung glaube ich nicht, dass ich dagegen bin, weil einige Datenbanken dies nicht unterstützen. Ich bin jedoch mit dem SQL-Code nicht allzu vertraut, daher bin ich mir nicht sicher, was der beste Ansatz ist.

Eine Möglichkeit besteht darin, einige Beispiele für Upserts bereitzustellen, die den Callable method wenn dieser PR eingeführt wird: https://github.com/pandas-dev/pandas/pull/21401

Für Postgres würde das

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)

Ähnliches könnte für mysql gemacht werden .

Für Postgres verwende ich execute_values. In meinem Fall ist meine Abfrage eine jinja2-Vorlage, um zu kennzeichnen, ob ich update set oder nichts tun soll . Das ging recht schnell und flexibel. Nicht so schnell wie die Verwendung von COPY oder copy_expert, aber es funktioniert gut.

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 können Sie bitte ein Beispiel dafür geben, wie das funktionieren würde?

Ich habe versucht, einen Blick in bulk_update_mappings zu werfen, aber ich habe mich wirklich verlaufen und konnte es nicht zum Laufen bringen.

@cristianionescu92 Ein Beispiel wäre dieses:
Ich habe eine Tabelle namens User mit den folgenden Feldern: ID und Name.

| ID | Name |
| --- | --- |
| 0 | Johannes |
| 1 | Joe |
| 2 | Harry |

Ich habe einen Pandas-Datenrahmen mit denselben Spalten, aber aktualisierten Werten:

| id | Name |
| --- | --- |
| 0 | Chris |
| 1 | James |

Nehmen wir auch an, dass wir eine Sitzungsvariable geöffnet haben, um auf die Datenbank zuzugreifen. Durch Aufruf dieser Methode:

session.bulk_update_mappings(
User,
<pandas dataframe above>.to_dict(orient='records')
)

Pandas konvertiert die Tabelle in eine Liste von Wörterbüchern [{id: 0, name: "chris"}, {id: 1, name:"james"}], die von SQL verwendet werden, um die Zeilen der Tabelle zu aktualisieren. Der Final Table sieht also wie folgt aus:

| ID | Name |
| --- | --- |
| 0 | Chris |
| 1 | James |
| 2 | Harry |

Hallo @danich1 und vielen Dank für deine Antwort. Ich habe selbst herausgefunden, wie das Update funktionieren würde. Leider weiß ich nicht, wie man mit einer Session arbeitet, ich bin ziemlicher Anfänger.

Lassen Sie mich Ihnen zeigen, was ich tue:

` pypyodbc importieren
from to_sql_newrows import clean_df_db_dups, to_sql_newrows #das sind 2 Funktionen die ich auf GitHub gefunden habe, leider kann ich mich nicht an den Link erinnern. Clean_df_db_dups schließt aus einem Datenrahmen die Zeilen aus, die bereits in einer SQL-Tabelle vorhanden sind, indem es mehrere Schlüsselspalten überprüft und to_sql_newrows ist eine Funktion, die die neuen Zeilen in SQL einfügt.

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

Der obige Code schließt grundsätzlich die Zeilen aus, die ich bereits in SQL habe, und fügt nur die neuen Zeilen ein. Was ich brauche, ist, die vorhandenen Zeilen zu aktualisieren. Können Sie mir bitte helfen zu verstehen, was ich als nächstes tun soll?

Motivation für ein besseres TO_SQL
to_sql bessere Integration in Datenbankpraktiken wird immer wertvoller, da Data Science wächst und sich mit Data Engineering vermischt.

upsert ist eine davon, insbesondere weil viele Leute feststellen, dass die Umgehung darin besteht, stattdessen replace verwenden, wodurch die Tabelle und damit alle Ansichten und Einschränkungen gelöscht werden.

Die Alternative, die ich bei erfahreneren Benutzern gesehen habe, besteht darin, die Verwendung von Pandas zu diesem Zeitpunkt einzustellen, und dies neigt dazu, sich stromaufwärts zu verbreiten und führt dazu, dass das Pandas-Paket bei erfahrenen Benutzern verloren geht. Ist das die Richtung, in die Pandas gehen wollen?

Ich verstehe, dass wir to_sql so weit wie möglich datenbankunabhängig bleiben und die Core-SQL-Alchemie verwenden möchten. Eine Methode, die anstelle eines echten Upserts abschneidet oder löscht, würde jedoch immer noch viel Wert hinzufügen.

Integration mit Pandas Produktvision
Ein Großteil der obigen Debatte fand vor der Einführung des Arguments method (wie von @kjford mit psql_insert_copy ) und der Möglichkeit, einen Callable weiterzugeben, statt.

Ich würde gerne entweder zur Kernfunktion von Pandas beitragen oder, falls dies nicht der Fall ist, eine Dokumentation zur Lösung / Best Practice zum Erreichen einer Upsert-Funktionalität in Pandas, wie z.
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method

Was ist der bevorzugte Weg nach vorne für Pandas Core-Entwickler/Produktmanager?

Ich denke, wir sind offen für eine Engine-spezifische Implementierung. Der Vorschlag, method='upsert' scheint vernünftig, aber ich denke, wir brauchen jetzt jemanden, der einen klaren Designvorschlag macht.

Ich habe eine ähnliche Anforderung, bei der ich vorhandene Daten in einer MySQL-Tabelle aus mehreren CSVs im Laufe der Zeit aktualisieren möchte.

Ich dachte, ich könnte mit df.to_sql() die neuen Daten in eine neu erstellte temporäre Tabelle einfügen und dann eine MySQL-Abfrage ausführen, um zu steuern, wie Daten in die vorhandene Tabelle angehängt/aktualisiert werden.

MySQL-Referenz: https://stackoverflow.com/questions/2472229/insert-into-select-from-on-duplicate-key-update?answertab=active#tab -top

Haftungsausschluss: Ich habe erst vor wenigen Tagen angefangen, Python und Pandas zu verwenden.

Hey Pandas-Leute: Ich hatte das gleiche Problem und musste meine lokale Datenbank häufig mit Datensätzen aktualisieren, die ich schließlich in Pandas lade und manipuliere. Ich habe dafür eine einfache Bibliothek erstellt - im Grunde ist es ein Ersatz für df.to_sql und pd.read_sql_table, die standardmäßig den DataFrame-Index als Primärschlüssel verwendet. Verwendet nur den Sqlalchemy-Kern.

https://pypi.org/project/pandabase/0.2.1/
Https://github.com/notsambeck/pandabase

Dieses Tool ist ziemlich eigensinnig und wahrscheinlich nicht geeignet, um es in Pandas wie es ist, aufzunehmen. Aber für meinen speziellen Anwendungsfall löst es das Problem ... wenn Interesse besteht, dies zu massieren, um es in Pandas zu passen, helfe ich gerne.

Im Moment funktioniert Folgendes (im begrenzten Fall von aktuellen Pandas und Sqlalchemy, benannter Index als Primärschlüssel, SQLite- oder Postgres-Backend und unterstützte Datentypen):

pip install pandabase / pandabase.to_sql(df, table_name, con_string, how='upsert')

Arbeite mit cvonsteg an einer generellen Lösung dafür. Wir planen, im Oktober mit einem vorgeschlagenen Design zurückzukehren.

@TomAugspurger wie vorgeschlagen, @rugg2 und ich haben den folgenden Designvorschlag für eine Option upsert in to_sql() .

Schnittstellenvorschlag

2 neue Variablen, die als mögliches method Argument in der to_sql() Methode hinzugefügt werden sollen:
1) upsert_update – bei Zeilenübereinstimmung, Zeile in der Datenbank aktualisieren (zum wissentlich Aktualisieren von Datensätzen – repräsentiert die meisten Anwendungsfälle)
2) upsert_ignore - bei Zeilenübereinstimmung, Zeile in der Datenbank nicht aktualisieren (für Fälle, in denen sich Datensätze überschneiden und Sie Daten in Tabellen nicht überschreiben möchten)

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

Implementierungsvorschlag

Um dies zu implementieren, würde die Klasse SQLTable 2 neue private Methoden erhalten, die die Upsert-Logik enthalten, die von der Methode SQLTable.insert() aufgerufen würde:

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

    ...

Wir schlagen die folgende Implementierung vor, deren Begründung im Folgenden detailliert beschrieben wird (alle Punkte stehen zur Diskussion):

(1) Engine-Agnostik mit SQLAlchemy-Kern, über eine atomare Sequenz von DELETE und INSERT

  • Nur einige DBMs unterstützen nativ upsert , und die Implementierungen können je nach Variante variieren
  • Als erste Implementierung glauben wir, dass es einfacher wäre, eine Implementierung über alle DBMs hinweg zu testen und zu warten. Zukünftig können bei Bedarf motorspezifische Implementierungen hinzugefügt werden.
  • Für upsert_ignore diese Operationen offensichtlich bei übereinstimmenden Datensätzen übersprungen
  • Es lohnt sich, eine motorunabhängige Implementierung mit motorspezifischen Implementierungen in Bezug auf die Leistung zu vergleichen.

(2) Upsert nur auf Primärschlüssel

  • Upserts standardmäßig zu Konflikten mit Primärschlüsseln, sofern nicht anders angegeben
  • Einige DBMS ermöglichen es Benutzern, Nicht-Primärschlüssel-Spalten anzugeben, die auf Eindeutigkeit geprüft werden. Dies bietet dem Benutzer zwar mehr Flexibilität, birgt jedoch potenzielle Fallstricke. Wenn diese Spalten keinen UNIQUE Constraint haben, dann ist es plausibel, dass mehrere Zeilen die Upsert-Bedingung erfüllen können. In diesem Fall sollte kein Upsert durchgeführt werden, da nicht eindeutig ist, welcher Datensatz aktualisiert werden soll. Um dies von Pandas zu erzwingen, müsste jede Zeile einzeln bewertet werden, um zu überprüfen, ob nur 1 oder 0 Zeilen übereinstimmen, bevor sie eingefügt wird. Obwohl diese Funktionalität relativ einfach zu implementieren ist, führt sie dazu, dass jeder Datensatz einen Lese- und einen Schreibvorgang erfordert (plus ein Löschen, wenn ein Konflikt mit einem Datensatz gefunden wurde), was bei größeren Datensätzen äußerst ineffizient erscheint.
  • In einer zukünftigen Verbesserung könnten wir, wenn die Community dies fordert, die Funktionalität hinzufügen, um Upsert zu erweitern, um nicht nur am Primärschlüssel, sondern auch an benutzerdefinierten Feldern zu arbeiten. Dies ist eine längerfristige Frage für das Core-Dev-Team, ob Pandas einfach bleiben sollten, um Benutzer mit einer schlecht gestalteten Datenbank oder mehr Funktionalitäten zu schützen.

@TomAugspurger , wenn der mit @cvonsteg entworfene upsert Vorschlag zu Ihnen passt, werden wir mit der Implementierung in Code (inkl. Tests) fortfahren und eine Pull-Anfrage stellen.

Lassen Sie es uns wissen, wenn Sie anders vorgehen möchten.

Das Durchlesen des Vorschlags steht auf meiner Todo-Liste. Ich bin ein bisschen im Rückstand mit meinem
E-Mail gleich.

Am Mittwoch, 9. Oktober 2019 um 9:18 Uhr schrieb Romain [email protected] :

@TomAugspurger https://github.com/TomAugspurger , wenn das Design wir
designed mit @cvonsteg https://github.com/cvonsteg passt zu dir, wir werden
Fahren Sie mit der Implementierung in Code (inkl. Tests) fort und lösen Sie einen Pull aus
Anfrage.

Lassen Sie es uns wissen, wenn Sie anders vorgehen möchten.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/pandas-dev/pandas/issues/14553?email_source=notifications&email_token=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVREXG43VMVBJ002KTLNWW2B2
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.

Ich persönlich habe nichts dagegen, daher denke ich, dass eine PR willkommen ist. Eine Implementierung in allen DBMs, die SQLAlchemy-Kern verwenden, ist sicherlich, wie dies beginnen sollte, wenn ich Ihre Punkte richtig lese, und dasselbe nur mit Primärschlüsseln.

Es ist immer einfacher, klein und fokussiert anzufangen und von dort aus zu expandieren

brauche diese Funktion dringend.

PR, die wir mit cvonsteg geschrieben haben, sollte jetzt die Funktionalität geben: jetzt zu den Bewertungen!

Diese Funktionalität wäre absolut herrlich! Ich bin mit dem Vokabular von Github nicht allzu versiert; Bedeutet der Kommentar von reduziert " ist, dass es am Panda-Team liegt, sie zu überprüfen? Und wenn es genehmigt wird, bedeutet das, dass es über eine neue Version von Pandas verfügbar wird, die wir installieren können, oder müssen wir das Commit selbst manuell über git anwenden? (Ich hatte Probleme damit durch Conda. Wenn dies der Fall ist, möchte ich mich auf den neuesten Stand bringen, bis diese Funktionalität bereit ist). Dankeschön!!

@ pmgh2345 - ja, wie Sie sagten, "bis zu den Bewertungen jetzt" bedeutet, dass eine Pull-Anfrage gestellt wurde und von den Kernentwicklern überprüft wird. Sie können die oben erwähnte PR (#29636) sehen. Sobald es genehmigt ist, könnten Sie den Zweig technisch mit dem aktualisierten Code abspalten und Ihre eigene lokale Version von Pandas mit der integrierten Funktionalität kompilieren. Ich persönlich würde jedoch empfehlen, zu warten, bis er in Master zusammengeführt und veröffentlicht wurde, und dann einfach die Installation pip zu machen die neueste Panda-Version.

PR, die wir mit cvonsteg geschrieben haben, sollte jetzt die Funktionalität geben: jetzt zu den Bewertungen!

Es könnte sich lohnen, der Methode to_sql einen neuen Parameter hinzuzufügen, anstatt if_exists . Der Grund dafür ist, dass if_exists Existenz einer Tabelle und nicht einer Zeile überprüft.

@cvonsteg schlug ursprünglich vor, method= , was die Mehrdeutigkeit vermeiden würde, zwei Bedeutungen für if_exists .

df.to_sql(
    name='table_name', 
    con=engine, 
    if_exists='append', 
    method='upsert_update' # (or upsert_ignore)
)

@brylie wir könnten einen neuen Parameter hinzufügen, der wahr ist, aber wie Sie wissen, macht jeder neue Parameter eine API klobiger. Es gibt einen Kompromiss.

Wenn wir unter den aktuellen Parametern wählen müssen, haben wir, wie Sie sagten, ursprünglich daran gedacht, das Argument method , aber nach mehr Überlegung haben wir festgestellt, dass sowohl (1) die Verwendung als auch (2) die Logik besser zu if_exists passen

1) aus Sicht der API-Nutzung
Der Benutzer wird einerseits method="multi" oder None und andererseits "upsert" wählen wollen. Es gibt jedoch keine gleich starken Anwendungsfälle bei der gleichzeitigen Verwendung der "Upsert"-Funktionalität mit if_exists="append" oder "replace", falls vorhanden.

2) aus logischer Sicht

  • Methode funktioniert derzeit auf _wie_ die Daten eingefügt werden: Zeile für Zeile oder "multi"
  • if_exists erfasst die Geschäftslogik, wie wir unsere Datensätze verwalten: "replace", "append", "upsert_update" (upsert wenn Schlüssel vorhanden, anhängen wenn neu), "upsert_ignore" (ignore wenn Schlüssel existiert, anhängen wenn neu). Obwohl replace und append die Existenz von Tabellen untersuchen, können sie auch in ihren Auswirkungen auf Datensatzebene verstanden werden.

Lassen Sie es mich wissen, wenn ich Ihren Punkt gut verstanden habe, und rufen Sie bitte an, wenn Sie der Meinung sind, dass die derzeit geprüfte Implementierung (PR #29636) ein Netto-Negativ wäre!

Ja, du verstehst meinen Standpunkt. Die aktuelle Implementierung ist netto positiv, wird aber durch eine mehrdeutige Semantik leicht gemindert.

Ich behaupte immer noch, dass sich das if_exists weiterhin nur auf eine Sache beziehen sollte, die Tabellenexistenz. Mehrdeutigkeiten in den Parametern wirken sich negativ auf die Lesbarkeit aus und können zu einer verworrenen internen Logik führen. Das Hinzufügen eines neuen Parameters wie upsert=True ist klar und eindeutig.

Hallo!

Wenn Sie eine nicht-agnostische Implementierung für Upserts sehen möchten, habe ich ein Beispiel mit meiner Bibliothek pangres . Es verarbeitet PostgreSQL und MySQL mit sqlalchemy-Funktionen, die für diese Datenbanktypen spezifisch sind. SQlite (und andere Datenbanktypen, die eine ähnliche Upsert-Syntax ermöglichen) verwendet ein kompiliertes reguläres sqlalchemy-Insert.

Ich teile dieses Denken, es könnte den Mitarbeitern ein paar Ideen geben (ich bin mir jedoch bewusst, dass dies SQL-Typ-agnostisch sein soll, was sehr sinnvoll ist). Vielleicht wäre auch ein Geschwindigkeitsvergleich interessant, wenn die PR von @cvonsteg durchgeht.
Bitte beachten Sie, dass ich kein langjähriger Sqlalchemie-Experte oder so bin!

Ich möchte diese Funktion unbedingt haben. Ich stimme zu, dass ein method='upsert_update' eine gute Idee ist.

Ist das noch geplant? Pandas brauchen diese Funktion wirklich

Ja, das ist noch geplant, und wir sind fast da!

Code ist geschrieben, aber es gibt einen Test, der nicht besteht. Hilfe willkommen!
https://github.com/pandas-dev/pandas/pull/29636

Am Di, 5. Mai 2020, 19:18 schrieb Leonel Atencio [email protected] :

Ist das noch geplant? Pandas brauchen diese Funktion wirklich


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
oder abmelden
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.

Hallo! Ist die Funktionalität fertig oder fehlt noch etwas? Sollte noch etwas fehlen, lassen Sie es mich bitte wissen, wenn ich Ihnen helfen kann!

Irgendwelche Neuigkeiten?))

Ich komme aus der Java-Welt und hätte nie gedacht, dass diese einfache Funktionalität meine Codebasis auf den Kopf stellen könnte.

Hallo allerseits,

Ich habe untersucht, wie Upserts in SQL in verschiedenen Dialekten implementiert werden, und eine Reihe von Techniken gefunden, die hier Designentscheidungen beeinflussen können. Aber zuerst möchte ich davor warnen, die DELETE ... INSERT-Logik zu verwenden. Wenn Fremdschlüssel oder Trigger vorhanden sind, werden andere Datensätze in der Datenbank am Ende gelöscht oder anderweitig durcheinander gebracht. In MySQL richtet REPLACE denselben Schaden an. Ich habe mir tatsächlich stundenlange Arbeit beim Reparieren von Daten gemacht, weil ich REPLACE verwendet habe. Hier sind also die in SQL implementierten Techniken:

Dialekt | Technik
-- | --
MySQL | INSERT ... ON DUPLICATE KEY UPDATE
PostgreSQL | EINFÜGEN ... BEI KONFLIKT
SQLite | EINFÜGEN ... BEI KONFLIKT
DB2 | VERSCHMELZEN
SQL Server | VERSCHMELZEN
Orakel | VERSCHMELZEN
SQL:2016 | VERSCHMELZEN

Bei der stark variierenden Syntax verstehe ich die Versuchung, DELETE ... INSERT zu verwenden, um die Implementierung dialektagnostisch zu machen. Aber es gibt noch einen anderen Weg: Wir können die Logik der MERGE-Anweisung nachahmen, indem wir eine temporäre Tabelle und grundlegende INSERT- und UPDATE-Anweisungen verwenden. Die SQL:2016 MERGE-Syntax lautet wie folgt:

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,...);

Aus dem Oracle-Tutorial entlehnt
und angepasst an SQL Wikibook

Da jeder von SQLAlchemy unterstützte Dialekt temporäre Tabellen unterstützt, wäre ein sichererer, dialektunabhängiger Ansatz für die Durchführung eines Upserts in einer einzigen Transaktion:

  1. Erstellen Sie eine temporäre Tabelle.
  2. Fügen Sie die Daten in diese temporäre Tabelle ein.
  3. Machen Sie ein UPDATE ... JOIN.
  4. INSERT, wo der Schlüssel (PRIMARY oder UNIQUE) nicht übereinstimmt.
  5. Löschen Sie die temporäre Tabelle.

Abgesehen davon, dass es sich um ein dialektagnostisches Verfahren handelt, hat es auch den Vorteil, dass es erweitert werden kann, indem es dem Endbenutzer ermöglicht, zu wählen, wie die Daten eingefügt oder aktualisiert werden sollen, sowie mit welchem ​​Schlüssel die Daten verbunden werden sollen.

Obwohl sich die Syntax von temporären Tabellen und Update-Joins zwischen den Dialekten geringfügig unterscheiden kann, sollten sie überall unterstützt werden.

Unten ist ein Proof of Concept, den ich für MySQL geschrieben habe:

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}`;')

Dabei gehe ich von folgenden Annahmen aus:

  1. Die Struktur von Quelle und Ziel ist gleich.
  2. Dass Sie einfache Einfügungen mit den Daten in Ihrem Datenrahmen durchführen möchten.
  3. Dass Sie einfach alle Nicht-Schlüsselspalten mit den Daten aus Ihrem Datenrahmen aktualisieren möchten.
  4. Dass Sie keine Änderungen an Daten in Schlüsselspalten vornehmen möchten.

Trotz der Annahmen hoffe ich, dass meine von MERGE inspirierte Technik die Bemühungen zum Aufbau einer flexiblen, robusten Upsert-Option unterstützt.

Ich denke, dies ist eine nützliche Funktionalität, die jedoch außerhalb des Geltungsbereichs liegt, da es intuitiv ist, eine solche gemeinsame Funktion beim Hinzufügen von Zeilen zu einer Tabelle zu haben.

Bitte denken Sie noch einmal daran, diese Funktion hinzuzufügen: Es ist sehr nützlich, Zeilen zu einer bestehenden Tabelle hinzuzufügen.
Leider ist Pangres auf Python 3.7+ beschränkt. Wie in meinem Fall (ich bin gezwungen, ein altes Python 3.4) zu verwenden, ist dies nicht immer eine praktikable Lösung.

Danke, @GoldstHa - das ist wirklich hilfreicher Input. Ich werde versuchen, einen POC für die MERGE-ähnliche Implementierung zu erstellen

Angesichts der Probleme mit dem DELETE/INSERT Ansatz und dem potenziellen Blocker bei @GoldstHa MERGE Ansatz bei MySQL-DBs habe ich ein bisschen mehr gegraben. Ich habe mit der sqlalchemy-Update- Funktionalität einen Proof of Concept zusammengekratzt, der vielversprechend aussieht. Ich werde versuchen, es diese Woche richtig in der Pandas-Codebasis zu implementieren, um sicherzustellen, dass dieser Ansatz für alle DB-Varianten funktioniert.

Vorschlag für einen modifizierten Ansatz

Es gab einige gute Diskussionen über die API und wie ein Upsert eigentlich aufgerufen werden sollte (zB über das if_exists Argument oder über ein explizites upsert Argument). Dies wird demnächst geklärt. Im Moment ist dies der Pseudocode-Vorschlag dafür, wie die Funktionalität mit der SqlAlchemy upsert Anweisung funktionieren würde:

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
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen