Pandas: Adicionando (inserir ou atualizar se houver chave) opção para `.to_sql`

Criado em 1 nov. 2016  ·  42Comentários  ·  Fonte: pandas-dev/pandas

Suponha que você tenha uma tabela SQL existente chamada person_age , onde id é a chave primária:

    age
id  
1   18
2   42

e você também tem novos dados em um DataFrame chamado extra_data

    age
id  
2   44
3   95

então seria útil ter uma opção em extra_data.to_sql() que permite passar o DataFrame para SQL com uma opção INSERT ou UPDATE nas linhas, com base em primary key .

Neste caso, a linha id=2 seria atualizada para age=44 e a linha id=3 seria adicionada

Saída Esperada

    age
id  
1   18
2   44
3   95

(Talvez) referências de código úteis

Eu olhei para pandas sql.py código fonte para encontrar uma solução, mas não consegui entender.

Código para replicar o exemplo acima

(Desculpas por misturar sqlalchemy e 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

Comentários muito úteis

Enquanto INSERT OR UPDATE não é suportado por todos os mecanismos, um INSERT OR REPLACE pode se tornar agnóstico do mecanismo excluindo linhas da tabela de destino para o conjunto de chaves primárias no índice DataFrame seguido por uma inserção de todas as linhas no DataFrame. Você gostaria de fazer isso em uma transação.

Todos 42 comentários

Esta seria uma boa funcionalidade, mas o principal problema é que queremos que ela seja independente do tipo de banco de dados e baseada no núcleo sqlalchemy (então não sqlalchemy ORM) para inclusão no próprio pandas.
O que tornará isso difícil de implementar.

Sim, eu acho que isso está fora do escopo para pandas, já que upserts não são suportados por todos os motores de banco de dados.

Enquanto INSERT OR UPDATE não é suportado por todos os mecanismos, um INSERT OR REPLACE pode se tornar agnóstico do mecanismo excluindo linhas da tabela de destino para o conjunto de chaves primárias no índice DataFrame seguido por uma inserção de todas as linhas no DataFrame. Você gostaria de fazer isso em uma transação.

@TomAugspurger Podemos adicionar a opção upsert para motores db suportados e lançar um erro para motores db não suportados?

Eu gostaria de ver isso também. Estou preso entre o uso de SQL puro e SQL Alchemy (ainda não fiz isso funcionar, acho que tem algo a ver com a forma como passo os ditos). Eu uso psycopg2 COPY para inserção em massa, mas adoraria usar pd.to_sql para tabelas em que os valores podem mudar com o tempo e não me importo em inserir um pouco mais devagar.

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

E 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 funcionou para mim (insert_statement.excluded é um alias para a linha de dados que violou a restrição):

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 snippet pode não funcionar no caso de chaves compostas, esse cenário também deve ser cuidado. Vou tentar encontrar uma maneira de fazer o mesmo

Uma maneira de resolver esse problema de atualização é usar bulk_update_mappings do sqlachemy . Esta função obtém uma lista de valores de dicionário e atualiza cada linha com base na chave primária da tabela.

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

Eu concordo com @neilfrndes , não deveria permitir que um bom recurso como este não seja implementado porque alguns bancos de dados não suportam. Existe alguma chance desse recurso acontecer?

Provavelmente. se alguém fizer um PR. Pensando melhor, não acho que me oponho a isso com base no princípio de que alguns bancos de dados não oferecem suporte. No entanto, não estou muito familiarizado com o código sql, então não tenho certeza de qual é a melhor abordagem.

Uma possibilidade é fornecer alguns exemplos de upserts usando method callable se este PR for introduzido: https://github.com/pandas-dev/pandas/pull/21401

Para postgres que seria algo como (não testado):

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)

Algo semelhante pode ser feito para o mysql .

Para postgres, estou usando execute_values. No meu caso, minha consulta é um modelo jinja2 para sinalizar se devo atualizar o conjunto ou não fazer nada . Isso foi bastante rápido e flexível. Não é tão rápido quanto usar COPY ou copy_expert, mas funciona bem.

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 você pode, por favor, dar um exemplo de como isso funcionaria?

Tentei dar uma olhada em bulk_update_mappings, mas me perdi muito e não consegui fazer funcionar.

@ cristianionescu92 Um exemplo seria este:
Tenho uma tabela chamada Usuário com os seguintes campos: id e nome.

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

