How to create multiple sequences in one table?

风格不统一 提交于 2019-12-07 08:57:02

问题


I have a table "receipts". I have columns customer_id (who had the receipt) and receipt_number. The receipt_number should start on 1 for each customer and be a sequence. This means that customer_id and receipt_number will be unique. How can I elegantly do this. Can I use the built-in sequeance functionality with CREATE SEQUENCE or similar? It seems like I would have to create a sequence for each customer, which of course is not an elegant solution.

EDIT: There must be a thread-safe and idiot-secure way to do this. It should be quite a simple/common need.


回答1:


SEQUENCE does not guarantee there are no gaps. For example, one transaction might generate a new number and then abort (due to a bug or a power failure or whatever...). The next transaction would then blindly get the next number, not the one that was "lost".

It would be best if your client application did not depend on "no gaps" assumption in the firs place. You could, however, minimize gaps like this:

  1. SELECT MAX(receipt_number) FROM receipts WHERE customer_id = :ci
  2. INSERT INTO receipts(customer_id, receipt_number) VALUES (:ci, aboveresult+1), or just insert 1 if step 1 returned NULL.
  3. If step 2 returned a PK violation*, retry from the beginning.

*Because a concurrent transaction has gone through the same process and committed.

As long as rows are just added and not deleted, this should prevent any gaps, even in a concurrent environment.


BTW, you can "condense" steps 1 and 2 like this:

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

[SQL Fiddle]

The index underneath the PK {customer_id, receipt_number} should ensure that the SELECT part of this query is satisfied efficiently.




回答2:


Why do receipt numbers begin with 1 for each customer? Is that part of the defined requirements?

The simplest way to get this done is to have the program that generates new receipts query the database for max(ReceiptNumber) where CustomerId = CurrentCustomerId and then add 1.

currentCustomerId is a program variable not a database value.

This is a little inelegant in that involves an extra search of the table. You will need to create your indexes carefully, in order to get one of the indexes to answer the question without a full table scan.

An alternative that's a little quicker at insert time is to create an extra column, called MaxReeceiptNumber, in the customer table. Increment that whennever you want to insert a new receipt.




回答3:


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

This is not thread-safe, so make sure to implement error handling if two separate threads try to create a new receipt for a given customer at the same time.


EDIT

There is more to thread-safety than just avoiding race-conditions. Suppose there are two separate threads creating a new receipt for the same customer at the same time. Should it happen? Is this normal, a bug, or security breach? Suppose a bank where two tellers are creating a new record for the same customer at the same time -- something is very wrong. If this is supposed to happen, you can use locks; if not, then some kind of error is in order.




回答4:


You could use a trigger like this to update your column:

Table definition with unique constraint on 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);

Function to check for max receipt_number for the client, or 1 if no previous receipts

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';

Trigger to fire the function on each row insert:

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

Then, executing the following:

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);

should yield:

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


来源:https://stackoverflow.com/questions/12746106/how-to-create-multiple-sequences-in-one-table

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!