Pandas: Добавление (Вставить или обновить, если ключ существует) параметр в `.to_sql`

Созданный на 1 нояб. 2016  ·  42Комментарии  ·  Источник: pandas-dev/pandas

Предположим, у вас есть существующая таблица SQL с именем person_age , где id - это первичный ключ:

    age
id  
1   18
2   42

и у вас также есть новые данные в DataFrame под названием extra_data

    age
id  
2   44
3   95

тогда было бы полезно иметь параметр в extra_data.to_sql() который позволяет передавать DataFrame в SQL с параметром INSERT или UPDATE для строк на основе primary key .

В этом случае строка id=2 будет обновлена ​​до age=44 а строка id=3 будет добавлена

Ожидаемый результат

    age
id  
1   18
2   44
3   95

(Возможно) полезные ссылки на код

Я посмотрел на pandas sql.py исходный код, чтобы придумать решение, но не смог уследить.

Код для воспроизведения примера выше

(Приносим извинения за смешивание sqlalchemy и 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

Самый полезный комментарий

Хотя INSERT OR UPDATE поддерживается не всеми движками, INSERT OR REPLACE можно сделать независимым от движка, удалив строки из целевой таблицы для набора первичных ключей в индексе DataFrame с последующей вставкой все строки в DataFrame. Вы хотите сделать это в транзакции.

Все 42 Комментарий

Это была бы хорошая функциональность, но основная проблема в том, что мы хотим, чтобы она была независимой от базы данных и основывалась на ядре sqlalchemy (а не на ORM sqlalchemy) для включения в сам pandas.
Что сделает это трудным для реализации.

Да, я думаю, что это выходит за рамки возможностей pandas, поскольку upserts не поддерживаются всеми механизмами db.

Хотя INSERT OR UPDATE поддерживается не всеми движками, INSERT OR REPLACE можно сделать независимым от движка, удалив строки из целевой таблицы для набора первичных ключей в индексе DataFrame с последующей вставкой все строки в DataFrame. Вы хотите сделать это в транзакции.

@TomAugspurger Можно ли добавить опцию upsert для поддерживаемых механизмов БД и

Я тоже хочу это увидеть. Я застрял между использованием чистого SQL и SQL Alchemy (еще не получил, чтобы это работало, я думаю, что это как-то связано с тем, как я передаю dicts). Я использую psycopg2 COPY для массовой вставки, но мне бы хотелось использовать pd.to_sql для таблиц, где значения могут изменяться со временем, и я не возражаю, чтобы вставка была немного медленнее.

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

И чистый 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 этот стиль у меня сработал (insert_statement.excluded - это псевдоним для строки данных, которая нарушила ограничение):

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 Этот фрагмент может не работать в случае составных ключей, и об этом сценарии также следует позаботиться. Я постараюсь найти способ сделать то же самое

Один из способов решить эту проблему с обновлением - использовать sqlachemy bulk_update_mappings . Эта функция принимает список значений словаря и обновляет каждую строку на основе первичного ключа таблицы.

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

Я согласен с @neilfrndes , не следует допускать, чтобы такая приятная функция не была реализована, потому что некоторые БД не поддерживают. Есть ли шанс, что эта функция может появиться?

Наверное. если кто-то делает пиар. При дальнейшем рассмотрении, я не думаю, что я против этого по принципу, что некоторые базы данных не поддерживают его. Однако я не слишком хорошо знаком с кодом sql, поэтому не уверен, что лучше всего подходит.

Одна из возможностей - предоставить несколько примеров для апсертов с использованием вызываемого method если введен этот PR: https://github.com/pandas-dev/pandas/pull/21401

Для postgres это будет выглядеть примерно так (непроверено):

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)

Что-то подобное можно было бы сделать для mysql .

Для postgres я использую execute_values. В моем случае мой запрос представляет собой шаблон jinja2, чтобы указать, следует ли мне выполнять набор обновлений или ничего не делать . Это было довольно быстро и гибко. Не так быстро, как при использовании COPY или copy_expert, но работает хорошо.

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 , пожалуйста,

Я попытался заглянуть в bulk_update_mappings, но действительно потерялся и не смог заставить его работать.

@ cristianionescu92 Вот пример:
У меня есть таблица под названием User со следующими полями: id и name.

| id | имя |
| --- | --- |
| 0 | Джон |
| 1 | Джо |
| 2 | Гарри |

