【问题标题】:Increment counter based on column match (postgres)基于列匹配的增量计数器(postgres)
【发布时间】:2019-12-10 03:03:39
【问题描述】:

我有一个按项目跟踪唯一user_id 的列(供外部使用)。

我想在创建新用户时增加user_id 列,这取决于该项目的计数。已有许多记录,但从现在开始,我们希望 user_id 为给定 project_id 中的每个新记录增加 1。

用户表示例

id   user_id   project_id
-------------------------
1    100       1
2    101       1
3    1000      2
4    1001      2
5    17        3
6    18        3
7    102       1

New row with project_id = 1 should use user_id = 103
New row with project_id = 2 should use user_id = 1002
New row with project_id = 3 should use user_id = 19

如何构造user_id 列和/或INSERT 查询,使其始终根据对应project_id 中现有的最大user_id 递增user_id,并保证没有两个用户在同一个项目中,在并发插入时分配了相同的user_id

【问题讨论】:

  • 任何解决方案都将比基于序列的计数器更昂贵(并发性更差)。对不同的project_ids 具有相同号码的要求对您来说如此重要吗?
  • @LaurenzAlbe 我不确定你在问什么,但项目之间的 user_ids 根本不相关
  • 我的意思是,为什么不创建一个独立于product_id 的生成user_id 的序列?
  • id 基本上就是这样,但是每个项目都有用户,并且需要在项目中拥有唯一的、连续的 user_ids
  • 如果你使用一个序列,它们仍然是唯一的并且会增加,只是会有间隙。除非您有禁止此类差距的法律要求,否则请接受它们。它会让一切变得更快、更简单、更好。

标签: sql postgresql


【解决方案1】:

保证同一项目中没有两个用户在并发插入时被分配相同的user_id 的直接方法是防止并发活动。

实现它的一种方法是将transaction isolation level 设置为可序列化。

BEGIN TRANSACTION

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

-- here I took the query from George Joseph's answer

insert into user_table
    (user_id, project_id)
select
    coalesce(max(user_id), 0) + 1 as user_id
    ,@project_id as project_id
from
    user_table
where
    project_id=@project_id

COMMIT TRANSACTION

您可以同时从多个会话运行此查询块,引擎将在后台处理并发。我真的不知道 Postgres 是如何做到的。最有可能的并发事务将等待前一个事务完成。

为了有效地工作,您需要在(project_id, user_id) 上建立索引。 您还需要使其唯一以强制执行您的约束。此索引中列的顺序很重要。


您还提到,您预计会有数千个项目,最终每个项目会有数百万用户。这加起来有十亿行,对于每个插入运行 MAX 来说是相当多的。即使有适当的索引。

您可以为每个project_id 创建一个单独的表project_sequences 来存储user_id 的最后一个值。该表将有两列project_idlast_user_id,它们的主键为(project_id, last_user_id)。索引中列的顺序很重要。

现在您可以查询和更新小表project_sequences,在主大表中每次插入只有 1000 行。我对变量的 Postgres 语法不熟悉,所以下面是伪代码。

BEGIN TRANSACTION

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

-- read the last_user_id for the given project_id from the small table into a variable
-- and increment it
-- update the small table with the new last_user_id
-- use the freshly generated user_id to insert into the main table


-- or, without variables
-- increment the last_user_id
update project_sequences
set last_user_id = 
    (
    select coalesce(max(last_user_id), 0) + 1 
    from project_sequences
    where project_id=@project_id
    )
where
    project_id=@project_id


-- use the new id to insert into the main table
insert into user_table
    (user_id, project_id)
select
    last_user_id
    ,@project_id as project_id
from
    project_sequences
where
    project_id=@project_id


COMMIT TRANSACTION

使用变量,当给定的project_id 是新的,在表中尚不存在并将新的user_id 设置为以 1 开头或任何开头时,处理这种情况会更容易一些您需要的价值。

【讨论】:

  • 建议在插入第一个用户时稍作更改以适应 Null 最大值:更新 project_sequences set last_user_id = ( select COALESCE(max(last_user_id),0) + 1 from project_sequences where project_id=@project_id ) where project_id=@project_id
【解决方案2】:

你需要使用 WITH 子句。

这里是实现。

--PostgreSQL 9.6
create table tab
(
    id SERIAL ,
    user_id integer ,
    project_id integer 
 );

INSERT INTO tab(user_id, project_id ) VALUES (100 ,      1);
INSERT INTO tab(user_id, project_id ) VALUES (101     ,   1);
INSERT INTO tab(user_id, project_id ) VALUES (1000    ,   2);
INSERT INTO tab(user_id, project_id ) VALUES (1001    ,   2);
INSERT INTO tab(user_id, project_id ) VALUES (17      ,   3);
INSERT INTO tab(user_id, project_id ) VALUES (18      ,   3);
INSERT INTO tab(user_id, project_id ) VALUES (102     ,   1);

create table src 
(
    project_id integer
);

insert into src values  (1); 
insert into src values (2);
insert into src values (3) ; 

select * from src ; 
select * from tab ; 

with cur as 
(
select project_id , max(user_id)  as max_user_id from tab group by project_id
)
INSERT INTO tab(user_id, project_id ) 
    SELECT cur.max_user_id +  row_number() over( partition by src.project_id  ) , src.project_id 
    from src inner join cur on src.project_id = cur.project_id  ;

