Postgres 中违反唯一键约束是否会导致性能损失?

Jam*_*Hay 6 postgresql postgresql-performance

在我的 API 中,当存在具有该唯一键的行时,用户可能会发送一个尝试创建新行的请求。

目前,我正在捕获唯一键错误并返回一条消息,指出 X 已存在。但是,首先查找该行(在同一连接上)并且仅在该行不存在时才运行 INSERT 语句是否会更高效?

我的直觉告诉我,从 PostgreSQL 读取错误应该会更有效,但我想确保我正在按照惯用的方式做事。

PostgreSQL 版本为 12

我的 API 中的唯一键不是代理 ID 值,它是由外键与文本值组合而成的组合。如果唯一键约束没有失败,数据库确实已经为此行生成了自己的代理 ID 。所以该行的 ID 不是我要检查的内容。正确的行为是不插入行,因为 FK/文本值在表中需要是唯一的。如果请求包含表中已存在的 FK/文本值,则不应插入任何行。

bob*_*lux 13

\n

首先查找该行(在同一连接上),并且仅在该行不存在时才运行 INSERT 语句是否会提高性能?

\n
\n

如果其他人同时插入重复项,则没有问题:选择不会看到它,但将强制执行唯一约束。不过,您仍然需要复制错误处理代码。但是,如果有人在选择看到重复项后删除了它,那么它就不会被插入。

\n

我运行了 Python 基准测试,源代码可在Pastebin上找到。这是一个简单的示例,使用仅具有主键和虚拟文本列的表。对于 0..99 范围内的每个 id,它会插入 100 次。只有第一次有效,其余的都会因为唯一性约束而被拒绝。

\n

候选人是:

\n
    \n
  • insert_only:发送插入,然后要么有效,要么不满足唯一约束。

    \n
  • \n
  • select_then_insert:先选择检查,然后插入。

    \n
  • \n
  • insert_select 将前两个查询合并为一个,这也消除了竞争条件:

    \n
    INSERT INTO testins (id,t) SELECT %s,\'hello, world\'\nWHERE NOT EXISTS( SELECT FROM testins WHERE id=%s )\nRETURNING id\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
  • on_conflict 使用 upsert 功能:

    \n
    INSERT INTO testins (id,t) VALUES (%s,\'hello, world\') \nON CONFLICT (id) DO NOTHING \nRETURNING id\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
\n

RETURNING id如果该行已插入,则简单地返回id,因此您知道它是插入的。如果查询没有返回任何内容,则意味着存在重复项。

\n

结果:“延迟”是每次 INSERT 尝试的时间。“行数”是在表中留下一行的成功 INSERT 数。总共有 10k 次 INSERT 尝试。

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n
方法潜伏行数工作台尺寸
on_冲突:68.3\xc2\xb5s100行8.000 KB
仅插入:85.0\xc2\xb5s100行512.000 KB
选择然后插入:73.6\xc2\xb5s100行8.000 KB
插入_选择:61.5\xc2\xb5s100行8.000 KB
\n

此测试每个插入有 99 个重复项,因此让我们尝试更合理的每次插入 1 个重复项:

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n
方法潜伏行数工作台尺寸
on_冲突:74.3\xc2\xb5s5000行256.000 KB
仅插入:78.2\xc2\xb5s5000行512.000 KB
选择然后插入:94.3\xc2\xb5s5000行256.000 KB
插入_选择:66.8\xc2\xb5s5000行256.000 KB
\n

没有重复:

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n
方法潜伏行数工作台尺寸
on_冲突:81.6\xc2\xb5s10000 行512.000 KB
仅插入:69.1\xc2\xb5s10000 行512.000 KB
选择然后插入:184\xc2\xb5s10000 行512.000 KB
插入_选择:77.7\xc2\xb5s10000 行512.000 KB
\n

在所有情况下,大部分时间都花在连接上的往返和提交事务上。

\n

结论:

\n

直的问题INSERT在于它仍然将行写入表中,然后尝试将其写入索引中,但在重复时失败,然后回滚事务。这会导致磁盘写入(表和 WAL),并且表会因死行而膨胀,需要 VACUUMing。做所有这些事情解释了性能的微小损失。

\n

如果存在重复行,其他解决方案不会插入该行,这可以避免无用的写入和表膨胀。

\n

对于 postgres 最惯用的说法是ON CONFLICT.

\n

因此,如果您期望有大量重复项,即大多数时候都会INSERT失败,并且此查询的流量很高,那么使用ON CONFLICT.

\n

如果您期望很少有重复项,即大多数时候都INSERT可以工作,那么您可以让它抛出错误。

\n

如果这是一个较大事务的一部分,您不想失败、回滚并再次完成所有工作,那么ON CONFLICT可以提供帮助,因为在重复的情况下它不会抛出错误。

\n


J.D*_*.D. 10

但是,首先查找该行(在同一连接上)并且仅在该行不存在时才运行 INSERT 语句是否会更高效?

无论它是否具有更高的性能,如果在查找期间不锁定整个表直到INSERT. 在不锁定表的情况下,理论上,某人可以INSERT在您检查和执行操作之间使用相同的数据密钥INSERT(即使它们相隔纳秒)。按照这个速度,从整体角度来看,整个系统的性能可能比仅仅依赖唯一键约束要低。


Qua*_*noi 7

在 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 树两次,如果您的错误率非常低(应该如此),那么在很长一段时间内进行一次死条目性能打击可能是值得的,而不是进行检查在每个插入物上。

请注意,在正确设计的系统中,无论您是否提前检查,唯一约束都应该存在。