postgresql generate sequence with no gap

前端 未结 4 697
失恋的感觉
失恋的感觉 2020-11-28 15:37

I must / have to create unique ID for invoices. I have a table id and another column for this unique number. I use serialization isolation level. Using

  var         


        
相关标签:
4条回答
  • 2020-11-28 15:47

    Sequences do not generate gap-free sets of numbers, and there's really no way of making them do that because a rollback or error will "use" the sequence number.

    I wrote up an article on this a while ago. It's directed at Oracle but is really about the fundamental principles of gap-free numbers, and I think the same applies here.

    Well, it’s happened again. Someone has asked how to implement a requirement to generate a gap-free series of numbers and a swarm of nay-sayers have descended on them to say (and here I paraphrase slightly) that this will kill system performance, that’s it’s rarely a valid requirement, that whoever wrote the requirement is an idiot blah blah blah.

    As I point out on the thread, it is sometimes a genuine legal requirement to generate gap-free series of numbers. Invoice numbers for the 2,000,000+ organisations in the UK that are VAT (sales tax) registered have such a requirement, and the reason for this is rather obvious: that it makes it more difficult to hide the generation of revenue from tax authorities. I’ve seen comments that it is a requirement in Spain and Portugal, and I’d not be surprised if it was not a requirement in many other countries.

    So, if we accept that it is a valid requirement, under what circumstances are gap-free series* of numbers a problem? Group-think would often have you believe that it always is, but in fact it is only a potential problem under very particular circumstances.

    1. The series of numbers must have no gaps.
    2. Multiple processes create the entities to which the number is associated (eg. invoices).
    3. The numbers must be generated at the time that the entity is created.

    If all of these requirements must be met then you have a point of serialisation in your application, and we’ll discuss that in a moment.

    First let’s talk about methods of implementing a series-of-numbers requirement if you can drop any one of those requirements.

    If your series of numbers can have gaps (and you have multiple processes requiring instant generation of the number) then use an Oracle Sequence object. They are very high performance and the situations in which gaps can be expected have been very well discussed. It is not too challenging to minimise the amount of numbers skipped by making design efforts to minimise the chance of a process failure between generation of the number and commiting the transaction, if that is important.

    If you do not have multiple processes creating the entities (and you need a gap-free series of numbers that must be instantly generated), as might be the case with the batch generation of invoices, then you already have a point of serialisation. That in itself may not be a problem, and may be an efficient way of performing the required operation. Generating the gap-free numbers is rather trivial in this case. You can read the current maximum value and apply an incrementing value to every entity with a number of techniques. For example if you are inserting a new batch of invoices into your invoice table from a temporary working table you might:

    insert into
      invoices
        (
        invoice#,
        ...)
    with curr as (
      select Coalesce(Max(invoice#)) max_invoice#
      from   invoices)
    select
      curr.max_invoice#+rownum,
      ...
    from
      tmp_invoice
      ...
    

    Of course you would protect your process so that only one instance can run at a time (probably with DBMS_Lock if you're using Oracle), and protect the invoice# with a unique key contrainst, and probably check for missing values with separate code if you really, really care.

    If you do not need instant generation of the numbers (but you need them gap-free and multiple processes generate the entities) then you can allow the entities to be generated and the transaction commited, and then leave generation of the number to a single batch job. An update on the entity table, or an insert into a separate table.

    So if we need the trifecta of instant generation of a gap-free series of numbers by multiple processes? All we can do is to try to minimise the period of serialisation in the process, and I offer the following advice, and welcome any additional advice (or counter-advice of course).

    1. Store your current values in a dedicated table. DO NOT use a sequence.
    2. Ensure that all processes use the same code to generate new numbers by encapsulating it in a function or procedure.
    3. Serialise access to the number generator with DBMS_Lock, making sure that each series has it’s own dedicated lock.
    4. Hold the lock in the series generator until your entity creation transaction is complete by releasing the lock on commit
    5. Delay the generation of the number until the last possible moment.
    6. Consider the impact of an unexpected error after generating the number and before the commit is completed — will the application rollback gracefully and release the lock, or will it hold the lock on the series generator until the session disconnects later? Whatever method is used, if the transaction fails then the series number(s) must be “returned to the pool”.
    7. Can you encapsulate the whole thing in a trigger on the entity’s table? Can you encapsulate it in a table or other API call that inserts the row and commits the insert automatically?

    Original article

    0 讨论(0)
  • 2020-11-28 15:53

    You either lock the table to inserts, and/or need to have retry code. There's no other option available. If you stop to think about what can happen with:

    1. parallel processes rolling back
    2. locks timing out

    you'll see why.

    0 讨论(0)
  • 2020-11-28 16:05

    In 2006, someone posted a gapless-sequence solution to the PostgreSQL mailing list: http://www.postgresql.org/message-id/44E376F6.7010802@seaworthysys.com

    0 讨论(0)
  • 2020-11-28 16:10

    You could create a sequence with no cache , then get the next value from the sequence and use that as your counter.

    CREATE SEQUENCE invoice_serial_seq START 101 CACHE 1;
    SELECT nextval('invoice_serial_seq');
    

    More info here

    0 讨论(0)
提交回复
热议问题