select * from tab order by project_id , user_id ;

结果:

    project_id
1   1
2   2
3   3

    id  user_id project_id
1   1   100 1
2   2   101 1
3   3   1000    2
4   4   1001    2
5   5   17  3
6   6   18  3
7   7   102 1

    id  user_id project_id
1   1   100 1
2   2   101 1
3   7   102 1
4   8   103 1
5   3   1000    2
6   4   1001    2
7   9   1002    2
8   5   17  3
9   6   18  3
10  10  19  3

https://rextester.com/HREM53701

在此处阅读有关 with 子句的更多信息

https://www.tutorialspoint.com/postgresql/postgresql_with_clause.htm

【讨论】:

    【解决方案3】:

    您可以通过 project_id 找出 user_id 的最大值然后将其增加 1 来做到这一点。如果您有一个多用户场景,那么需要考虑某种序列化来确保并发用户不会使用同一个号码 例如:假设您要将 project_id 作为变量传递 @project_id

    insert 
      into user_table
           (user_id
            ,project_id
           )
    select 
           (select max(user_id)+1
              from user_table
             where project_id=@project_id) as user_id
           ,@project_id
    

    【讨论】:

    • 我们需要保证两个用户在同一个项目中永远不会收到相同的user_id,所以你的并发点意味着这对我们不起作用。
    • 您可以在 (project_id,user_id) 字段组合上添加唯一约束/索引。因此,在并发插入的情况下,其中一个会话将获得 next_up 编号,而另一个会话将获得错误
    • 似乎应该有一种方法可以从一开始就避免出现该错误,这正是我们希望实现的。当然,我们可以处理应用程序逻辑中的错误,但让我感到惊讶的是 Postgres 没有涵盖这种情况。
    【解决方案4】:

    我建议在插入之前使用触发器,这样 99.99% 的 shure 就不会在序列中出现重复和漏洞(101,102,missing,111,112)。

    序列的问题在于,如果不小心使用,您可能会失去对当前数字的控制。最终导致数据库中缺少数字。

    只需让触发器负责递增数字即可。

    此外,通过这种方式,您不必担心会消耗大量内存和处理能力的复杂查询。

    触发器是:

    CREATE OR REPLACE FUNCTION set_user_id()
        RETURNS trigger AS $set_user_id$
    BEGIN
        IF NEW.user_id IS NULL
        THEN
            NEW.user_id = COALESCE( ( SELECT MAX(user_id) FROM data WHERE project_id = NEW.project_id ), 0 ) + 1;
        END IF;
        RETURN NEW;
    END $set_user_id$ LANGUAGE plpgsql;
    
    CREATE TRIGGER table_user_id
        BEFORE INSERT ON data
        FOR EACH ROW EXECUTE PROCEDURE set_user_id();
    

    注意事项:

    • 仅当插入向 user_id 发送 null 时才会增加用户。例如:

    INSERT INTO data (project_id) VALUES (1);

    INSERT INTO data (user_id,project_id) VALUES (NULL,1);

    使用您的示例数据,这将插入

    id   user_id   project_id
    -------------------------
    8    103       1
    
    • 如果没有以前的user_id,则将值设置为1(COALESCE

    INSERT INTO data (project_id) VALUES (4);

    使用您的示例数据,这将插入

    id   user_id   project_id
    -------------------------
    9    1         4
    
    • 如果要设置起始 user_id,只需在该 project_id 的第一个插入项上设置即可。

    INSERT INTO data (user_id,project_id) VALUES (10,5);

    使用您的示例数据,这将插入

    id   user_id   project_id
    -------------------------
    10   10        5
    

    【讨论】:

    • 您能否详细说明“99.99% 确定您不会有重复”?我们会在什么情况下进行?
    • 好吧,因为在这种情况下,插入被委托给 postgresql 引擎,所以插入的处理属于 postgresql 插入数据的方式。两个查询同时执行是非常罕见的,但可能会出现重复的情况。为此,我建议您为 user_id、project_id 列添加 UNIQUE 约束(必须同时具有)。 Here's the manual regarding concurrency。我相信 UNIQUE 约束应该足够了
    • 触发器很有用,但很难管理,而且在我过去 15 年的数据库开发经验中,在层级之下也无法让我“喜欢”它们。我的建议是远离触发器。
    • 在 PostgreSQL 中,触发器与其他 DBMS 不同。如果您不使它们过于复杂,它们的效果会非常好。如果您需要花费大量处理时间和功率的东西,那么您应该改变您的方法,但是对于像增量这样简单的东西,触发器就可以了。
    【解决方案5】:

    为了自动增加您的 id,您可以使用 3 种方法,

    1. 使用身份 例如 - 创建表时

          create table a( key int identity(1,1)) 
      
          -- first "1" is initial value
      
             -- second"1" is a value which is added to next one
      
    2. 创建序列

    创建序列 seq_name

    作为 dat_type -- bigint

    从 1 开始

    递增 1 - 递增值

    参考-https://www.techonthenet.com/sql_server/sequences.php

    3 - 使用 select sum(col_name) from t_name ##### 来自编程代码并将一个值添加到检索到的值并将该值用于将添加到新创建的 id 中的 id..

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-05-20
      • 1970-01-01
      • 2015-06-05
      • 2011-07-17
      • 2020-09-01
      • 1970-01-01
      • 2021-07-05
      相关资源
      最近更新 更多