简体   繁体   中英

Two column primary keys, auto-increment column depending on value of the 2nd column

I am having a problem with writing sql for Firebird. What i want to achieve:

table_id   database_id     other_columns
1          1
2          1
3          1
1          2
2          2

Where the table_id is the auto-increment part and the database_id is the second part.

Basically like this MySQL solution but using Firebird: mysql two column primary key with auto-increment

How do I create a table and how do I insert into it?

This is not so simple to do in Firebird as it is in MySQL. If the number of database_id is known in advance, you can allocate a sequence for each id and use that in a trigger, but this quickly becomes unwieldy for a large number of ids.

The rest of my answer assumes the use of Firebird 2.5 (I have tested it with Firebird 2.5.2 Update 1).

If we only have database_id s 1 and 2, we can create two sequences:

CREATE SEQUENCE multisequence_1;
CREATE SEQUENCE multisequence_1;

We need an exception when an id is used that has no sequence:

CREATE OR ALTER EXCEPTION no_sequence 'No corresponding sequence found';

We can then use the following trigger:

CREATE OR ALTER TRIGGER multisequence_BI FOR multisequence
   ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
   IF (NEW.database_id = 1) THEN
       NEW.table_id = NEXT VALUE FOR multisequence_1;
   ELSE IF (NEW.database_id = 2) THEN
       NEW.table_id = NEXT VALUE FOR multisequence_2;
   ELSE 
       EXCEPTION no_sequence;
END

As you can see this will quickly lead to a lot of IF/ELSE statements. This can probably be simplified by using EXECUTE STATEMENT and a dynamically generated query for the next sequence value. This won't work if you cannot control the number of database_id values (and their sequences) in advance.

You can try to solve this using dynamic queries as shown below. This might have its own problems (especially if there is a high volume of inserts) because EXECUTE STATEMENT has some overhead, and it might also lead to problems because of the use of dynamic DDL (eg lock/update-conflicts on the metadata tables).

CREATE OR ALTER TRIGGER multisequence_BI FOR multisequence
   ACTIVE BEFORE INSERT POSITION 0
AS
   DECLARE new_id INTEGER;
   DECLARE get_sequence VARCHAR(255);
BEGIN
    get_sequence = 'SELECT NEXT VALUE FOR multisequence_' || NEW.database_id || 
         ' FROM RDB$DATABASE';
    BEGIN
        EXECUTE STATEMENT get_sequence INTO :new_id;
        WHEN SQLCODE -104 DO
        BEGIN
            EXECUTE STATEMENT 
                'CREATE SEQUENCE multisequence_' || NEW.database_id 
                WITH AUTONOMOUS TRANSACTION;
            EXECUTE STATEMENT get_sequence INTO :new_id;
        END
    END
    NEW.table_id = new_id;
END

This code is still susceptible to multiple transaction trying to create the same sequence. Adding a WHEN ANY DO after the statement that (attempted to) create the sequence, might allow you to use the sequence anyway, but it might also lead to spurious errors like lock conflicts. Also note that using DDL in EXECUTE STATEMENT is discouraged (see the warning in the documentation ).

Before using this solution in a production situation, I'd strongly suggest to thoroughly test this under load!

Note that the WITH AUTONOMOUS TRANSACTION clause is technically not necessary for creating the sequence, but it is required to ensure the sequence is also visible to other transactions (and doesn't get deleted if the original transaction is rolled back).

Also be aware of the maximum number of sequences (or: generators) in a single Firebird database: +/- 32758, see Firebird Generator Guide: How many generators are available in one database? .

The "table_id" field is really a simple row numbering based on record insertion order and partitioned by "database_id". The first "database_id" foo gets a "table_id" of 1, the second foo a 2, the first bar a 1, the second bar a 2, etc.

This can be dynamically computed, if you have some way of knowing the order of row insertions for each "database_id". A conventional auto-increment column, applied across all rows, gives you that ordering. Then the computation can be hidden behind a VIEW and your application need be none the wiser.

Partitioned row numbers are easy to express with SQL window functions, which are supported in Firebird 3 if I'm not mistaken:

SELECT ROW_NUMBER() OVER (PARTITION BY "database_id" ORDER BY auto-increment-column ) AS "table_id"

For Firebird 2, you can compute it yourself by asking, for each distinct "database_id", how many rows precede it:

   SELECT COUNT(b.auto_inc_id) + 1 AS table_id,
          a.database_id
     FROM tbl a
LEFT JOIN tbl b
          ON a.database_id = b.database_id AND b.auto_inc_id < a.auto_inc_id
 GROUP BY 2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM