在Oracle中生成唯一和连续数字的最佳方法

时间:2009-12-31 16:39:10

标签: sql oracle plsql

我需要以快速可靠的方式生成唯一且连续的数字(用于发票)。目前使用Oracle序列,但在某些情况下,由于可能发生的异常,生成的数字不连续

我想了几个解决方案来解决这个问题,但他们都没有说服我。你推荐什么解决方案?

  1. 使用select max()

    SELECT MAX (NVL (doc_num, 0)) +1 FROM invoices
    
  2. 使用表格存储为发票生成的最后一个数字。

    UPDATE docs_numbers
        SET last_invoice = last_invoice + 1
    
  3. 另一种解决方案?

10 个答案:

答案 0 :(得分:7)

正如他所建议的,你应该真正检讨“无间隙”要求的必要性

答案 1 :(得分:3)

如果事务使用序列号但随后回滚,则会出现间隙。

也许答案不是在发票无法回滚之前分配发票号。这最大限度地减少了(但可能没有消除)间隙的可能性。

我不确定是否有任何快速或简单的方法来确保序列中没有间隙 - 扫描MAX,添加一个,插入可能是最接近安全的,但不建议出于性能原因(和并发困难)并且该技术不会检测是否分配了最新的发票号,然后删除并重新分配。

你能否以某种方式解决差距 - 通过确定哪些发票号码被“使用”但是“不是永久性的”某种方式?自治交易可以帮助做到这一点吗?


另一种可能性 - 假设差距相对较小且很远。

创建一个表,记录在抓取新序列值之前必须重复使用的序列号。通常情况下,它会为空,但是每隔一分钟,每小时,每天运行一些进程...检查间隙并将错过的值插入此表中。所有进程首先检查错过的值表,如果有任何存在,请使用其中的值,经历更新表的缓慢过程并删除它们使用的行。如果表为空,则抓取下一个序列号。

不是很愉快,但是“发送发票编号”与“扫描错过的值”的解耦意味着即使某个线程在使用其中一个错过的值时发票处理失败,该值也会被重新发现下次遗失并重新重新发行 - 重复直到某个过程成功。

答案 2 :(得分:1)

保持当前序列 - 您可以使用以下内容将值重置为表中当前存储的最大值:

-- --------------------------------
-- Purpose..: Resets the sequences 
-- --------------------------------

DECLARE
  -- record of temp data table
  TYPE data_rec_type IS RECORD(
    sequence_name VARCHAR2(30),
    table_name    VARCHAR2(30),
    column_name   VARCHAR2(30));

  -- temp data table
  TYPE data_table_type IS TABLE OF data_rec_type INDEX BY BINARY_INTEGER;

  v_data_table data_table_type;
  v_index      NUMBER;
  v_tmp_id     NUMBER;

  -- add row to temp table for later processing
  --
  PROCEDURE map_seq_to_col(in_sequence_name VARCHAR2,
                           in_table_name    VARCHAR2,
                           in_column_name   VARCHAR2) IS
    v_i_index NUMBER;
  BEGIN
    v_i_index := v_data_table.COUNT + 1;
    v_data_table(v_i_index).sequence_name := in_sequence_name;
    v_data_table(v_i_index).table_name := in_table_name;
    v_data_table(v_i_index).column_name := in_column_name;
  END;

  /**************************************************************************
      Resets a sequence to a given value
  ***************************************************************************/
  PROCEDURE reset_seq(in_seq_name VARCHAR2, in_new_value NUMBER) IS

    v_sql       VARCHAR2(2000);
    v_seq_name  VARCHAR2(30) := in_seq_name;
    v_reset_val NUMBER(10);
    v_old_val   NUMBER(10);
    v_new_value NUMBER(10);

  BEGIN

    -- get current sequence value

    v_sql := 'SELECT ' || v_seq_name || '.nextval FROM DUAL';
    EXECUTE IMMEDIATE v_sql
      INTO v_old_val;

    -- handle empty value
    v_new_value := in_new_value;
    if v_new_value IS NULL then
      v_new_value := 0;
    END IF;

    IF v_old_val <> v_new_value then    
      IF v_old_val > v_new_value then
        -- roll backwards
        v_reset_val := (v_old_val - v_new_value) * -1;
      elsif v_old_val < v_new_value then
        v_reset_val := (v_new_value - v_old_val);
      end if;

      -- make the sequence rollback to 0 on the next call
      v_sql := 'alter sequence ' || v_seq_name || ' increment by ' ||
           v_reset_val || ' minvalue 0';
      EXECUTE IMMEDIATE (v_sql);

      -- select from the sequence to make it roll back
      v_sql := 'SELECT ' || v_seq_name || '.nextval FROM DUAL';
      EXECUTE IMMEDIATE v_sql
        INTO v_reset_val;

      -- make it increment correctly again
      v_sql := 'alter sequence ' || v_seq_name || ' increment by 1';
      EXECUTE IMMEDIATE (v_sql);

      -- select from it again to prove it reset correctly.
      v_sql := 'SELECT ' || v_seq_name || '.currval FROM DUAL';
      EXECUTE IMMEDIATE v_sql
        INTO v_reset_val;

    END IF;

    DBMS_OUTPUT.PUT_LINE(v_seq_name || ': ' || v_old_val || ' to ' ||
                     v_new_value);
  END;

  /*********************************************************************************************
    Retrieves a max value for a table and then calls RESET_SEQ.
  *********************************************************************************************/
  PROCEDURE reset_seq_to_table(in_sequence_name VARCHAR2,
                               in_table_name    VARCHAR2,
                               in_column_name   VARCHAR2) IS

    v_sql_body  VARCHAR2(2000);
    v_max_value NUMBER;

      BEGIN

    -- get max value in the table
    v_sql_body := 'SELECT MAX(' || in_column_name || '+0) FROM ' ||
              in_table_name;
    EXECUTE IMMEDIATE (v_sql_body)
      INTO v_max_value;

    if v_max_value is null then
      -- handle empty tables
      v_max_value := 0;
    end if;

    -- use max value to reset the sequence
    RESET_SEQ(in_sequence_name, v_max_value);

  EXCEPTION
    WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE('Failed to reset ' || in_sequence_name ||
                       ' from ' || in_table_name || '.' ||
                       in_column_name || ' - ' || sqlerrm);
  END;