У меня есть фрейм данных pandas с теми же столбцами, но обновленными значениями:

| id | имя |
| --- | --- |
| 0 | Крис |
| 1 | Джеймс |

Также предположим, что у нас есть переменная сеанса, открытая для доступа к базе данных. Вызывая этот метод:

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

Pandas преобразует таблицу в список словарей [{id: 0, name: "chris"}, {id: 1, name: "james"}], которые sql будет использовать для обновления строк таблицы. Итоговая таблица будет выглядеть так:

| id | имя |
| --- | --- |
| 0 | Крис |
| 1 | Джеймс |
| 2 | Гарри |

Привет, @ danich1 ,

Позвольте мне показать вам, что я делаю:

`импорт pypyodbc
from to_sql_newrows import clean_df_db_dups, to_sql_newrows # это 2 функции, которые я нашел на GitHub, к сожалению, я не могу вспомнить ссылку. Clean_df_db_dups исключает из фрейма данных строки, которые уже существуют в таблице SQL, путем проверки нескольких ключевых столбцов, а to_sql_newrows - это функция, которая вставляет в sql новые строки.

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

Приведенный выше код в основном исключает из фрейма данных строки, которые у меня уже есть в SQL, и вставляет только новые строки. Мне нужно обновить существующие строки. Не могли бы вы помочь мне понять, что мне делать дальше?

Мотивация к лучшему TO_SQL
to_sql Лучшая интеграция с методами работы с базами данных приобретает все большую ценность по мере того, как наука о данных растет и смешивается с инженерией данных.

upsert - один из них, в частности потому, что многие люди считают, что вместо этого нужно использовать replace , который удаляет таблицу, а вместе с ней и все представления и ограничения.

Альтернатива, которую я видел у более опытных пользователей, - это прекратить использовать pandas на этом этапе, и это имеет тенденцию распространяться вверх по течению и ослабляет удержание пакета pandas среди опытных пользователей. Это то направление, в котором хочет идти Панда?

Я понимаю, что мы хотим, чтобы to_sql оставался максимально независимым от базы данных и использовал базовую алхимию sql. Однако метод, который усекает или удаляет вместо истинного upsert, все равно добавляет большую ценность.

Интеграция с видением продукта Pandas
Многие из вышеперечисленных дебатов происходили до введения аргумента method (как упоминалось @kjford с psql_insert_copy ) и возможности передать вызываемый объект.

Я с радостью внесу свой вклад либо в основную функциональность pandas, либо, в противном случае, документацию по решению / передовой практике о том, как достичь функциональности upsert в Pandas, например, ниже:
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method

Какой путь вперед предпочитают основные разработчики / менеджеры по продукту Pandas?

Я думаю, что мы открыты для реализации, специфичной для движка. Предложение использовать method='upsert' кажется разумным, но на данный момент я думаю, что нам нужен кто-то, кто придет с четким предложением по дизайну.

У меня есть аналогичное требование, когда я хочу обновлять существующие данные в таблице MySQL из нескольких CSV с течением времени.

Я думал, что могу df.to_sql () вставить новые данные во вновь созданную временную таблицу, а затем запустить запрос MySQL, чтобы контролировать, как добавлять / обновлять данные в существующей таблице .

Ссылка на MySQL: https://stackoverflow.com/questions/2472229/insert-into-select-from-on-duplicate-key-update?answertab=active#tab -top

Отказ от ответственности: я начал использовать Python и Pandas всего несколько дней назад.

Привет, панда: у меня была такая же проблема, когда мне приходилось часто обновлять мою локальную базу данных записями, которые я в конечном итоге загружаю и обрабатываю в пандах. Для этого я создал простую библиотеку - по сути, она заменяет df.to_sql и pd.read_sql_table, которая по умолчанию использует индекс DataFrame в качестве первичного ключа. Использует только ядро ​​sqlalchemy.

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

Этот инструмент довольно самоуверенный, вероятно, не подходит для включения в Pandas как есть. Но для моего конкретного случая использования он решает проблему ... если есть интерес в том, чтобы массировать это, чтобы оно соответствовало Pandas, я рад помочь.

На данный момент работает следующее (в ограниченном случае текущих pandas и sqlalchemy, именованный индекс в качестве первичного ключа, серверная часть SQLite или Postgres и поддерживаемые типы данных):

pip install pandabase / pandabase.to_sql (df, имя_таблицы, con_string, how = 'upsert')

Работая над общим решением этой проблемы с помощью cvonsteg. Планирую вернуться с предложенным дизайном в октябре.

@TomAugspurger, как было предложено, мы с @rugg2 сделали следующее предложение по дизайну для опции upsert в to_sql() .

Предложение интерфейса

2 новые переменные, которые будут добавлены в качестве возможного аргумента method в метод to_sql() :
1) upsert_update - при совпадении строки, обновить строку в базе данных (для сознательного обновления записей - представляет большинство случаев использования)
2) upsert_ignore - при совпадении строки, не обновлять строку в базе данных (для случаев, когда наборы данных перекрываются, и вы не хотите переопределять данные в таблицах)

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

Предложение по реализации

Чтобы реализовать это, класс SQLTable получит 2 новых закрытых метода, содержащих логику обновления, которые будут вызываться из метода 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))

    ...

Мы предлагаем следующую реализацию, с обоснованием, подробно изложенным ниже (все вопросы открыты для обсуждения):

(1) Агностик двигателя с использованием ядра SQLAlchemy через атомарную последовательность DELETE и INSERT

  • Только некоторые СУБД изначально поддерживают upsert , и реализации могут различаться в зависимости от вкусов.
  • В качестве первой реализации мы считаем, что было бы проще тестировать и поддерживать одну реализацию для всех СУБД. В будущем, если будет потребность, могут быть добавлены реализации для конкретного движка.
  • Для upsert_ignore эти операции, очевидно, будут пропущены для совпадающих записей.
  • Стоит сравнить реализацию, не зависящую от движка, и реализации, зависящие от движка, с точки зрения производительности.

(2) Обновление только на первичном ключе

  • Восстанавливает по умолчанию на конфликты первичных ключей, если не указано иное
  • Некоторые СУБД позволяют пользователям указывать столбцы, не являющиеся первичными ключами, для проверки их уникальности. Хотя это дает пользователю большую гибкость, это связано с потенциальными подводными камнями. Если эти столбцы не имеют ограничения UNIQUE , то вполне вероятно, что несколько строк могут соответствовать условию upsert. В этом случае не следует выполнять обновление, так как неясно, какая запись должна быть обновлена. Чтобы добиться этого от pandas, каждая строка должна быть индивидуально оценена, чтобы проверить, совпадают ли только 1 или 0 строк, прежде чем она будет вставлена. Хотя эту функциональность довольно просто реализовать, она приводит к тому, что каждая запись требует операции чтения и записи (плюс удаление, если обнаружено одно противоречие), что кажется крайне неэффективным для больших наборов данных.
  • В будущем усовершенствовании, если сообщество потребует этого, мы могли бы добавить функциональность, чтобы расширить upsert, чтобы он работал не только с первичным ключом, но и с полями, указанными пользователем. Это более долгосрочный вопрос для команды разработчиков ядра: должны ли Pandas оставаться простыми для защиты пользователей с плохо спроектированной базой данных или иметь больше функций.

@TomAugspurger , если вам подходит предложение upsert разработанное с помощью @cvonsteg , мы продолжим реализацию в коде (включая тесты) и

Сообщите нам, если вы хотите действовать иначе.

Прочитать предложение в моем списке дел. Я немного отстал от своего
электронная почта прямо сейчас.

В среду, 9 октября 2019 г., в 9:18 Ромен [email protected] написал:

@TomAugspurger https://github.com/TomAugspurger , если дизайн мы
разработан с использованием @cvonsteg https://github.com/cvonsteg подходит вам, мы будем
продолжите реализацию в коде (включая тесты) и поднимите запрос
запрос.

Сообщите нам, если вы хотите действовать иначе.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/pandas-dev/pandas/issues/14553?email_source=notifications&email_token=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVREWWK3TUL52HS4DFMVWWK3TUL52HS4DFMVRWWWK3TUL52HS4DFMVRE
или отключить поток
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.

Лично я ничего не имею против, поэтому считаю, что пиар приветствуется. Одна реализация для всех DBM, использующих ядро ​​SQLAlchemy, безусловно, должна начинаться, если я правильно читаю ваши аргументы, и то же самое только с первичными ключами.

Всегда легче начать с малого и сфокусироваться, а потом расширяться

эта функция очень нужна.

PR, который мы писали с помощью cvonsteg, теперь должен дать функциональность: теперь перейдем к обзорам!

Эта функциональность была бы просто великолепной! Я не слишком разбираюсь в лексике гитхаба; означает ли комментарий @ rugg2 о том, что функциональность «сейчас

@ pmgh2345 -

PR, который мы писали с помощью cvonsteg, теперь должен дать функциональность: теперь перейдем к обзорам!

Возможно, стоит добавить новый параметр в метод to_sql , а не использовать if_exists . Причина в том, что if_exists проверяет наличие таблицы, а не строки.

@cvonsteg изначально предлагал использовать method= , что позволило бы избежать двусмысленности двух значений для if_exists .

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

@brylie, мы могли бы добавить новый параметр, который является истинным, но, как вы знаете, каждый новый параметр делает API более неуклюжим. Есть компромисс.

Если нам нужно выбрать один из текущих параметров, как вы сказали, мы изначально думали об использовании аргумента method , но после дополнительных размышлений мы поняли, что и (1) использование, и (2) логика лучше соответствует if_exists аргумент.

1) с точки зрения использования API
Пользователь захочет выбрать как method = "multi" или None, с одной стороны, так и "upsert", с другой. Однако не существует равнозначно сильных вариантов использования функции «upsert» одновременно с if_exists = «append» или «replace», если таковые имеются.

2) с логической точки зрения

  • метод в настоящее время работает над _ как_ вставляются данные: строка за строкой или "несколько"
  • if_exists фиксирует бизнес-логику того, как мы управляем нашими записями: «заменить», «добавить», «upsert_update» (добавить, если ключ существует, добавить, если новый), «upsert_ignore» (игнорировать, когда ключ существует, добавлять, когда новый). Хотя replace и append рассматривают существование таблицы, это также можно понять по ее влиянию на уровне записи.

Сообщите мне, хорошо ли я понял вашу точку зрения, и, пожалуйста, кричите, если вы считаете, что текущая реализация, находящаяся на рассмотрении (PR # 29636), будет отрицательной!

Да, вы понимаете мою точку зрения. Текущая реализация является чистой положительной, но несколько ослаблена неоднозначной семантикой.

Я по-прежнему утверждаю, что if_exists должно по-прежнему относиться только к одному - существованию таблицы. Неоднозначность параметров отрицательно влияет на удобочитаемость и может привести к запутанной внутренней логике. Принимая во внимание, что добавление нового параметра, например upsert=True является ясным и явным.

Привет!

Если вы хотите увидеть неагностическую реализацию для выполнения апсертов, у меня есть пример с моей библиотекой pangres . Он обрабатывает PostgreSQL и MySQL, используя функции sqlalchemy, специфичные для этих типов баз данных. Что касается SQlite (и других типов баз данных, допускающих аналогичный синтаксис upsert), он использует скомпилированную обычную вставку sqlalchemy.

Я разделяю это мнение, что это может дать несколько идей для соавторов (хотя я знаю, что мы хотим, чтобы это не зависело от типа SQL, что имеет большой смысл). Также, возможно, будет интересно сравнить скорость, когда пройдет PR @cvonsteg .
Пожалуйста, учтите, что я не являюсь экспертом по sqlalchemy или тому подобное!

Мне очень нужна эта функция. Я согласен с тем, что method='upsert_update' - хорошая идея.

Это все еще планируется? Пандам действительно нужна эта функция

Да, это все еще планируется, и мы почти закончили!

Код написан, но есть один тест, который не проходит. Помощь приветствуется!
https://github.com/pandas-dev/pandas/pull/29636

Во вторник, 5 мая 2020 г., 19:18 Леонель Атенсио [email protected] написал:

Это все еще планируется? Пандам действительно нужна эта функция

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
или отказаться от подписки
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.

Привет! Функционал готов или чего-то еще не хватает? Если чего-то по-прежнему не хватает, дайте мне знать, могу ли я чем-нибудь помочь!

Любые новости?))

Исходя из мира Java, никогда не думал, что эта простая функциональность может перевернуть мою кодовую базу с ног на голову.

Всем привет,

Я изучил, как апсерты реализованы в SQL на разных диалектах, и нашел здесь ряд методов, которые могут помочь в принятии дизайнерских решений. Но сначала я хочу предостеречь от использования логики DELETE ... INSERT. Если есть внешние ключи или триггеры, другие записи в базе данных будут удалены или иным образом испорчены. В MySQL REPLACE наносит такой же ущерб. На самом деле я потратил часы на исправление данных, потому что использовал REPLACE. Итак, вот методы, реализованные в SQL:

Диалект | Техника
- | -
MySQL | ВСТАВИТЬ ... ПРИ ОБНОВЛЕНИИ ДВОЙНОГО КЛЮЧА
PostgreSQL | ВСТАВИТЬ ... ПРИ КОНФЛИКТЕ
SQLite | ВСТАВИТЬ ... ПРИ КОНФЛИКТЕ
Db2 | ОБЪЕДИНЕНИЕ
SQL Server | ОБЪЕДИНЕНИЕ
Oracle | ОБЪЕДИНЕНИЕ
SQL: 2016 | ОБЪЕДИНЕНИЕ

Я понимаю, что из-за сильно различающегося синтаксиса возникает соблазн использовать DELETE ... INSERT, чтобы сделать диалект реализации независимым. Но есть другой способ: мы можем имитировать логику оператора MERGE, используя временную таблицу и базовые операторы INSERT и UPDATE. Синтаксис SQL: 2016 MERGE следующий:

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

Заимствовано из Oracle Tutorial
и настроен в соответствии с SQL Wikibook

Поскольку каждый диалект, поддерживаемый SQLAlchemy, поддерживает временные таблицы, более безопасный, не зависящий от диалекта подход к выполнению upsert будет заключаться в одной транзакции:

  1. Создайте временную таблицу.
  2. Вставьте данные в эту временную таблицу.
  3. Сделайте ОБНОВЛЕНИЕ ... ПРИСОЕДИНЯЙТЕСЬ.
  4. ВСТАВИТЬ, где ключ (ПЕРВИЧНЫЙ или УНИКАЛЬНЫЙ) не совпадает.
  5. Отбросьте временную таблицу.

Помимо того, что это метод, не зависящий от диалекта, он также имеет то преимущество, что он расширяется, позволяя конечному пользователю выбирать, как вставлять или как обновлять данные, а также какой ключ для соединения данных.

Хотя синтаксис временных таблиц и обновлений может немного отличаться в зависимости от диалекта, они должны поддерживаться повсюду.

Ниже приведено доказательство концепции, которую я написал для 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}`;')