Eu tenho um quadro de dados do pandas com as mesmas colunas, mas com valores atualizados:

| id | nome |
| --- | --- |
| 0 | Chris |
| 1 James |

Vamos supor também que temos uma variável de sessão aberta para acessar o banco de dados. Chamando este método:

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

O Pandas converterá a tabela em uma lista de dicionários [{id: 0, name: "chris"}, {id: 1, name: "james"}] que o sql usará para atualizar as linhas da tabela. Portanto, a mesa final será semelhante a:

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

Olá, @ danich1 e muito obrigado pela sua resposta. Eu descobri por mim mesmo a mecânica de como a atualização funcionaria. Infelizmente não sei trabalhar como trabalhar com uma sessão, sou bastante iniciante.

Deixe-me mostrar o que estou fazendo:

`import pypyodbc
from to_sql_newrows import clean_df_db_dups, to_sql_newrows # estas são 2 funções que encontrei no GitHub, infelizmente não consigo me lembrar do link. Clean_df_db_dups exclui de um dataframe as linhas que já existem em uma tabela SQL, verificando várias colunas-chave e to_sql_newrows é uma função que insere no sql as novas linhas.

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

O código acima está basicamente excluindo de um dataframe as linhas que eu já tenho no SQL e apenas insere as novas linhas. O que eu preciso é atualizar as linhas que existem. Você pode, por favor, me ajudar a entender o que devo fazer a seguir?

Motivação para um TO_SQL melhor
to_sql integrar-se melhor com as práticas de banco de dados é cada vez mais valioso à medida que a ciência de dados cresce e se mistura com a engenharia de dados.

upsert é um deles, em particular porque muitas pessoas acham que a solução é usar replace vez disso, o que elimina a tabela e, com ela, todas as visualizações e restrições.

A alternativa que tenho visto em usuários mais experientes é parar de usar o pandas neste estágio, e isso tende a se propagar no sentido ascendente e faz com que o pacote do pandas perca a retenção entre os usuários experientes. É esta a direção que o Pandas quer seguir?

Eu entendo que queremos que to_sql permaneça agnóstico de banco de dados tanto quanto possível e use alquimia sql central. Um método que trunca ou exclui em vez de um upsert verdadeiro ainda agregaria muito valor.

Integração com a visão do produto Pandas
Muito do debate acima aconteceu antes da introdução do argumento method (como mencionado por @kjford com psql_insert_copy ) e a possibilidade de passar um callable.

Eu ficaria feliz em contribuir com a funcionalidade central do pandas ou, na falta disso, com a documentação sobre a solução / prática recomendada sobre como obter uma funcionalidade upsert dentro do Pandas, como a seguir:
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method

Qual é o caminho preferido para os gerentes de desenvolvimento / produto centrais do Pandas?

Acho que estamos abertos a uma implementação específica do mecanismo. A proposta de usar method='upsert' parece razoável, mas neste ponto acho que precisamos de alguém para apresentar uma proposta de design clara.

Tenho um requisito semelhante em que desejo atualizar os dados existentes em uma tabela MySQL de vários CSVs ao longo do tempo.

Achei que poderia df.to_sql () para inserir os novos dados em uma tabela temporária recém-criada e, em seguida, executar uma consulta MySQL para controlar como anexar / atualizar dados na tabela existente .

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

Isenção de responsabilidade: comecei a usar Python e Pandas há apenas alguns dias.

Olá, pessoal do Pandas: Eu tive esse mesmo problema, precisando atualizar meu banco de dados local com frequência com registros que carrego e manipulo nos pandas. Eu construí uma biblioteca simples para fazer isso - é basicamente um substituto para df.to_sql e pd.read_sql_table que usa o índice DataFrame como uma chave primária por padrão. Usa apenas o núcleo sqlalchemy.

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

Esta ferramenta é bastante opinativa, provavelmente não apropriada para incluir no Pandas como está. Mas para o meu caso de uso específico ele resolve o problema ... se houver interesse em massagear isso para que se encaixe no Pandas, fico feliz em ajudar.

Por enquanto, o seguinte funciona (no caso limitado de pandas atuais e sqlalchemy, índice nomeado como chave primária, back end SQLite ou Postgres e tipos de dados suportados):

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

Trabalhando em uma solução geral para isso com cvonsteg. Planejando voltar com uma proposta de design em outubro.

@TomAugspurger conforme sugerido, @ rugg2 e eu upsert em to_sql() .

Proposta de Interface

2 novas variáveis a ser adicionado como um possível method argumento no to_sql() método:
1) upsert_update - correspondência de linha, linha de atualização no banco de dados (para atualizar registros conscientemente - representa a maioria dos casos de uso)
2) upsert_ignore - na correspondência de linha, não atualize a linha no banco de dados (para os casos em que os conjuntos de dados se sobrepõem e você não deseja substituir os dados nas tabelas)

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

Proposta de Implementação

Para implementar isso, a classe SQLTable receberia 2 novos métodos privados contendo a lógica upsert, que seria chamada a partir do 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))

    ...

Propomos a seguinte implementação, com os fundamentos descritos em detalhes abaixo (todos os pontos estão abertos para discussão):

(1) Motor agnóstico usando núcleo SQLAlchemy, por meio de uma sequência atômica de DELETE e INSERT

  • Apenas alguns dbms suportam nativamente upsert , e as implementações podem variar entre os sabores
  • Como primeira implementação, acreditamos que seria mais fácil testar e manter uma implementação em todos os dbms. No futuro, se houver demanda, implementações específicas do mecanismo podem ser adicionadas.
  • Para upsert_ignore essas operações seriam obviamente ignoradas nos registros correspondentes
  • Vale a pena comparar uma implementação independente de mecanismo com implementações específicas de mecanismo em termos de desempenho.

(2) Upsert apenas na chave primária

  • Upserts padrão para conflitos de chave primária, a menos que seja especificado de outra forma
  • Alguns DBMS permitem que os usuários especifiquem colunas de chave não primária, nas quais verificar a exclusividade. Embora isso conceda ao usuário mais flexibilidade, apresenta potenciais armadilhas. Se essas colunas não tiverem uma restrição UNIQUE , então é plausível que várias linhas possam corresponder à condição upsert. Nesse caso, nenhum upsert deve ser executado, pois é ambíguo quanto ao registro que deve ser atualizado. Para impor isso aos pandas, cada linha precisaria ser avaliada individualmente para verificar se apenas 1 ou 0 linhas correspondem, antes de ser inserida. Embora essa funcionalidade seja razoavelmente simples de implementar, ela resulta em cada registro exigindo uma operação de leitura e gravação (mais uma exclusão se for encontrado um conflito de 1 registro), o que parece altamente ineficiente para conjuntos de dados maiores.
  • Em uma melhoria futura, se a comunidade solicitar, poderíamos adicionar a funcionalidade para estender o upsert para funcionar não apenas na chave primária, mas também em campos especificados pelo usuário. Esta é uma questão de longo prazo para a equipe de desenvolvimento principal, se o Pandas deve permanecer simples para proteger os usuários que têm um banco de dados mal projetado ou têm mais funcionalidades.

@TomAugspurger , se a proposta upsert projetada com @cvonsteg for adequada para você, prosseguiremos com a implementação em código (incluindo testes) e levantaremos uma solicitação pull.

Informe-nos se deseja proceder de forma diferente.

Ler a proposta está na minha lista de tarefas. Estou um pouco atrasado no meu
e-mail agora.

Na quarta-feira, 9 de outubro de 2019 às 9h18, Romain [email protected] escreveu:

@TomAugspurger https://github.com/TomAugspurger , se o design nós
projetado com @cvonsteg https://github.com/cvonsteg combina com você, nós iremos
prossiga com a implementação em código (incluindo testes) e gere um puxão
solicitar.

Informe-nos se deseja proceder de forma diferente.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/pandas-dev/pandas/issues/14553?email_source=notifications&email_token=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAYBJ7A#issuecomment-540022012 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.

Eu pessoalmente não tenho nada contra isso, então acho que um PR é bem-vindo. Uma implementação em todos os DBMs usando o núcleo SQLAlchemy é certamente como isso deve começar se eu estiver lendo seus pontos corretamente, e o mesmo apenas com as chaves primárias.

Sempre mais fácil começar pequeno e focado e expandir a partir daí

preciso muito desse recurso.

PR que escrevemos com cvonsteg agora deve fornecer a funcionalidade: até os comentários agora!

Essa funcionalidade seria absolutamente gloriosa! Não sou muito versado no vocabulário do github; o comentário de @ rugg2 de que a funcionalidade está "reduzida a análises agora" significa que cabe à equipe do pandas analisá-la? E se for aprovado, isso significa que estará disponível por meio de uma nova versão do pandas que podemos instalar, ou seríamos obrigados a aplicar o commit manualmente via git? (Tive problemas com isso por meio do conda, então, se for esse o caso, gostaria de me atualizar quando esta funcionalidade estiver pronta). Obrigado!!

@ pmgh2345 - sim, como você disse, "até as revisões agora" significa que uma solicitação de pull foi levantada e está sendo revisada pelos desenvolvedores principais. Você pode ver o PR mencionado acima (# 29636). Depois de aprovado, você poderia tecnicamente bifurcar o branch com o código atualizado e compilar sua própria versão local do pandas com a funcionalidade incorporada. No entanto, eu pessoalmente recomendo esperar até que ele seja mesclado no master e lançado, e então apenas instalando pip a versão mais recente dos pandas.

PR que escrevemos com cvonsteg agora deve fornecer a funcionalidade: até os comentários agora!

Pode valer a pena adicionar um novo parâmetro ao método to_sql , em vez de usar if_exists . O motivo é que if_exists está verificando a existência de uma tabela, não de uma linha.

@cvonsteg originalmente propôs usar method= , o que evitaria a ambigüidade de ter dois significados para if_exists .

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

@brylie poderíamos adicionar um novo parâmetro verdadeiro, mas como você sabe, cada novo parâmetro torna a API mais desajeitada. Existe uma troca.

Se tivermos que escolher entre os parâmetros atuais, como você disse, pensamos inicialmente em usar o argumento method , mas depois de mais pensamentos, percebemos que (1) o uso e (2) a lógica se ajusta melhor ao if_exists argumento.

1) do ponto de vista do uso da API
O usuário vai querer escolher method = "multi" ou None por um lado e "upsert" por outro. No entanto, não há casos de uso equivalentes fortes com o uso da funcionalidade "upsert" ao mesmo tempo que if_exists = "append" ou "replace", se houver.

2) do ponto de vista lógico

  • método atualmente funciona em _como_ os dados estão sendo inseridos: linha por linha ou "multi"
  • if_exists captura a lógica de negócios de como gerenciamos nossos registros: "substituir", "anexar", "upsert_update" (upsert quando a chave existe, anexar quando novo), "upsert_ignore" (ignorar quando a chave existe, anexar quando novo). Embora replace e append examinem a existência da tabela, ele também pode ser compreendido em seu impacto no nível de registro.

Deixe-me saber se entendi bem o seu ponto e grite se você acha que a implementação atual em análise (PR # 29636) seria um resultado líquido negativo!

Sim, você entende meu ponto. A implementação atual é uma rede positiva, mas ligeiramente diminuída por uma semântica ambígua.

Ainda mantenho que if_exists deve continuar a referir-se a apenas uma coisa, a existência de mesa. Ter ambiguidade nos parâmetros afeta negativamente a legibilidade e pode levar a uma lógica interna complicada. Por outro lado, adicionar um novo parâmetro, como upsert=True é claro e explícito.

Olá!

Se você quiser ver uma implementação não agnóstica para fazer upserts, tenho um exemplo com os painéis da minha biblioteca. Ele lida com PostgreSQL e MySQL usando funções sqlalchemy específicas para esses tipos de banco de dados. Quanto ao SQlite (e outros tipos de bancos de dados que permitem uma sintaxe upsert semelhante), ele usa um Insert sqlalchemy regular compilado.

Eu compartilho este pensamento que pode dar algumas idéias aos colaboradores (estou ciente, porém, que queremos que isso seja agnóstico do tipo SQL, o que faz muito sentido). Talvez uma comparação de velocidade também seja interessante quando o PR do @cvonsteg for
Lembre-se de que não sou um especialista em sqlalchemy há muito tempo ou algo parecido!

Eu realmente quero esse recurso. Eu concordo que method='upsert_update' é uma boa ideia.

Ainda está planejado? Os pandas realmente precisam desse recurso

Sim, isso ainda está planejado e estamos quase lá!

O código está escrito, mas há um teste que não passa. Ajuda bem-vinda!
https://github.com/pandas-dev/pandas/pull/29636

Na terça-feira, 5 de maio de 2020, 19:18, Leonel Atencio [email protected] escreveu:

Ainda está planejado? Os pandas realmente precisam desse recurso

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.

Olá! A funcionalidade está pronta ou ainda falta alguma coisa? Se algo ainda estiver faltando, entre em contato se eu puder ajudar em alguma coisa!

Qualquer notícia?))

Vindo do mundo Java, nunca pensei que essa funcionalidade simples pudesse virar minha base de código de cabeça para baixo.

Olá a todos,

Eu examinei como os upserts são implementados em SQL entre dialetos e encontrei várias técnicas que podem informar as decisões de design aqui. Mas, primeiro, quero alertar contra o uso da lógica DELETE ... INSERT. Se houver chaves estrangeiras ou gatilhos, outros registros no banco de dados acabarão sendo excluídos ou bagunçados. No MySQL, REPLACE causa o mesmo dano. Na verdade, criei horas de trabalho para mim mesmo corrigindo dados porque usei REPLACE. Dito isso, aqui estão as técnicas implementadas em SQL:

Dialeto | Técnica
- | -
MySQL | INSERT ... ON DUPLICATE KEY UPDATE
PostgreSQL | INSERIR ... NO CONFLITO
SQLite | INSERIR ... NO CONFLITO
Db2 | MERGE
SQL Server | MERGE
Oracle | MERGE
SQL: 2016 | MERGE

Com uma sintaxe extremamente variável, eu entendo a tentação de usar DELETE ... INSERT para tornar o dialeto de implementação agnóstico. Mas há outra maneira: podemos imitar a lógica da instrução MERGE usando uma tabela temporária e instruções básicas INSERT e UPDATE. A sintaxe SQL: 2016 MERGE é a seguinte:

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

Emprestado do Tutorial Oracle
e ajustado para se adequar ao SQL Wikibook

Uma vez que cada dialeto compatível com SQLAlchemy oferece suporte a tabelas temporárias, uma abordagem mais segura e agnóstica de dialeto para fazer um upsert seria, em uma única transação:

  1. Crie uma tabela temporária.
  2. Insira os dados nessa tabela temporária.
  3. Faça um UPDATE ... JOIN.
  4. INSERT onde a chave (PRIMARY ou UNIQUE) não corresponde.
  5. Largue a mesa temporária.

Além de ser uma técnica agnóstica de dialeto, também tem a vantagem de ser expandida, permitindo ao usuário final escolher como inserir ou atualizar os dados, bem como em qual chave juntar os dados.

Embora a sintaxe das tabelas temporárias e das junções de atualização possam ser ligeiramente diferentes entre os dialetos, elas devem ser suportadas em todos os lugares.

Abaixo está uma prova de conceito que escrevi para o 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}`;')

