Pandas: Añadiendo la opción (Insertar o actualizar si existe la clave) a `.to_sql`

Creado en 1 nov. 2016  ·  42Comentarios  ·  Fuente: pandas-dev/pandas

Suponga que tiene una tabla SQL existente llamada person_age , donde id es la clave principal:

    age
id  
1   18
2   42

y también tiene nuevos datos en un DataFrame llamado extra_data

    age
id  
2   44
3   95

entonces sería útil tener una opción en extra_data.to_sql() que permita pasar el DataFrame a SQL con una opción INSERT o UPDATE en las filas, basada en el primary key .

En este caso, la fila id=2 se actualizaría a age=44 y se agregaría la fila id=3

Rendimiento esperado

    age
id  
1   18
2   44
3   95

(Quizás) referencias de código útiles

Miré el código fuente de pandas sql.py para encontrar una solución, pero no pude seguir.

Código para replicar el ejemplo anterior

(Disculpas por mezclar sqlalchemy y 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

Comentario más útil

Si bien un INSERT OR UPDATE no es compatible con todos los motores, un INSERT OR REPLACE puede hacerse independiente del motor eliminando filas de la tabla de destino para el conjunto de claves primarias en el índice DataFrame seguido de una inserción de todas las filas del DataFrame. Querría hacer esto en una transacción.

Todos 42 comentarios

Esta sería una buena funcionalidad, pero el problema principal es que queremos que sea independiente del tipo de base de datos y que se base en el núcleo de sqlalchemy (por lo tanto, no en el ORM de sqlalchemy) para su inclusión en los pandas.
Lo que hará que esto sea difícil de implementar.

Sí, creo que esto está fuera del alcance de los pandas, ya que las actualizaciones no son compatibles con todos los motores de base de datos.

Si bien un INSERT OR UPDATE no es compatible con todos los motores, un INSERT OR REPLACE puede hacerse independiente del motor eliminando filas de la tabla de destino para el conjunto de claves primarias en el índice DataFrame seguido de una inserción de todas las filas del DataFrame. Querría hacer esto en una transacción.

@TomAugspurger ¿Podríamos agregar la opción upsert para los motores de base de datos compatibles y arrojar un error para los motores de base de datos no compatibles?

Me gustaría ver esto también. Estoy atrapado entre el uso de SQL puro y SQL Alchemy (aún no he logrado que esto funcione, creo que tiene algo que ver con la forma en que paso los dictados). Uso psycopg2 COPY para insertar masivamente, pero me encantaría usar pd.to_sql para tablas donde los valores pueden cambiar con el tiempo y no me importa que se inserte un poco más lento.

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

Y SQL puro:

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 este estilo funcionó para mí (insert_statement.excluded es un alias a la fila de datos que violó la restricción):

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 Este fragmento podría no funcionar en el caso de claves compuestas, ese escenario también debe ser atendido. Intentaré encontrar una manera de hacer lo mismo

Una forma de resolver este problema de actualización es utilizar bulk_update_mappings de sqlachemy . Esta función toma una lista de valores de diccionario y actualiza cada fila según la clave primaria de la tabla.

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

Estoy de acuerdo con @neilfrndes , no debería permitir que una característica agradable como esta no se implemente porque algunas bases de datos no son compatibles. ¿Existe alguna posibilidad de que ocurra esta característica?

Probablemente. si alguien hace un PR. En una consideración más profunda, no creo que me oponga a esto por el principio de que algunas bases de datos no lo admiten. Sin embargo, no estoy muy familiarizado con el código sql, por lo que no estoy seguro de cuál es el mejor enfoque.

Una posibilidad es proporcionar algunos ejemplos para upserts usando el method invocable si se introduce este PR: https://github.com/pandas-dev/pandas/pull/21401

Para postgres que se vería como (no probado):

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)

Se podría hacer algo similar para mysql .

Para postgres estoy usando execute_values. En mi caso, mi consulta es una plantilla jinja2 para marcar si debo actualizar el conjunto o no hacer nada . Esto ha sido bastante rápido y flexible. No es tan rápido como usar COPY o copy_expert pero funciona 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, ¿puedes, por favor, dar un ejemplo de cómo funcionaría esto?

Intenté echar un vistazo a bulk_update_mappings pero realmente me perdí y no pude hacerlo funcionar.

@ cristianionescu92 Un ejemplo sería este:
Tengo una tabla llamada Usuario con los siguientes campos: id y nombre.

| id | nombre |
| --- | --- |
| 0 | John |
| 1 | Joe |
| 2 | Harry |

