假设您有一个名为person_age
的现有 SQL 表,其中id
是主键:
age
id
1 18
2 42
并且您在名为extra_data
的DataFrame
还有新数据
age
id
2 44
3 95
那么在extra_data.to_sql()
上有一个选项会很有用,它允许基于primary key
在行上使用INSERT
或UPDATE
选项将 DataFrame 传递给 SQL primary key
。
在这种情况下, id=2
行将更新为age=44
,而id=3
行将被添加
age
id
1 18
2 44
3 95
merge
吗?我查看了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)
这将是一个不错的功能,但主要问题是我们希望它独立于数据库风格并基于 sqlalchemy 核心(因此不是 sqlalchemy ORM)以包含在 Pandas 本身中。
这将使这难以实施..
是的,我认为这超出了 Pandas 的范围,因为所有数据库引擎都不支持 upsert。
而一个INSERT OR UPDATE
不是由所有的引擎的支持,一个INSERT OR REPLACE
可以通过从目标表中删除行对于该组中的数据帧索引主键进行发动机不可知随后的插入DataFrame 中的所有行。 您希望在事务中执行此操作。
@TomAugspurger我们能否为支持的数据库引擎添加 upsert 选项并为不支持的数据库引擎抛出错误?
我也想看这个。 我被夹在使用纯 SQL 和 SQL Alchemy 之间(还没有让它起作用,我认为这与我如何传递字典有关)。 我使用 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 ,不应该允许不实现这样的好功能,因为某些数据库不支持。 这个功能有可能发生吗?
大概。 如果有人做 PR。 进一步考虑,我不认为我反对某些数据库不支持它的原则。 但是,我对 sql 代码不太熟悉,所以我不确定最好的方法是什么。
如果引入此 PR,一种可能性是使用method
可调用的 upserts 提供一些示例: https :
对于看起来像(未经测试)的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。
| 身份证 | 姓名 |
| --- | --- |
| 0 | 约翰 |
| 1 | 乔|
| 2 | 哈里 |
我有一个具有相同列但更新值的熊猫数据框:
| 身份证 | 姓名 |
| --- | --- |
| 0 | 克里斯 |
| 1 | 詹姆斯 |
我们还假设我们打开了一个会话变量来访问数据库。 通过调用这个方法:
session.bulk_update_mappings(
User,
<pandas dataframe above>.to_dict(orient='records')
)
Pandas 会将表转换为字典列表 [{id: 0, name: "chris"}, {id: 1, name:"james"}] ,sql 将使用该列表来更新表的行。 所以决赛桌看起来像:
| 身份证 | 姓名 |
| --- | --- |
| 0 | 克里斯 |
| 1 | 詹姆斯 |
| 2 | 哈里 |
嗨, @danich1 ,非常感谢您的回复。 我自己弄清楚了更新将如何工作的机制。 不幸的是,我不知道如何处理会话,我是初学者。
让我告诉你我在做什么:
` 导入pypyodbc
from to_sql_newrows import clean_df_db_dups, to_sql_newrows #这些是我在 GitHub 上找到的 2 个函数,不幸的是我不记得链接了。 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 功能做出贡献,或者失败,关于如何在 Pandas 中实现 upsert 功能的解决方案/最佳实践的文档,如下所示:
https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#io -sql-method
Pandas 核心开发/产品经理的首选前进方向是什么?
我认为我们对特定于引擎的实现持开放态度。 使用method='upsert'
的提议似乎是合理的,但在这一点上我认为我们需要有人提出一个明确的设计方案。
嘿熊猫人:我遇到了同样的问题,需要经常使用我最终在熊猫中加载和操作的记录更新我的本地数据库。 我构建了一个简单的库来执行此操作 - 它基本上是 df.to_sql 和 pd.read_sql_table 的替代品,默认情况下使用 DataFrame 索引作为主键。 仅使用 sqlalchemy 核心。
https://pypi.org/project/pandabase/0.2.1/
https://github.com/notsambeck/pandabase
这个工具相当固执,可能不适合按原样包含在 Pandas 中。 但是对于我的特定用例,它解决了问题……如果有兴趣对其进行按摩以使其适合 Pandas,我很乐意提供帮助。
目前,以下工作(在当前-ish pandas 和 sqlalchemy 的有限情况下,将索引命名为主键,SQLite 或 Postgres 后端,以及支持的数据类型):
pip install pandabase / pandabase.to_sql(df, table_name, con_string, how='upsert')
与 cvonsteg 一起研究这个问题的通用解决方案。 计划在 10 月带着拟议的设计回来。
@TomAugspurger按照建议, @rugg2和我为to_sql()
的upsert
选项提出了以下设计建议。
在to_sql()
方法中要添加 2 个新变量作为可能的method
参数:
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 个包含 upsert 逻辑的新私有方法,这些方法将从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))
...
我们提出以下实施方案,并在下面详细概述了基本原理(所有要点都可供讨论):
DELETE
和INSERT
的原子序列upsert
,并且实现可能因风格而异upsert_ignore
这些操作显然会在匹配记录上被跳过UNIQUE
约束,那么多行可能匹配 upsert 条件是合理的。 在这种情况下,不应该执行 upsert,因为它不明确应该更新哪个记录。 为了在 Pandas 中强制执行此操作,在插入之前,需要单独评估每一行以检查是否只有 1 或 0 行匹配。 虽然此功能实现起来相当简单,但它导致每条记录都需要读取和写入操作(如果发现 1 个记录冲突,则加上删除),这对于较大的数据集来说效率非常低。@TomAugspurger ,如果用@cvonsteg设计的upsert
提案适合您,我们将继续在代码中实现(包括测试)并提出拉取请求。
如果您想以不同的方式继续,请告诉我们。
通读提案在我的待办事项清单上。 我有点落后
立即发送电子邮件。
在星期三,2019年10月9日在上午09时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=AAKAOITBNTWOQRBW3OWDEZDQNXR25A5CNFSM4CU2M7O2YY3PNVWWK3TUL52HS4DFVREXG43MVVMVBWZKLEN200000WWWWZG00200000WWJK002000000000000000000000000000000000000000000000000000000000000000000004MVVMVBWZG001MVVMVBWZG000000000000000000000000000000000007CNFSM4CNFSM4CU2CNFSM4CNFSM4CNFSM4CNFSM4CNFSM4CNFSM4CU2
或静音线程
https://github.com/notifications/unsubscribe-auth/AAKAOIRZQEQWUY36PQ36QTLQNXR25ANCNFSM4CU2M7OQ
.
我个人并不反对它,所以认为 PR 是受欢迎的。 使用 SQLAlchemy 核心的所有 DBM 的一种实现当然是如果我正确阅读您的观点,这应该如何开始,并且仅使用主键。
总是更容易从小处开始,专注于并从那里扩展
非常需要这个功能。
我们用 cvonsteg 编写的 PR 现在应该提供以下功能:现在可以进行评论了!
这个功能绝对是光荣的! 我不是太精通github的词汇; @rugg2的评论说功能“现在需要审查”是否意味着由
@pmgh2345 - 是的,正如您所说,“现在进行审查”意味着已提出拉取请求,并且正在接受核心开发人员的审查。 你可以看到上面提到的 PR (#29636)。 一旦获得批准,您可以在技术上使用更新的代码分叉分支,并使用内置功能编译您自己的本地版本的 Pandas。但是,我个人建议等到它被合并到 master 并发布,然后只需 pip 安装最新版本的熊猫。
我们用 cvonsteg 编写的 PR 现在应该提供以下功能:现在可以进行评论了!
可能值得向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"。 但是,没有与 if_exists="append" 或 "replace"(如果有)同时使用“upsert”功能的强大用例。
2)从逻辑的角度
如果我理解您的观点,请告诉我,如果您认为正在审查的当前实施 (PR #29636) 是负面的,请大声喊出来!
是的,你明白我的意思。 当前的实现是积极的,但由于模糊的语义而略有减弱。
我仍然认为if_exists
应该继续只引用一件事,表的存在。 参数中的歧义会对可读性产生负面影响,并可能导致复杂的内部逻辑。 然而,添加一个新参数,如upsert=True
是清晰明确的。
我真的很想要这个功能。 我同意method='upsert_update'
是个好主意。
这还在计划中吗? 熊猫真的需要这个功能
是的,这仍在计划中,我们快到了!
代码写好了,但有一个测试没有通过。 欢迎帮助!
https://github.com/pandas-dev/pandas/pull/29636
在周二2020年5月5日19:18莱昂内尔Atencio [email protected]写道:
这还在计划中吗? 熊猫真的需要这个功能
—
你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/pandas-dev/pandas/issues/14553#issuecomment-624223231 ,
或取消订阅
https://github.com/notifications/unsubscribe-auth/AI5X625A742YTYFZE7YW5A3RQBJ6NANCNFSM4CU2M7OQ
.
你好! 功能准备好还是仍然缺少某些东西? 如果仍然缺少某些东西,请告诉我是否可以提供任何帮助!
任何新闻?))
来自 Java 世界,从没想过这个简单的功能可能会颠覆我的代码库。
嗨,大家好,
我研究了如何在 SQL 中跨方言实现 upsert,并在这里找到了许多可以为设计决策提供信息的技术。 但首先,我想警告不要使用 DELETE ... INSERT 逻辑。 如果有外键或触发器,数据库中的其他记录最终将被删除或以其他方式混乱。 在 MySQL 中,REPLACE 会造成同样的损害。 因为我使用了 REPLACE,我实际上为自己创造了修复数据的工作时间。 所以,也就是说,这里是在 SQL 中实现的技术:
方言| 技术
-- | ——
MySQL | 插入...在重复的密钥更新上
PostgreSQL | 插入 ... 关于冲突
SQLite | 插入 ... 关于冲突
数据库 | 合并
SQL Server | 合并
甲骨文| 合并
SQL:2016 | 合并
由于语法千差万别,我理解使用 DELETE ... INSERT 使实现方言不可知的诱惑。 但还有另一种方式:我们可以使用临时表和基本的 INSERT 和 UPDATE 语句来模仿 MERGE 语句的逻辑。 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 教程
并调整以符合SQL Wikibook
由于 SQLAlchemy 支持的每种方言都支持临时表,因此在单个事务中执行更新插入的更安全、与方言无关的方法是:
除了是一种与方言无关的技术之外,它还具有通过允许最终用户选择如何插入或如何更新数据以及连接数据的键来扩展的优势。
虽然临时表的语法和更新连接在方言之间可能略有不同,但它们应该在任何地方都得到支持。
以下是我为 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}`;')
在这里,我做出以下假设:
尽管有这些假设,但我希望我的受 MERGE 启发的技术能够为构建灵活、强大的 upsert 选项的努力提供信息。
我认为这是一个有用的功能,但它似乎超出了范围,因为在向表中添加行时拥有这样一个通用功能是很直观的。
请再考虑添加此功能:将行添加到现有表中非常有用。
Alas Pangres 仅限于 Python 3.7+。 就我而言(我被迫使用旧的 Python 3.4),它并不总是一个可行的解决方案。
谢谢, @GoldstHa - 这是非常有用的输入。 我将尝试为类似 MERGE 的实现创建一个 POC
鉴于DELETE/INSERT
方法的问题,以及@GoldstHa MERGE
方法对 MySQL 数据库的潜在阻碍,我做了更多的挖掘。 我已经使用sqlalchemy 更新功能进行了概念验证,这看起来很有希望。 本周我将尝试在 Pandas 代码库中正确实现它,确保这种方法适用于所有 DB 风格。
关于 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
最有用的评论
而一个
INSERT OR UPDATE
不是由所有的引擎的支持,一个INSERT OR REPLACE
可以通过从目标表中删除行对于该组中的数据帧索引主键进行发动机不可知随后的插入DataFrame 中的所有行。 您希望在事务中执行此操作。