6

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.

4

5 回答 5

3

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.

于 2012-10-05T16:31:03.730 回答
1

enter image description here

-- 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.

于 2012-10-05T12:50:12.810 回答
1

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.

于 2012-10-05T12:51:27.030 回答
1

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
于 2012-10-05T13:07:13.550 回答
0

I would like to propose my solution to this problem - use +1 column on the customer table to store latest_receipt_id, and use incremental function 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;
$$;

Then you can either use it in the receipt INSERT trigger:

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

OR inside your ORM (pseudocode):

# 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)' )

Disregarding of any concurrency on inserts you will always have sequential ids.

Cheers!

于 2020-06-25T16:16:04.663 回答