Tengo un marco de datos de pandas con las mismas columnas pero valores actualizados:

| id | nombre |
| --- | --- |
| 0 | Chris |
| 1 | James |

Supongamos también que tenemos una variable de sesión abierta para acceder a la base de datos. Al llamar a este método:

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

Pandas convertirá la tabla en una lista de diccionarios [{id: 0, name: "chris"}, {id: 1, name: "james"}] que sql usará para actualizar las filas de la tabla. Entonces la mesa final se verá así:

| id | nombre |
| --- | --- |
| 0 | Chris |
| 1 | James |
| 2 | Harry |

Hola @ danich1 y muchas gracias por tu respuesta. Yo mismo descubrí la mecánica de cómo funcionaría la actualización. Desafortunadamente, no sé cómo trabajar con una sesión, soy bastante principiante.

Déjame mostrarte lo que estoy haciendo:

`importar pypyodbc
from to_sql_newrows import clean_df_db_dups, to_sql_newrows # estas son 2 funciones que encontré en GitHub, desafortunadamente no puedo recordar el enlace. Clean_df_db_dups excluye de un marco de datos las filas que ya existen en una tabla SQL al verificar varias columnas clave y to_sql_newrows es una función que inserta en sql las nuevas filas.

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

El código anterior básicamente excluye de un marco de datos las filas que ya tengo en SQL y solo inserta las nuevas filas. Lo que necesito es actualizar las filas que existen. ¿Puede, por favor, ayudarme a entender qué debo hacer a continuación?

Motivación para un mejor TO_SQL
to_sql Integrarse mejor con las prácticas de bases de datos es cada vez más valioso a medida que la ciencia de datos crece y se mezcla con la ingeniería de datos.

upsert es uno de ellos, en particular porque muchas personas encuentran que la solución es usar replace lugar, lo que elimina la tabla y, con ella, todas las vistas y restricciones.

La alternativa que he visto en usuarios más experimentados es dejar de usar pandas en esta etapa, y esto tiende a propagarse en sentido ascendente y hace que el paquete pandas pierda retención entre los usuarios experimentados. ¿Es esta la dirección en la que Pandas quiere ir?

Entiendo que queremos que to_sql permanezca independiente de la base de datos tanto como sea posible, y use la alquimia básica de SQL. Sin embargo, un método que trunca o elimina en lugar de un verdadero upsert aún agregaría mucho valor.

Integración con la visión del producto Pandas
Gran parte del debate anterior ocurrió antes de la introducción del argumento method (como lo menciona @kjford con psql_insert_copy ) y la posibilidad de pasar un invocable.

Con mucho gusto contribuiría a la funcionalidad principal de Pandas o, en su defecto, a la documentación sobre la solución / mejores prácticas sobre cómo lograr una funcionalidad de inserción dentro de Pandas, como la siguiente:
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method

¿Cuál es la forma preferida de avanzar para los gerentes de desarrollo / productos centrales de Pandas?

Creo que estamos abiertos a una implementación que sea específica del motor. La propuesta de usar method='upsert' parece razonable, pero en este punto creo que necesitamos a alguien que presente una propuesta de diseño clara.

Tengo un requisito similar en el que quiero actualizar los datos existentes en una tabla MySQL desde varios CSV a lo largo del tiempo.

Pensé que podría df.to_sql () para insertar los nuevos datos en una tabla temporal recién creada y luego ejecutar una consulta MySQL para controlar cómo agregar / actualizar datos en la tabla existente .

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

Descargo de responsabilidad: comencé a usar Python y Pandas hace solo unos días.

Hola gente de Pandas: He tenido el mismo problema, y ​​necesito actualizar con frecuencia mi base de datos local con registros que finalmente cargo y manipulo en pandas. Construí una biblioteca simple para hacer esto: es básicamente un sustituto de df.to_sql y pd.read_sql_table que usa el índice DataFrame como clave principal de forma predeterminada. Utiliza únicamente el núcleo de sqlalchemy.

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

Esta herramienta es bastante obstinada, probablemente no sea apropiada para incluirla en Pandas tal cual. Pero para mi caso de uso específico, resuelve el problema ... si hay interés en masajear esto para que encaje en Pandas, estoy feliz de ayudar.

Por ahora, los siguientes trabajos (en el caso limitado de los pandas actuales y sqlalchemy, denominado índice como clave principal, back-end de SQLite o Postgres y tipos de datos compatibles):

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

Trabajando en una solución general para esto con cvonsteg. Planeando volver con un diseño propuesto en octubre.

@TomAugspurger como se sugirió, @ rugg2 y yo hemos creado la siguiente propuesta de diseño para una opción upsert en to_sql() .

Propuesta de interfaz

2 nuevas variables que se añadirán como un posible method argumento en el to_sql() Método:
1) upsert_update - en la coincidencia de filas, actualice la fila en la base de datos (para actualizar registros a sabiendas - representa la mayoría de los casos de uso)
2) upsert_ignore - en la coincidencia de filas, no actualice la fila en la base de datos (para los casos en que los conjuntos de datos se superponen y no desea anular los datos en las tablas)

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

Propuesta de implementación

Para implementar esto, la clase SQLTable recibiría 2 nuevos métodos privados que contienen la lógica upsert, que se llamaría desde el método 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))

    ...

Proponemos la siguiente implementación, con la justificación descrita en detalle a continuación (todos los puntos están abiertos a discusión):

(1) Agnóstico del motor usando el núcleo de SQLAlchemy, a través de una secuencia atómica de DELETE y INSERT

  • Solo algunos dbms admiten de forma nativa upsert , y las implementaciones pueden variar según los sabores
  • Como primera implementación, creemos que sería más fácil probar y mantener una implementación en todos los dbms. En el futuro, si existe demanda, se pueden agregar implementaciones específicas del motor.
  • Por upsert_ignore estas operaciones obviamente se omitirían en los registros coincidentes
  • Valdrá la pena comparar una implementación independiente del motor frente a implementaciones específicas del motor en términos de rendimiento.

(2) Upsert solo en clave principal

  • Actualiza de forma predeterminada los conflictos de claves primarias a menos que se especifique lo contrario
  • Algunos DBMS permiten a los usuarios especificar columnas que no son de clave principal, contra las cuales verificar la unicidad. Si bien esto otorga al usuario más flexibilidad, conlleva posibles dificultades. Si estas columnas no tienen una restricción UNIQUE , entonces es plausible que varias filas coincidan con la condición upsert. En este caso, no se debe realizar ningún upsert ya que es ambiguo en cuanto a qué registro debe actualizarse. Para hacer cumplir esto desde los pandas, cada fila debería evaluarse individualmente para verificar que solo 1 o 0 filas coincidan, antes de que se inserte. Si bien esta funcionalidad es razonablemente sencilla de implementar, hace que cada registro requiera una operación de lectura y escritura (más una eliminación si se encuentra un conflicto de 1 registro), lo que se siente altamente ineficiente para conjuntos de datos más grandes.
  • En una mejora futura, si la comunidad lo requiere, podríamos agregar la funcionalidad para extender upsert no solo para trabajar en la clave principal, sino también en los campos especificados por el usuario. Esta es una pregunta a más largo plazo para el equipo de desarrollo central, en cuanto a si Pandas debería seguir siendo simple para proteger a los usuarios que tienen una base de datos mal diseñada o si tienen más funcionalidades.

@TomAugspurger , si la propuesta upsert diseñada con @cvonsteg le conviene, procederemos con la implementación en código (incluidas las pruebas) y generaremos una solicitud de extracción.

Háganos saber si desea proceder de otra manera.

Leer la propuesta está en mi lista de tareas pendientes. Estoy un poco atrasado en mi
correo electrónico ahora mismo.

El miércoles 9 de octubre de 2019 a las 9:18 a.m., Romain [email protected] escribió:

@TomAugspurger https://github.com/TomAugspurger , si el diseño
diseñado con @cvonsteg https://github.com/cvonsteg le conviene, lo haremos
continuar con la implementación en código (incluidas las pruebas) y generar un tirón
solicitud.

Háganos saber si desea proceder de otra manera.

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/pandas-dev/pandas/issues/14553?email_source=notifications&email_token=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVDVREXHG43V2TUL52HS4DFVDVREXWG43V
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.

Personalmente, no tengo nada en contra, así que creo que un PR es bienvenido. Una implementación en todos los DBM que usan el núcleo de SQLAlchemy es ciertamente cómo debería comenzar esto si estoy leyendo sus puntos correctamente, y lo mismo con las claves primarias.

Siempre es más fácil comenzar con algo pequeño y enfocado y expandirse a partir de ahí.

Necesito urgentemente esta característica.

Las relaciones públicas que escribimos con cvonsteg ahora deberían dar la funcionalidad: ¡hasta las revisiones ahora!

¡Esta funcionalidad sería absolutamente gloriosa! No conozco demasiado el vocabulario de github; ¿El comentario de @ rugg2 de que la funcionalidad está "ahora disponible para las revisiones" significa que

@ pmgh2345 : sí, como dijiste, "hasta las revisiones ahora" significa que se ha generado una solicitud de extracción y está siendo revisada por los desarrolladores principales. Puede ver el PR mencionado anteriormente (# 29636). Una vez que se aprueba, técnicamente podría bifurcar la rama con el código actualizado y compilar su propia versión local de pandas con la funcionalidad incorporada. Sin embargo, personalmente recomendaría esperar hasta que se haya fusionado con el maestro y lanzado, y luego simplemente instalar pip. la versión más nueva de pandas.

Las relaciones públicas que escribimos con cvonsteg ahora deberían dar la funcionalidad: ¡hasta las revisiones ahora!

Podría valer la pena agregar un nuevo parámetro al método to_sql , en lugar de usar if_exists . La razón es que if_exists está comprobando la existencia de la tabla, no de la fila.

@cvonsteg originalmente propuso usar method= , lo que evitaría la ambigüedad de tener dos significados para if_exists .

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

@brylie podríamos agregar un nuevo parámetro que sea cierto, pero como saben, cada nuevo parámetro hace que una API sea más torpe. Hay una compensación.

Si tenemos que elegir entre los parámetros actuales, como dijiste, inicialmente pensamos en usar el argumento method , pero después de pensarlo más, nos dimos cuenta de que tanto (1) el uso como (2) la lógica se ajusta mejor al if_exists argumento.

1) desde el punto de vista del uso de la API
El usuario querrá elegir tanto method = "multi" o None por un lado, y "upsert" por el otro. Sin embargo, no hay casos de uso igualmente sólidos con el uso de la funcionalidad "upsert" al mismo tiempo que if_exists = "append" o "replace", si corresponde.

2) desde un punto de vista lógico

  • El método funciona actualmente en _cómo_ se insertan los datos: fila por fila o "multi"
  • if_exists captura la lógica comercial de cómo administramos nuestros registros: "reemplazar", "agregar", "actualizar_actualizar" (agregar cuando existe una clave, agregar cuando es nueva), "insertar_ignore" (ignorar cuando existe una clave, agregar cuando es nueva). Aunque reemplazar y agregar están analizando la existencia de la tabla, también se puede entender en su impacto a nivel de registro.

¡Avíseme si entendí bien su punto y por favor grite si cree que la implementación actual bajo revisión (PR # 29636) sería netamente negativa!

Sí, entiendes mi punto. La implementación actual es netamente positiva pero levemente disminuida por la semántica ambigua.

Sigo manteniendo que if_exists debería seguir refiriéndose a una sola cosa, la existencia de la tabla. Tener ambigüedad en los parámetros impacta negativamente en la legibilidad y puede conducir a una lógica interna complicada. Considerando que, agregar un nuevo parámetro, como upsert=True es claro y explícito.

¡Hola!

Si desea ver una implementación no agnóstica para hacer upserts, tengo un ejemplo con mi biblioteca pangres . Maneja PostgreSQL y MySQL usando funciones sqlalchemy específicas para esos tipos de bases de datos. En cuanto a SQlite (y otros tipos de bases de datos que permiten una sintaxis similar de upsert), utiliza un inserto sqlalchemy regular compilado.

Comparto esta idea de que podría dar algunas ideas a los colaboradores (aunque soy consciente de que queremos que esto sea independiente del tipo SQL, lo que tiene mucho sentido). También quizás una comparación de velocidad también sería interesante cuando el PR de @cvonsteg pasa.
¡Tenga en cuenta que no soy un experto en sqlalchemy desde hace mucho tiempo o algo así!

Realmente quiero esta característica. Estoy de acuerdo en que method='upsert_update' es una buena idea.

¿Está esto todavía planeado? Los pandas realmente necesitan esta función

Sí, esto todavía está planeado, ¡y ya casi llegamos!

El código está escrito, pero hay una prueba que no pasa. ¡Ayuda bienvenida!
https://github.com/pandas-dev/pandas/pull/29636

El martes 5 de mayo de 2020 a las 19:18, Leonel Atencio [email protected] escribió:

¿Está esto todavía planeado? Los pandas realmente necesitan esta función

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.

¡Hola! ¿Está lista la funcionalidad o aún falta algo? Si aún falta algo, ¡avíseme si puedo ayudar con algo!

¿Hay noticias?))

Viniendo del mundo de Java, nunca pensé que esta simple funcionalidad podría cambiar mi base de código al revés.

Hola a todos,

He investigado cómo se implementan los upserts en SQL en todos los dialectos y encontré una serie de técnicas que pueden informar las decisiones de diseño aquí. Pero primero, quiero advertir contra el uso de la lógica DELETE ... INSERT. Si hay claves foráneas o disparadores, otros registros en la base de datos terminarán siendo eliminados o estropeados. En MySQL, REPLACE hace el mismo daño. De hecho, he creado horas de trabajo para mí mismo arreglando datos porque usé REPLACE. Entonces, dicho esto, aquí están las técnicas implementadas en SQL:

Dialecto | Técnica
- | -
MySQL | INSERTAR ... EN LA ACTUALIZACIÓN DE CLAVE DUPLICADA
PostgreSQL | INSERTAR ... EN CONFLICTO
SQLite | INSERTAR ... EN CONFLICTO
Db2 | UNIR
SQL Server | UNIR
Oracle | UNIR
SQL: 2016 | UNIR

Con una sintaxis muy variable, entiendo la tentación de usar DELETE ... INSERT para hacer que el dialecto de implementación sea agnóstico. Pero hay otra forma: podemos imitar la lógica de la declaración MERGE usando una tabla temporal y declaraciones básicas INSERT y UPDATE. La sintaxis de SQL: 2016 MERGE es la siguiente:

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

Tomado prestado del tutorial de Oracle
y ajustado para cumplir con SQL Wikibook

Dado que todos los dialectos admitidos por SQLAlchemy admiten tablas temporales, un enfoque más seguro e independiente del dialecto para realizar una inserción sería, en una sola transacción:

  1. Crea una tabla temporal.
  2. Inserta los datos en esa tabla temporal.
  3. Haz una ACTUALIZACIÓN ... ÚNETE.
  4. INSÉRTESE donde la clave (PRIMARIO o ÚNICO) no coincida.
  5. Suelta la tabla temporal.

Además de ser una técnica agnóstica del dialecto, también tiene la ventaja de ser ampliada al permitir que el usuario final elija cómo insertar o cómo actualizar los datos, así como en qué clave unir los datos.

Si bien la sintaxis de las tablas temporales y las combinaciones de actualización pueden diferir ligeramente entre los dialectos, deberían ser compatibles en todas partes.

A continuación se muestra una prueba de concepto que escribí para 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}`;')