Aqui, faço as seguintes suposições:

  1. A estrutura de sua origem e destino são as mesmas.
  2. Que você deseja fazer inserções simples usando os dados em seu dataframe.
  3. Que você deseja simplesmente atualizar todas as colunas não-chave com os dados do seu dataframe.
  4. Que você não deseja fazer nenhuma alteração nos dados das colunas principais.

Apesar das suposições, espero que minha técnica inspirada em MERGE informe os esforços para construir uma opção de upsert robusta e flexível.

Acho que é uma funcionalidade útil, mas parece que está fora do escopo, pois é intuitivo ter um recurso tão comum ao adicionar linhas a uma tabela.

Pense novamente em adicionar esta função: é muito útil adicionar linhas a uma tabela existente.
Alas Pangres está limitado ao Python 3.7+. Como no meu caso (sou forçado a usar um antigo Python 3.4), nem sempre é uma solução viável.

Obrigado, @GoldstHa - essa é uma contribuição muito útil. Vou tentar criar um POC para a implementação do tipo MERGE

Dados os problemas com a abordagem DELETE/INSERT e o potencial bloqueador na abordagem @GoldstHa MERGE em bancos de dados MySQL, fiz um pouco mais de pesquisa. Eu fiz uma prova de conceito usando a funcionalidade de atualização sqlalchemy , que parece promissora. Tentarei implementá-lo adequadamente esta semana na base de código do Pandas, garantindo que essa abordagem funcione em todos os tipos de banco de dados.

Proposta de abordagem modificada

Tem havido algumas boas discussões sobre a API e como um upsert deve realmente ser chamado (ou seja, por meio do argumento if_exists ou por meio de um argumento upsert explícito). Isso será esclarecido em breve. Por enquanto, esta é a proposta de pseudocódigo de como a funcionalidade funcionaria usando a instrução 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
Esta página foi útil?
0 / 5 - 0 avaliações