is there a way to avoid calling nextval() if the insert fails in PostgreSQL?

前端 未结 3 443
夕颜
夕颜 2021-01-12 05:20

In a PostgreSQL database I have a table with a primary key and another field which needs to be unique.

  CREATE TABLE users (
    id      INTEGER PRIMARY KEY         


        
3条回答
  •  时光说笑
    2021-01-12 06:13

    It is possible, though cumbersome, to do this. As bortzmeyer says, it's dangerous to rely on values from sequences being contiguous, so it's best to just leave things as they are if you can.

    If you can't:

    Every access to the table that could cause a row to have a certain name (that is, every INSERT to that table, and if you allow it (though it's poor practice) every UPDATE that could change the name field) must do so inside a transaction that locks soemthing first. The simplest and least performant option is to simply lock the entire table using LOCK users IN EXCLUSIVE MODE (adding the last 3 words permits concurrent read access by other processes, which is safe).

    However that is a very coarse lock that will slow performance if there are many concurrent modifications to users; a better option would be locking a single, corresponding row in another table that must already exist. This row can be locked with SELECT ... FOR UPDATE. This makes sense only when working with a "child" table that has a FK dependency on another "parent" table.

    For example, imagine for the time being that we are actually trying to safely create new orders for a customer, and that these orders somehow have identifying 'names'. (I know, poor example...) orders has a FK dependency on customers. Then to prevent ever creating two orders with the same name for a given customer, you could do the following:

    BEGIN;
    
    -- Customer 'jbloggs' must exist for this to work.  
    SELECT 1 FROM customers
    WHERE id = 'jbloggs'
    FOR UPDATE
    
    -- Provided every attempt to create an order performs the above step first,
    -- at this point, we will have exclusive access to all orders for jbloggs.
    SELECT 1 FROM orders
    WHERE id = 'jbloggs'
    AND order_name = 'foo'
    
    -- Determine if the preceding query returned a row or not.
    -- If it did not:
    INSERT orders (id, name) VALUES ('jbloggs', 'foo');
    
    -- Regardless, end the transaction:
    END;
    

    Note that it is not sufficient to simply lock the corresponding row in users with SELECT ... FOR UPDATE -- if the row does not already exist, several concurrent processes may simultaneously report that the row does not exist, and then attempt simultaneous insertions, resulting in failed transactions and thus sequence gaps.

    Either locking scheme will work; what's important is that anyone trying to create a row with the same name must attempt to lock the same object.

提交回复
热议问题