Aquí, hago las siguientes suposiciones:

  1. La estructura de su origen y destino es la misma.
  2. Que desea hacer inserciones simples utilizando los datos en su marco de datos.
  3. Que desea simplemente actualizar todas las columnas que no son clave con los datos de su marco de datos.
  4. Que no desea realizar ningún cambio en los datos de las columnas clave.

A pesar de las suposiciones, espero que mi técnica inspirada en MERGE informe los esfuerzos para construir una opción de inserción flexible y sólida.

Creo que esta es una funcionalidad útil, sin embargo, parece fuera de alcance, ya que es intuitivo tener una característica tan común al agregar filas a una tabla.

Piense nuevamente en agregar esta función: es muy útil agregar filas a una tabla existente.
Alas Pangres está limitado a Python 3.7+. Como en mi caso (me veo obligado a usar un antiguo Python 3.4), no siempre es una solución viable.

Gracias, @GoldstHa , esa es una entrada realmente útil. Intentaré crear un POC para la implementación similar a MERGE

Dados los problemas con el enfoque DELETE/INSERT y el posible bloqueador en el enfoque @GoldstHa MERGE en bases de datos MySQL, he investigado un poco más. Hice una prueba de concepto utilizando la funcionalidad de actualización sqlalchemy , que parece prometedora. Intentaré implementarlo correctamente esta semana en el código base de Pandas, asegurándome de que este enfoque funcione en todos los tipos de bases de datos.

Propuesta de enfoque modificado

Ha habido algunas buenas discusiones en torno a la API y cómo se debería llamar realmente a un upsert (es decir, a través del argumento if_exists , o mediante un argumento upsert explícito). Esto se aclarará pronto. Por ahora, esta es la propuesta de pseudocódigo de cómo funcionaría la funcionalidad usando la declaración 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
¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

amelio-vazquez-reina picture amelio-vazquez-reina  ·  3Comentarios

songololo picture songololo  ·  3Comentarios

scls19fr picture scls19fr  ·  3Comentarios

matthiasroder picture matthiasroder  ·  3Comentarios

ebran picture ebran  ·  3Comentarios