每组值的自定义序列/自动增量

Posted

技术标签:

【中文标题】每组值的自定义序列/自动增量【英文标题】:Custom SERIAL / autoincrement per group of values 【发布时间】:2021-10-02 03:40:34 【问题描述】:

我正在尝试制作一个类似的博客系统,但遇到了一个小问题。

简单地说,我的article 表中有 3 列:

id SERIAL,
category VARCHAR FK,
category_id INT

id 列显然是PK,它被用作所有文章的全局标识符。

category 栏目很好.. 分类。

category_id 用作类别中的UNIQUE ID,因此目前存在UNIQUE(category, category_id) 约束。

不过,我也希望 category_id 能够自动递增

我想要这样每次我执行类似的查询

INSERT INTO article(category) VALUES ('***');

我希望category_id 列根据'***' 类别的最新category_id 自动填充。

在我的逻辑代码中实现这一点非常容易。我只是选择最新的 num 并插入其中的 +1 ,但这涉及两个单独的查询。 我正在寻找一种可以在一个查询中完成所有这些操作的 SQL 解决方案。

【问题讨论】:

@a_horse_with_no_name 哈哈,我完全忘记了这一点。无法相信我在浪费了无数个小时之后又犯了那个错误。 【参考方案1】:

这个问题已经被问过很多次了,一般的想法是在多用户环境中肯定会失败 - 博客系统听起来就是这样的情况。

所以最好的答案是:不要。考虑不同的方法。

从您的表中完全删除列 category_id - 它不存储其他两列 (id, category) 不会存储的任何信息。

您的 id 是一个 serial 列,并且已经以可靠的方式自动递增。

Auto increment SQL function

如果您需要某种category_id,每个category没有间隙,请使用row_number()即时生成它:

Serial numbers per group of rows for compound key

【讨论】:

如果没有其他东西真的依赖于将category_id 存储在表中,那么竞争条件是否存在问题?据我了解,只有当一些不可预知的动作可能连续发生时,才会出现这样的问题。我的意思是,考虑到交易,给定的category 是否会有两个确切的值? @ConsiderMe:是的,这完全可以(并且将会)发生。在选择当前最大值和提交已插入最大值+1 的新行的事务之间存在时间间隔。在此期间,任何数量的并发会话将看到相同的最大值,因此尝试插入相同的数量,如果您有唯一约束,则会产生重复或引发具有唯一违规的异常。 感谢您的回复。基本上一个人可以通过做一个独特的约束并做一个 upsert 来处理它,对吗?第二种选择是设置Read uncommitted 隔离级别? 此评论仅用于处理此类情况,不说明应遵循的要点。 @ConsiderMe:第 9.5 页的 UPSERT 提供了一种替代措施,以防出现独特的违规行为。 I just wrote a related answer. 但替代方案仍然可能失败,它没有解决手头的并发问题。 read uncommitted 会减少某些竞争条件的窗口,但也没有解决问题,并在phantom reads 中引入了一个新问题。 serializable 将解决问题 - 代价是主要的性能损失和序列化失败。 如果您坚持维护category_id,最好的做法是对categorycategory 表进行FK 约束,并对之前的类别进行行级锁定添加一个新的子行。那条线上有一个相关的答案,但我在ATM上找不到。我不会去那里根本,这就是为什么我的回答围绕着“不要”。【参考方案2】:

Postgresql 使用序列来实现这一点;这与您在 mysql 中使用的方法不同。请查看http://www.postgresql.org/docs/current/static/sql-createsequence.html 以获取完整参考。

基本上,您通过以下方式创建序列(数据库对象):

CREATE SEQUENCE serials;

然后当你想添加到你的表中时,你将拥有:

INSERT INTO mytable (name, id) VALUES ('The Name', NEXTVAL('serials')

【讨论】:

啊我明白了。所以这看起来是一个非常巧妙的问题。如果我理解正确,我需要为所有类别创建一个序列,对吗?如果是这样,无论何时添加新类别,是否都会自动执行此操作? 我真的不建议您遵循此答案中的方法。这将需要您的应用程序逻辑来区分类别和管理序列,而且对于一列有多个序列似乎不是一个好的设计。 哦...在那种情况下,你能推荐一个更好的方法吗?即使只是一个搜索关键字也会受到赞赏:) 为了更正我之前写的内容:您也可以在触发器内进行这种区分,但是这样做需要维护它或执行更复杂的查询。 这就是现有的serialid 已经自动执行的操作。你的回答只会以无益的方式干扰它。【参考方案3】:

概念

至少有几种方法可以解决这个问题。我想到的第一个:

通过覆盖INSERT 语句中的输入值,为每行执行的触发器内的category_id 列分配一个值。

动作

这里是 SQL Fiddle 查看实际代码


对于一个简单的测试,我正在创建article 表保存类别及其id,每个category 应该是唯一的。我省略了约束创建 - 这与提出要点无关。

create table article ( id serial, category varchar, category_id int )

使用generate_series() 函数为两个不同的类别插入一些值,以实现自动增量。

insert into article(category, category_id)
  select '***', i from generate_series(1,1) i
  union all
  select 'stackexchange', i from generate_series(1,3) i

创建一个触发函数,它将选择MAX(category_id) 并将其值增加1 以获得category,我们插入一行,然后覆盖该值,然后继续使用实际的INSERT 到表(BEFORE INSERT 触发器负责处理)。

CREATE OR REPLACE FUNCTION category_increment()
RETURNS trigger
LANGUAGE plpgsql
AS
$$
DECLARE
  v_category_inc int := 0;
BEGIN
  SELECT MAX(category_id) + 1 INTO v_category_inc FROM article WHERE category = NEW.category;
  IF v_category_inc is null THEN
    NEW.category_id := 1;
  ELSE
    NEW.category_id := v_category_inc;
  END IF;
RETURN NEW;
END;
$$ 

使用函数作为触发器。

CREATE TRIGGER trg_category_increment 
  BEFORE INSERT ON article 
  FOR EACH ROW EXECUTE PROCEDURE category_increment()

为已存在的类别和不存在的类别插入更多值(触发后设备)。

INSERT INTO article(category) VALUES 
  ('***'),
  ('stackexchange'),
  ('nonexisting');

查询用于选择数据:

select category, category_id From article order by 1,2

初始插入结果

category    category_id
stackexchange   1
stackexchange   2
stackexchange   3
***   1

最终插入后的结果:

category    category_id
nonexisting     1
stackexchange   1
stackexchange   2
stackexchange   3
stackexchange   4
***   1
***   2

【讨论】:

我认为您可以使用 Postgres 中可用的 FOUND 特殊变量(而不是检查变量是否为空),但 SQLFiddle 似乎对此很挑剔,或者我遗漏了一些东西。 这看起来很有希望!谢谢你:) 不要在多用户环境中使用它。存在不可预测的竞争条件。 欧文在这点上是对的。我写答案只是为了展示完成您所询问的工作的方法。您可以通过使用窗口函数并按串行列对分区进行排序来实现相同的目的。这样您就可以随时计算这些序列,而无需存储它们。 @Kamil G. 哦,谢谢您的解决方案。它帮助我完成了任务!

以上是关于每组值的自定义序列/自动增量的主要内容,如果未能解决你的问题,请参考以下文章

postgresql中的自定义自动增量字段(发票/订单号)

Hive - 如何获取每组值的分位数

sql:用group by分组后,每组随意取一个记录?

在 Jackson StdDeserializer 的自定义实例中自动装配 Bean

布尔值的自定义访问器

Anylogic - 具有汇总值的自定义分布?