Jam*_*Hay 6 postgresql postgresql-performance
在我的 API 中,当存在具有该唯一键的行时,用户可能会发送一个尝试创建新行的请求。
目前,我正在捕获唯一键错误并返回一条消息,指出 X 已存在。但是,首先查找该行(在同一连接上)并且仅在该行不存在时才运行 INSERT 语句是否会更高效?
我的直觉告诉我,从 PostgreSQL 读取错误应该会更有效,但我想确保我正在按照惯用的方式做事。
PostgreSQL 版本为 12
我的 API 中的唯一键不是代理 ID 值,它是由外键与文本值组合而成的组合。如果唯一键约束没有失败,数据库确实已经为此行生成了自己的代理 ID 。所以该行的 ID 不是我要检查的内容。正确的行为是不插入行,因为 FK/文本值在表中需要是唯一的。如果请求包含表中已存在的 FK/文本值,则不应插入任何行。
bob*_*lux 13
\n\n首先查找该行(在同一连接上),并且仅在该行不存在时才运行 INSERT 语句是否会提高性能?
\n
如果其他人同时插入重复项,则没有问题:选择不会看到它,但将强制执行唯一约束。不过,您仍然需要复制错误处理代码。但是,如果有人在选择看到重复项后删除了它,那么它就不会被插入。
\n我运行了 Python 基准测试,源代码可在Pastebin上找到。这是一个简单的示例,使用仅具有主键和虚拟文本列的表。对于 0..99 范围内的每个 id,它会插入 100 次。只有第一次有效,其余的都会因为唯一性约束而被拒绝。
\n候选人是:
\ninsert_only:发送插入,然后要么有效,要么不满足唯一约束。
\nselect_then_insert:先选择检查,然后插入。
\ninsert_select 将前两个查询合并为一个,这也消除了竞争条件:
\nINSERT INTO testins (id,t) SELECT %s,\'hello, world\'\nWHERE NOT EXISTS( SELECT FROM testins WHERE id=%s )\nRETURNING id\nRun Code Online (Sandbox Code Playgroud)\non_conflict 使用 upsert 功能:
\nINSERT INTO testins (id,t) VALUES (%s,\'hello, world\') \nON CONFLICT (id) DO NOTHING \nRETURNING id\nRun Code Online (Sandbox Code Playgroud)\nRETURNING id如果该行已插入,则简单地返回id,因此您知道它是插入的。如果查询没有返回任何内容,则意味着存在重复项。
结果:“延迟”是每次 INSERT 尝试的时间。“行数”是在表中留下一行的成功 INSERT 数。总共有 10k 次 INSERT 尝试。
\n| 方法 | 潜伏 | 行数 | 工作台尺寸 |
|---|---|---|---|
| on_冲突: | 68.3\xc2\xb5s | 100行 | 8.000 KB |
| 仅插入: | 85.0\xc2\xb5s | 100行 | 512.000 KB |
| 选择然后插入: | 73.6\xc2\xb5s | 100行 | 8.000 KB |
| 插入_选择: | 61.5\xc2\xb5s | 100行 | 8.000 KB |
此测试每个插入有 99 个重复项,因此让我们尝试更合理的每次插入 1 个重复项:
\n| 方法 | 潜伏 | 行数 | 工作台尺寸 |
|---|---|---|---|
| on_冲突: | 74.3\xc2\xb5s | 5000行 | 256.000 KB |
| 仅插入: | 78.2\xc2\xb5s | 5000行 | 512.000 KB |
| 选择然后插入: | 94.3\xc2\xb5s | 5000行 | 256.000 KB |
| 插入_选择: | 66.8\xc2\xb5s | 5000行 | 256.000 KB |
没有重复:
\n| 方法 | 潜伏 | 行数 | 工作台尺寸 |
|---|---|---|---|
| on_冲突: | 81.6\xc2\xb5s | 10000 行 | 512.000 KB |
| 仅插入: | 69.1\xc2\xb5s | 10000 行 | 512.000 KB |
| 选择然后插入: | 184\xc2\xb5s | 10000 行 | 512.000 KB |
| 插入_选择: | 77.7\xc2\xb5s | 10000 行 | 512.000 KB |
在所有情况下,大部分时间都花在连接上的往返和提交事务上。
\n结论:
\n直的问题INSERT在于它仍然将行写入表中,然后尝试将其写入索引中,但在重复时失败,然后回滚事务。这会导致磁盘写入(表和 WAL),并且表会因死行而膨胀,需要 VACUUMing。做所有这些事情解释了性能的微小损失。
如果存在重复行,其他解决方案不会插入该行,这可以避免无用的写入和表膨胀。
\n对于 postgres 最惯用的说法是ON CONFLICT.
因此,如果您期望有大量重复项,即大多数时候都会INSERT失败,并且此查询的流量很高,那么使用ON CONFLICT.
如果您期望很少有重复项,即大多数时候都INSERT可以工作,那么您可以让它抛出错误。
如果这是一个较大事务的一部分,您不想失败、回滚并再次完成所有工作,那么ON CONFLICT可以提供帮助,因为在重复的情况下它不会抛出错误。
J.D*_*.D. 10
但是,首先查找该行(在同一连接上)并且仅在该行不存在时才运行 INSERT 语句是否会更高效?
无论它是否具有更高的性能,如果在查找期间不锁定整个表直到INSERT. 在不锁定表的情况下,理论上,某人可以INSERT在您检查和执行操作之间使用相同的数据密钥INSERT(即使它们相隔纳秒)。按照这个速度,从整体角度来看,整个系统的性能可能比仅仅依赖唯一键约束要低。
在 PostgreSQL 中,唯一约束是通过先插入记录,然后在违反约束时回滚来实现的。
如果您的约束被推迟,则暂时也会插入重复的 B 树条目,然后unique_key_recheck运行调用的内部触发器来验证新插入的记录是否违反约束。
它看起来像这样:
test=# CREATE TABLE mytable (id INT NOT NULL, value TEXT NOT NULL, CONSTRAINT ux_mytable_id UNIQUE (id) DEFERRABLE INITIALLY DEFERRED);
CREATE TABLE
test=# INSERT INTO mytable VALUES (1, 'test');
INSERT 0 1
test=# INSERT INTO mytable VALUES (1, 'test2');
ERROR: duplicate key value violates unique constraint "ux_mytable_id"
DETAIL: Key (id)=(1) already exists.
test=# SELECT * FROM heap_page_items(get_raw_page('mytable', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------
1 | 8152 | 1 | 33 | 759 | 0 | 0 | (0,1) | 2 | 2306 | 24 | | | \x010000000b74657374
2 | 8112 | 1 | 34 | 760 | 0 | 0 | (0,2) | 2 | 2050 | 24 | | | \x010000000d7465737432
(2 rows)
test=# SELECT * FROM bt_page_items('ux_mytable_id', 1);
itemoffset | ctid | itemlen | nulls | vars | data | dead | htid | tids
------------+-------+---------+-------+------+-------------------------+------+-------+------
1 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,1) |
2 | (0,2) | 16 | f | f | 01 00 00 00 00 00 00 00 | f | (0,2) |
(2 rows)
Run Code Online (Sandbox Code Playgroud)
这些结果集中的第二条记录是当约束失败时已回滚的死记录。这些记录使表空间和 WAL 变得混乱。
因此,直接回答您的问题:是的,有些影响整体性能的事情会在您违反约束时发生,而在您不违反约束时不会发生。
我的直觉告诉我,从 Postgres 读取错误应该会更有效
这取决于这些约束违规发生的频率。
提前读取记录需要遍历 B 树两次,如果您的错误率非常低(应该如此),那么在很长一段时间内进行一次死条目性能打击可能是值得的,而不是进行检查在每个插入物上。
请注意,在正确设计的系统中,无论您是否提前检查,唯一约束都应该存在。
| 归档时间: |
|
| 查看次数: |
1712 次 |
| 最近记录: |