6

テーブル「領収書」があります。customer_id (レシートを持っている人) とレシート番号の列があります。領収書番号は、顧客ごとに 1 から始まり、連続している必要があります。これは、顧客 ID と領収書番号が一意であることを意味します。どうすればこれをエレガントに行うことができますか。組み込みのシーケンス機能を CREATE SEQUENCE などで使用できますか? もちろん、これは洗練されたソリューションではありません。

編集:これを行うには、スレッドセーフで馬鹿げた安全な方法が必要です。それは非常に単純で一般的な必要性であるはずです。

4

5 に答える 5

3

SEQUENCE は、ギャップがないことを保証しません。たとえば、1 つのトランザクションが新しい番号を生成し、その後 (バグや電源障害などにより) 中止する場合があります。次のトランザクションは、「失われた」番号ではなく、盲目的に次の番号を取得します。

クライアント アプリケーションが最初から「ギャップがない」という仮定に依存していないことが最善です。ただし、次のようにギャップを最小限に抑えることができます。

  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 違反*が返された場合は、最初からやり直してください。

* 並行トランザクションが同じプロセスを経てコミットされたため。

行が追加されるだけで削除されない限り、並行環境であってもギャップを防ぐことができます。


ところで、次のようにステップ 1 と 2を「要約」できます。

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

[SQLフィドル]

PK {customer_id, receive_number} の下のインデックスは、このクエリの SELECT 部分が効率的に満たされるようにする必要があります。

于 2012-10-05T16:31:03.730 に答える
1

次のようなトリガーを使用して、列を更新できます。

customer_id、recipe_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);

クライアントの最大領収書番号をチェックする関数、または以前の領収書がない場合は 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
于 2012-10-05T13:07:13.550 に答える
1

顧客ごとに受付番号が 1 から始まるのはなぜですか? それは定義された要件の一部ですか?

これを行う最も簡単な方法は、新しいレシートを生成するプログラムで、データベースに対して max(ReceiptNumber) を照会するようにすることです。ここで、CustomerId = CurrentCustomerId であり、1 を追加します。

currentCustomerId は、データベース値ではなくプログラム変数です。

これは、テーブルの余分な検索を伴うという点で少し洗練されていません。テーブル全体をスキャンせずにインデックスの 1 つを取得して質問に答えるには、慎重にインデックスを作成する必要があります。

挿入時の時間を少し短縮する別の方法は、顧客テーブルに MaxReeceiptNumber という追加の列を作成することです。新しいレシートを挿入したくないときはいつでもインクリメントしてください。

于 2012-10-05T12:51:27.030 に答える
1

ここに画像の説明を入力

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

これはスレッドセーフではないため、2 つの別々のスレッドが特定の顧客の新しいレシートを同時に作成しようとした場合は、必ずエラー処理を実装してください。


編集

スレッドセーフには、競合状態を回避するだけではありません。同じ顧客の新しい領収書を同時に作成する 2 つの別個のスレッドがあるとします。それは起こるべきですか?これは正常ですか、バグですか、それともセキュリティ違反ですか? 2 人の窓口担当者が同時に同じ顧客の新しいレコードを作成している銀行を考えてみましょう。何かが非常に間違っています。これが発生することになっている場合は、ロックを使用できます。そうでない場合は、何らかのエラーが発生しています。

于 2012-10-05T12:50:12.810 に答える