Insert, on duplicate update in PostgreSQL?

前端 未结 16 2273
别那么骄傲
别那么骄傲 2020-11-21 04:52

Several months ago I learned from an answer on Stack Overflow how to perform multiple updates at once in MySQL using the following syntax:

INSERT INTO table          


        
相关标签:
16条回答
  • 2020-11-21 05:30

    I was looking for the same thing when I came here, but the lack of a generic "upsert" function botherd me a bit so I thought you could just pass the update and insert sql as arguments on that function form the manual

    that would look like this:

    CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
        RETURNS VOID
        LANGUAGE plpgsql
    AS $$
    BEGIN
        LOOP
            -- first try to update
            EXECUTE sql_update;
            -- check if the row is found
            IF FOUND THEN
                RETURN;
            END IF;
            -- not found so insert the row
            BEGIN
                EXECUTE sql_insert;
                RETURN;
                EXCEPTION WHEN unique_violation THEN
                    -- do nothing and loop
            END;
        END LOOP;
    END;
    $$;
    

    and perhaps to do what you initially wanted to do, batch "upsert", you could use Tcl to split the sql_update and loop the individual updates, the preformance hit will be very small see http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php

    the highest cost is executing the query from your code, on the database side the execution cost is much smaller

    0 讨论(0)
  • 2020-11-21 05:31

    According the PostgreSQL documentation of the INSERT statement, handling the ON DUPLICATE KEY case is not supported. That part of the syntax is a proprietary MySQL extension.

    0 讨论(0)
  • 2020-11-21 05:31

    Edit: This does not work as expected. Unlike the accepted answer, this produces unique key violations when two processes repeatedly call upsert_foo concurrently.

    Eureka! I figured out a way to do it in one query: use UPDATE ... RETURNING to test if any rows were affected:

    CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);
    
    CREATE FUNCTION update_foo(k INT, v TEXT)
    RETURNS SETOF INT AS $$
        UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
    $$ LANGUAGE sql;
    
    CREATE FUNCTION upsert_foo(k INT, v TEXT)
    RETURNS VOID AS $$
        INSERT INTO foo
            SELECT $1, $2
            WHERE NOT EXISTS (SELECT update_foo($1, $2))
    $$ LANGUAGE sql;
    

    The UPDATE has to be done in a separate procedure because, unfortunately, this is a syntax error:

    ... WHERE NOT EXISTS (UPDATE ...)
    

    Now it works as desired:

    SELECT upsert_foo(1, 'hi');
    SELECT upsert_foo(1, 'bye');
    SELECT upsert_foo(3, 'hi');
    SELECT upsert_foo(3, 'bye');
    
    0 讨论(0)
  • 2020-11-21 05:32

    I custom "upsert" function above, if you want to INSERT AND REPLACE :

    `

     CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)
    
     RETURNS void AS
     $BODY$
     BEGIN
        -- first try to insert and after to update. Note : insert has pk and update not...
    
        EXECUTE sql_insert;
        RETURN;
        EXCEPTION WHEN unique_violation THEN
        EXECUTE sql_update; 
        IF FOUND THEN 
            RETURN; 
        END IF;
     END;
     $BODY$
     LANGUAGE plpgsql VOLATILE
     COST 100;
     ALTER FUNCTION upsert(text, text)
     OWNER TO postgres;`
    

    And after to execute, do something like this :

    SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
    

    Is important to put double dollar-comma to avoid compiler errors

    • check the speed...
    0 讨论(0)
  • 2020-11-21 05:33

    There is no simple command to do it.

    The most correct approach is to use function, like the one from docs.

    Another solution (although not that safe) is to do update with returning, check which rows were updates, and insert the rest of them

    Something along the lines of:

    update table
    set column = x.column
    from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
    where table.id = x.id
    returning id;
    

    assuming id:2 was returned:

    insert into table (id, column) values (1, 'aa'), (3, 'cc');
    

    Of course it will bail out sooner or later (in concurrent environment), as there is clear race condition in here, but usually it will work.

    Here's a longer and more comprehensive article on the topic.

    0 讨论(0)
  • 2020-11-21 05:34

    Warning: this is not safe if executed from multiple sessions at the same time (see caveats below).


    Another clever way to do an "UPSERT" in postgresql is to do two sequential UPDATE/INSERT statements that are each designed to succeed or have no effect.

    UPDATE table SET field='C', field2='Z' WHERE id=3;
    INSERT INTO table (id, field, field2)
           SELECT 3, 'C', 'Z'
           WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
    

    The UPDATE will succeed if a row with "id=3" already exists, otherwise it has no effect.

    The INSERT will succeed only if row with "id=3" does not already exist.

    You can combine these two into a single string and run them both with a single SQL statement execute from your application. Running them together in a single transaction is highly recommended.

    This works very well when run in isolation or on a locked table, but is subject to race conditions that mean it might still fail with duplicate key error if a row is inserted concurrently, or might terminate with no row inserted when a row is deleted concurrently. A SERIALIZABLE transaction on PostgreSQL 9.1 or higher will handle it reliably at the cost of a very high serialization failure rate, meaning you'll have to retry a lot. See why is upsert so complicated, which discusses this case in more detail.

    This approach is also subject to lost updates in read committed isolation unless the application checks the affected row counts and verifies that either the insert or the update affected a row.

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