11

I'm writing a function in node.js to query a PostgreSQL table.
If the row exists, I want to return the id column from the row.
If it doesn't exist, I want to insert it and return the id (insert into ... returning id).

I've been trying variations of case and if else statements and can't seem to get it to work.

Erwin Brandstetter
  • 479,275
  • 111
  • 893
  • 1,042
Jared
  • 1,883
  • 4
  • 30
  • 57
  • possible duplicate of [Upsert in Postgres using node.js](http://stackoverflow.com/questions/7910174/upsert-in-postgres-using-node-js) – a_horse_with_no_name Apr 07 '12 at 20:15
  • I don't want to update anything if it exists already. I just want to return the value (in either case). – Jared Apr 07 '12 at 20:22
  • possible duplicate of [Insert if not exists, else return id in postgresql](http://stackoverflow.com/questions/18192570/insert-if-not-exists-else-return-id-in-postgresql) – Clodoaldo Neto May 09 '14 at 23:56
  • **Update** Postgres 9.5 is getting an [UPSERT](https://en.wikipedia.org/wiki/Merge_(SQL)) feature. See [blog post by Craig Kerstiens](http://www.craigkerstiens.com/2015/05/08/upsert-lands-in-postgres-9.5/). – Basil Bourque Jul 17 '15 at 02:40

4 Answers4

10

A solution in a single SQL statement. Requires PostgreSQL 8.4 or later though.
Consider the following demo:

Test setup:

CREATE TEMP TABLE tbl (
  id  serial PRIMARY KEY
 ,txt text   UNIQUE   -- obviously there is unique column (or set of columns)
);

INSERT INTO tbl(txt) VALUES ('one'), ('two');

INSERT / SELECT command:

WITH v AS (SELECT 'three'::text AS txt)
    ,s AS (SELECT id FROM tbl JOIN v USING (txt))
    ,i AS (
       INSERT INTO tbl (txt)
       SELECT txt
       FROM   v
       WHERE  NOT EXISTS (SELECT * FROM s)
       RETURNING id
       )
SELECT id, 'i'::text AS src FROM i
UNION  ALL
SELECT id, 's' FROM s;
  • The first CTE v is not strictly necessary, but achieves that you have to enter your values only once.

  • The second CTE s selects the id from tbl if the "row" exists.

  • The third CTE i inserts the "row" into tbl if (and only if) it does not exist, returning id.

  • The final SELECT returns the id. I added a column src indicating the "source" - whether the "row" pre-existed and id comes from a SELECT, or the "row" was new and so is the id.

  • This version should be as fast as possible as it does not need an additional SELECT from tbl and uses the CTEs instead.

To make this safe against possible race conditions in a multi-user environment:
Also for updated techniques using the new UPSERT in Postgres 9.5 or later:

Community
  • 1
  • 1
Erwin Brandstetter
  • 479,275
  • 111
  • 893
  • 1,042
  • Is there some way to use this pattern on an arbitrary table? I keep using something akin to this, but repeating it N times for N tables.. – Garen Sep 16 '13 at 05:09
  • @Garen: You could use [`dynamic SQL` with `EXECUTE`](http://www.postgresql.org/docs/current/interactive/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN) in a plpgsql function or `DO` statement. Try a [search](http://stackoverflow.com/search?q=execute+[plpgsql]+[dynamic-sql]+table+name) to find related answers [like this one](http://stackoverflow.com/questions/7914325/insert-with-dynamic-table-name-in-trigger-function/7915100#7915100). Be [wary of SQL injection](http://dba.stackexchange.com/questions/49699/postgres-functions-vs-prepared-queries/49718#49718). – Erwin Brandstetter Sep 16 '13 at 11:54
8

I would suggest doing the checking on the database side and just returning the id to nodejs.

Example:

CREATE OR REPLACE FUNCTION foo(p_param1 tableFoo.attr1%TYPE, p_param2 tableFoo.attr1%TYPE) RETURNS tableFoo.id%TYPE AS $$
  DECLARE
  v_id tableFoo.pk%TYPE;
  BEGIN
    SELECT id
    INTO v_id
    FROM tableFoo
    WHERE attr1 = p_param1
    AND attr2 = p_param2;

    IF v_id IS NULL THEN
      INSERT INTO tableFoo(id, attr1, attr2) VALUES (DEFAULT, p_param1, p_param2)
      RETURNING id INTO v_id;
    END IF;

    RETURN v_id:

  END;
$$ LANGUAGE plpgsql;

And than on the Node.js-side (i'm using node-postgres in this example):

var pg = require('pg');
pg.connect('someConnectionString', function(connErr, client){

  //do some errorchecking here

  client.query('SELECT id FROM foo($1, $2);', ['foo', 'bar'], function(queryErr, result){

    //errorchecking

    var id = result.rows[0].id;      

  };

});
Marc Fischer
  • 1,296
  • 12
  • 16
  • I should have mentioned that during development, I'm using the free pgsql db on Heroku that doesn't allow stored procs. Could I do this in one query some way using node-postgres or will I have to do the select in one query and then do the insert in that callback? – Jared Apr 07 '12 at 20:39
  • 1
    @Jared: I could think of a quite ugly hack. Run the query INSERT INTO t(id, a) (SELECT COALESCE((SELECT id FROM t WHERE a = $1), nextval('t_id_seq')), $1 "a") RETURNING id; If it works you get the id returned, otherwise you will receive an error, that there is a violation of the unique constraint of the primary key. You could then just parse the error message for the number. Quite ugly, but actually works. – Marc Fischer Apr 07 '12 at 21:14
  • I was able to upgrade my pgsql instance on Heroku to the public beta, which supports functions. – Jared Apr 09 '12 at 20:41
2

Something like this, if you are on PostgreSQL 9.1

with test_insert as (
   insert into foo (id, col1, col2)
   select 42, 'Foo', 'Bar'
   where not exists (select * from foo where id = 42)
   returning foo.id, foo.col1, foo.col2
)
select id, col1, col2
from test_insert
union 
select id, col1, col2
from foo
where id = 42;

It's a bit longish and you need to repeat the id to test for several times, but I can't think of a different solution that involves a single SQL statement.

If a row with id=42 exists, the writeable CTE will not insert anything and thus the existing row will be returned by the second union part.

When testing this I actually thought the new row would be returned twice (therefor a union not a union all) but it turns out that the result of the second select statement is actually evaluated before the whole statement is run and it does not see the newly inserted row. So in case a new row is inserted, it will be taken from the "returning" part.

a_horse_with_no_name
  • 440,273
  • 77
  • 685
  • 758
0
create table t (
    id serial primary key,
    a integer
)
;

insert into t (a)
select 2
from (
    select count(*) as s
    from t
    where a = 2
    ) s
where s.s = 0
;
select id
from t
where a = 2
;
Clodoaldo Neto
  • 98,807
  • 21
  • 191
  • 235