Здесь я делаю следующие предположения:

  1. Структура вашего источника и назначения одинакова.
  2. Что вы хотите делать простые вставки, используя данные в вашем фрейме данных.
  3. Что вы хотите просто обновить все неключевые столбцы данными из вашего фрейма данных.
  4. Что вы не хотите вносить какие-либо изменения в данные в ключевых столбцах.

Несмотря на предположения, я надеюсь, что моя техника, основанная на MERGE, внесет вклад в усилия по созданию гибкого и надежного варианта upsert.

Я думаю, что это полезная функция, однако кажется, что она выходит за рамки, поскольку интуитивно понятно иметь такую ​​общую функцию при добавлении строк в таблицу.

Пожалуйста, подумайте еще раз, чтобы добавить эту функцию: очень полезно добавлять строки в существующую таблицу.
Увы, Пангрес ограничен Python 3.7+. Как и в моем случае (я вынужден использовать старый Python 3.4), это не всегда жизнеспособное решение.

Спасибо, @GoldstHa - это действительно полезный вклад. Я попытаюсь создать POC для реализации, подобной MERGE.

Учитывая проблемы с подходом DELETE/INSERT и потенциальным блокировщиком подхода @GoldstHa MERGE в базах данных MySQL, я немного покопался. Я собрал доказательство концепции с использованием функции обновления sqlalchemy , которая выглядит многообещающей. На этой неделе я попытаюсь реализовать его должным образом в кодовой базе Pandas, убедившись, что этот подход работает во всех вариантах БД.

Предложение модифицированного подхода

Было несколько хороших дискуссий вокруг API и того, как на самом деле следует вызывать upsert (т.е. через аргумент if_exists или через явный аргумент upsert ). Это будет уточнено в ближайшее время. На данный момент это предложение псевдокода о том, как эта функциональность будет работать с использованием оператора 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
Была ли эта страница полезной?
0 / 5 - 0 рейтинги