BEGIN
  --DBMS_OUTPUT.ENABLE(1000000);

  -- load sequence/table/column associations

  /***** START SCHEMA CUSTOMIZATION *****/
  map_seq_to_col('Your_SEQ',  
                 'your_table',
                 'the_invoice_number_column');

  /***** END SCHEMA CUSTOMIZATION *****/

  -- iterate all sequences that require a reset
  FOR v_index IN v_data_table.FIRST .. v_data_table.LAST LOOP

    BEGIN
      RESET_SEQ_TO_TABLE(v_data_table(v_index).sequence_name,
                         v_data_table(v_index).table_name,
                         v_data_table(v_index).column_name);
    END;
  END LOOP;

END;
/

-- -------------------------------------------------------------------------------------
-- End of Script.
-- -------------------------------------------------------------------------------------

示例是一个匿名的sproc - 将其更改为包中的正确程序,并在插入新发票之前调用它以保持编号一致。

答案 3 :(得分:1)

由于“可能发生的例外情况”,您的意思并不清楚。如果你希望数字不会增加,如果你的交易最终回滚,那么SEQUENCE对你不起作用,因为据我所知,一旦从序列请求NEXTVAL,序列位置就会增加,回滚也不会反转它。

如果这确实是一个要求,那么你可能不得不求助于将当前计数器存储在一个单独的表中,但要注意并发更新 - 来自'丢失更新'和可扩展性预期。

答案 4 :(得分:1)

我认为您会发现使用现有数字的MAX()容易出现一个新的令人兴奋的问题 - 如果同时创建多个发票,可能会出现重复问题。 (不要问我怎么知道......)。

一种可能的解决方案是从序列中导出INVOICE表上的主键,但这不是发票号。正确并正确创建发票后,在异常或用户突发奇想可能导致终止发票创建之后,您将转到第二个序列以获取序列号,该序号显示为“发票号” 。这意味着你的INVOICE表上会有两个唯一的,不重复的数字,而明显的一个(INVOICE_NO)将不是主键(但它可以而且应该是唯一的)所以有一些邪恶的悄悄进入,但是替代方案 - 即在主键中创建一个具有一个值的INVOICE行,然后在创建INVOICE之后更改主键 - 对于单词来说太邪恶了。 : - )

分享并享受。

答案 5 :(得分:1)

如果你真的想要没有差距,你需要完全序列化访问,否则总会有差距。差距的原因是:

  • 回滚
  • shutdown abort

答案 6 :(得分:1)

我之前遇到过这个问题。在一个案例中,我们能够说服企业接受“真实”发票可能存在差距,我们写了一份工作,每天都要用“无效”发票“填补”差距用于审计目的。

在实践中,如果我们将NOCACHE放在序列上,那么差距的数量会相对较低,因此只要对“无效”发票的查询不会返回太多结果,审计员通常会感到满意。 / p>

答案 7 :(得分:0)

您可能需要重新考虑您的流程并将其分解为更多步骤。有一个非事务性步骤创建占位符发票(这不在交易中应该消除差距),然后在交易中执行剩余的业务。我认为这就是我们在多年前被困在一个系统中的方式,但我记不起来了 - 我只记得它“很奇怪。”

我会说序列将保证唯一/连续的数字,但是当您在混合中抛出事务时,除非序列生成不在该事务中,否则无法保证。

答案 8 :(得分:0)

dpbradley在#2中的链接听起来是你最好的选择。 Tom保持与调用者的事务性,如果你不希望你可以像以下那样使它成为一个自治事务:

create or replace 
function getNextInvoiceNumber()
return number is
   l_invoicenum     number;

   pragma autonomous_transaction;
   begin
      update docs_numbers
         set last_invoice = last_invoice + 1
      returning last_invoice 
      into l_invoicenum;
      commit;

      return l_invoicenum;

   exception
      when others then
         rollback;
         raise;
end;

答案 9 :(得分:0)

我们所做的是向交易发出一个序列号,然后当我们正在处理的项目最终确定时,我们发出一个永久号码(也是一个序列)。 适合我们。

问候
ķ