如何在一个表中创建多个序列?

时间:2012-10-05 12:17:12

标签: database postgresql database-design relational-database postgresql-9.1

我有一张桌子“收据”。我有列customer_id(谁有收据)和receipt_number。对于每个客户,receipt_number应该从1开始并且是序列。这意味着customer_id和receipt_number将是唯一的。我怎样才能优雅地做到这一点。我可以使用CREATE SEQUENCE或类似的内置序列功能吗?似乎我必须为每个客户创建一个序列,这当然不是一个优雅的解决方案。

编辑:必须有线程安全和白痴安全的方法来做到这一点。这应该是一个非常简单/普遍的需求。

5 个答案:

答案 0 :(得分:2)

SEQUENCE不保证没有差距。例如,一个事务可能会生成一个新数字然后中止(由于错误或电源故障或其他...)。然后,下一个交易将盲目地获得下一个数字,而不是那个“丢失”的数字。

如果您的客户申请不依赖于第一个地方的“无差距”假设,那将是最好的。但是,您可以最大限度地减少这样的差距:

  1. SELECT MAX(receipt_number) FROM receipts WHERE customer_id = :ci
  2. INSERT INTO receipts(customer_id, receipt_number) VALUES (:ci, aboveresult+1),如果 第1步 返回NULL,则只插入1。
  3. 如果 第2步 返回PK违规 * ,请从头开始重试。
  4. * 因为并发事务已经过同一个进程并已提交。

    只要添加行而不删除行,即使在并发环境中也可以防止出现任何空白。


    顺便说一下,你可以像这样“浓缩” 第1步和第2步

    INSERT INTO receipts (customer_id, receipt_number)
    SELECT :ci, COALESCE(MAX(receipt_number), 0) + 1
    FROM receipts
    WHERE customer_id = :ci;
    

    [SQL Fiddle]

    PK {customer_id,receipt_number}下面的索引应确保有效满足此查询的SELECT部分​​。

答案 1 :(得分:1)

enter image description here

-- next CustomerReceiptNo
select coalesce(max(CustomerReceiptNo), 0) + 1
from  Receipt
where CustomerId = specific_customer_id;

这不是线程安全的,因此如果两个单独的线程试图同时为给定客户创建新收据,请务必实施错误处理。


修改

线程安全不仅仅是避免竞争条件。假设有两个单独的线程同时为同一个客户创建新收据。它会发生吗?这是正常的,错误还是安全漏洞?假设一个银行,两个柜员同时为同一个客户创建一个新记录 - 这是非常错误的。如果发生这种情况,你可以使用锁;如果没有,那么就会出现某种错误。

答案 2 :(得分:1)

为什么每个客户的收据编号都以1开头?这是定义要求的一部分吗?

完成此任务的最简单方法是让生成新收据的程序在数据库中查询max(ReceiptNumber),其中CustomerId = CurrentCustomerId,然后添加1.

currentCustomerId是程序变量而不是数据库值。

这有点不雅,因为需要额外搜索表格。您需要仔细创建索引,以便在没有全表扫描的情况下让其中一个索引回答问题。

在插入时更快一点的替代方法是在customer表中创建一个名为MaxReeceiptNumber的额外列。当您想要插入新收据时增加。

答案 3 :(得分:1)

您可以使用这样的触发器来更新列:

对customer_id,receipt_number具有唯一约束的表定义:

CREATE TABLE receipts (id serial primary key, customer_id bigint, receipt_number bigint default 1);
CREATE UNIQUE INDEX receipts_idx ON receipts(customer_id, receipt_number);

检查客户端的最大receipt_number的功能,如果没有先前的收据,则检查为1

CREATE OR REPLACE FUNCTION get_receipt_number()  RETURNS TRIGGER AS $receipts$
  BEGIN
    -- This lock will block other transactions from doing anything to table until
    -- committed. This may not offer the best performance, but is threadsafe.
    LOCK TABLE receipts IN ACCESS EXCLUSIVE MODE;
    NEW.receipt_number = (SELECT CASE WHEN max(receipt_number) IS NULL THEN 1 ELSE max(receipt_number) + 1 END FROM receipts WHERE customer_id = new.customer_id);
    RETURN NEW;
  END;
$receipts$ LANGUAGE 'plpgsql';

触发在每行插入时触发函数:

CREATE TRIGGER rcpt_trigger 
   BEFORE INSERT ON receipts 
   FOR EACH ROW 
   EXECUTE PROCEDURE get_receipt_number();

然后,执行以下操作:

db=> insert into receipts (customer_id) VALUES (1);
INSERT 0 1
db=> insert into receipts (customer_id) VALUES (1);
INSERT 0 1
db=> insert into receipts (customer_id) VALUES (2);
INSERT 0 1
db=> insert into receipts (customer_id) VALUES (2);
INSERT 0 1
db=> insert into receipts (customer_id) VALUES (2);

应该产生:

  id | customer_id | receipt_number 
 ----+-------------+----------------  
  14 |           1 |              1  
  15 |           1 |              2  
  16 |           2 |              1 
  17 |           2 |              2  
  18 |           2 |              3

答案 4 :(得分:0)

我想为这个问题提出解决方案-使用customer表上的+1列存储last_receipt_id,并使用增量函数next_receipt_id(customer_id):

ALTER TABLE customers ADD COLUMN latest_receipt_id integer DEFAULT 1;

-- ensure customer_id, receipt_number pair uniqueness
CREATE UNIQUE INDEX customer_receipt_ids_pair_uniq_index ON receipts USING btree (customer_id, receipt_number);

-- sequence-like function for the next receipt id, 
-- will increment it on every execution
CREATE FUNCTION next_receipt_id( for_customer_id integer ) RETURNS integer
LANGUAGE plpgsql AS 
$$
DECLARE 
  result integer;
BEGIN  
  UPDATE customers SET latest_receipt_id = latest_receipt_id + 1 WHERE id = for_customer_id RETURNING latest_receipt_id INTO result;
  RETURN result;
END;
$$;

然后,您可以在收据INSERT触发器中使用它:

-- somewhere inside trigger function, triggered on receipt INSERT 
NEW.receipt_number := next_receipt_id( NEW.customer_id );

ORM(伪代码)中的OR:

# it does not matter when you assign the receipt_number, 
# it could be even in standalone update execution, just do it only once! 
receipt.update( 'receipt_number = next_receipt_id(customer_id)' )

不管插入的任何并发性,您将始终具有顺序ID。

干杯!