Playing with upsert


It is not news that every new version of PostgreSQL provides multiple features. In this post we will explore Upsert, one of the few major features that was missing from this amazing database.

In simple terms, upsert is nothing more than an insert with new possibilities in case of a conflict. It is worth to remember that we can always refer to the amazing documentation that is provided when in doubt.

To start our exploration, let’s create a simple table with a few records:

CREATE TABLE games (
  game_id serial PRIMARY KEY,
  name varchar(100) NOT NULL UNIQUE,
  price integer NOT NULL
);

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 50),
       ('Tales of Abyss', 35),
       ('Breath of Fire IV', 20);

select * from games;

 game_id |         name          | price
---------+-----------------------+-------
       1 | Final Fantasy IX      |    50
       2 | Tales of Abyss        |    35
       3 | Breath of Fire IV     |    20
(3 rows)

Let’s begin with a traditional insert with a conflict.

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 50);

ERROR:  duplicate key value violates unique constraint "games_name_key"
DETAIL:  Key (name)=(Final Fantasy IX) already exists.

We got an error as expected since we already have this name in our table. Now suppose a new business rule specified that conflicts like this should be ignored, we could rescue (or catch) an exception in our application and life goes on, but now we have another option:

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 50)
ON CONFLICT DO NOTHING;
INSERT 0 0

The INSERT 0 0 hints us that nothing was inserted, which is exactly what we wanted. Everyone is happy for a while, but the rules change and now in case of a conflict the price need to be updated! Fear not, is what you say to your manager:

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 75)
ON CONFLICT (name)
DO UPDATE SET price = EXCLUDED.price;
INSERT 0 1
Time: 1.786 ms

SELECT * FROM games WHERE name = 'Final Fantasy IX';
 game_id |       name       | price
---------+------------------+-------
       1 | Final Fantasy IX |    75

Notice that we use the EXCLUDED keyword to specify the updated price. In this case we also specified the type of conflict, if a different conflict occurs it will not be “rescued” as we can see below:

INSERT INTO games(game_id, name, price)
VALUES (1, 'New Game', 10)
ON CONFLICT (name)
DO UPDATE SET price = EXCLUDED.price;

ERROR:  duplicate key value violates unique constraint "games_pkey"
DETAIL:  Key (game_id)=(1) already exists.

Suddenly the players start complaining that the games are getting expensive and by consequence the sales in our store are decreasing, the new rule is that the price should be updated only if the game gets cheaper.

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 80)
ON CONFLICT (name)
DO UPDATE SET price = EXCLUDED.price
WHERE EXCLUDED.price < games.price;
INSERT 0 0

SELECT * FROM games WHERE name = 'Final Fantasy IX';
 game_id |       name       | price
---------+------------------+-------
       1 | Final Fantasy IX |    75

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 60)
ON CONFLICT (name)
DO UPDATE SET price = EXCLUDED.price
WHERE EXCLUDED.price < games.price;
INSERT 0 1

SELECT * FROM games WHERE name = 'Final Fantasy IX';
 game_id |       name       | price
---------+------------------+-------
       1 | Final Fantasy IX |    60

We can specify the name of our constraint in ON CONFLICT, but it is recommended to use column names and let postgres specify the type of the index for you. Let’s see an example just in case:

\dS games
                                    Table "public.games"
 Column  |          Type          |                        Modifiers
---------+------------------------+---------------------------------------------------------
 game_id | integer                | not null default
nextval('games_game_id_seq'::regclass)
 name    | character varying(100) | not null
 price   | integer                | not null
Indexes:
    "games_pkey" PRIMARY KEY, btree (game_id)
    "games_name_key" UNIQUE CONSTRAINT, btree (name)

INSERT INTO games(name, price)
VALUES ('Final Fantasy IX', 50)
ON CONFLICT ON CONSTRAINT games_name_key DO NOTHING;
INSERT 0 0

If we had a composite index or a partial index we could also specify a condition in our ON CONFLICT clause. For example, if we had a composite index on (name, year), we could do:

INSERT INTO games(name, price)
VALUES ('Final Fantasy XV', 100)
ON CONFLICT (name, year)
WHERE year = 2016
DO NOTHING;

That’s it for now, if you have further questions or suggestions feel free to leave me a tweet or email so we can discuss things further!

See you in the next post!