Postgres: INSERT, если еще не существует

362

Я использую Python для записи в базу данных postgres:

sql_string = "INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred + ", '" + hundred_slug + "', " + status + ");"
cursor.execute(sql_string)

Но поскольку некоторые из моих строк идентичны, я получаю следующую ошибку:

psycopg2.IntegrityError: duplicate key value  
  violates unique constraint "hundred_pkey"

Как я могу написать оператор SQL INSERT, если эта строка уже не существует?

Я видел сложные заявления, как это рекомендуется:

IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

Но, во-первых, является ли это излишним для того, что мне нужно, а во-вторых, как я могу выполнить один из них в виде простой строки?

AP257
источник
56
Независимо от того, как вы решаете эту проблему, вы не должны генерировать свой запрос таким образом. Используйте параметры в своем запросе и передавайте значения отдельно; см. stackoverflow.com/questions/902408/…
Томас Воутерс
3
Почему бы не поймать исключение и игнорировать его?
Мэтью Митчелл
5
Начиная с Posgres 9.5 (в настоящее время на бета2) появилась новая функция, похожая на upsert, см. Postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno
2
Рассматривали ли вы принять ответ на это? =]
Релеквест

Ответы:

514

Postgres 9.5 (выпущен с 2016-01-07) предлагает команду «upsert» , также известную как предложение ON CONFLICT для INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Он решает многие тонкие проблемы, с которыми вы можете столкнуться при использовании параллельной операции, что предлагают некоторые другие ответы.

Арье
источник
14
9,5 вышел.
luckydonald
2
@TusharJain до PostgreSQL 9.5 вы можете сделать «старомодный» UPSERT (с CTE), но у вас могут возникнуть проблемы с условиями гонки, и он не будет работать как стиль 9.5. В этом блоге есть хорошая информация об upsert (в обновленной области внизу), включая некоторые ссылки, если вы хотите узнать больше о деталях.
Skyguard
17
Для тех, кто нуждается, вот два простых примера. (1) ВСТАВИТЬ, если не существует, иначе НИЧЕГО - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) ВСТАВИТЬ, если не существует, иначе ОБНОВЛЕНИЕ - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Эти примеры взяты из руководства - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan
13
Есть один нюанс / побочный эффект. В таблице со столбцом последовательности (последовательный или большой), даже если строка не вставлена, последовательность увеличивается при каждой попытке вставки.
Grzegorz Luczywo
2
Было бы лучше ссылаться на документацию INSERT, а не указывать на выпуск. Ссылка на документ: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo
379

Как я могу написать оператор SQL INSERT, если эта строка уже не существует?

Есть хороший способ сделать условную INSERT в PostgreSQL:

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

CAVEAT Этот подход не является на 100% надежным для одновременных операций записи. Существует очень крошечное состояние гонки между SELECTв NOT EXISTSборьбе с Полусоединением и INSERTсами. Это может потерпеть неудачу при таких условиях.

Джон Доу
источник
Насколько это безопасно, если предположить, что поле «name» имеет УНИКАЛЬНОЕ ограничение? Это когда-нибудь потерпит неудачу с уникальным нарушением?
agnsaft 29.12.12
2
Это отлично работает. Думаю, единственная проблема - это связь: что, если один из них модифицирует таблицу так, чтобы больше столбцов были уникальными. В этом случае все сценарии должны быть изменены. Было бы хорошо, если бы был более общий способ сделать это ...
Виллем Ван Онсем
1
Можно ли использовать его, RETURNS idнапример, для получения idвставленного или нет?
Оливье Понс
2
@OlivierPons да, это возможно. Добавьте RETURNING idв и запроса и он вернет либо новый идентификатор строки, либо ничего, если строка не была вставлена.
AlexM
4
Я обнаружил, что это ненадежно. Похоже, что Postgres иногда выполняет вставку до того, как он выполнил операцию выбора, и в результате я получаю нарушение ключа-дубликата, хотя запись еще не вставлена. Попробуйте использовать версию => 9.5 с ON CONFLICT.
Майкл Сильвер
51

Один из подходов заключается в создании неограниченной (без уникальных индексов) таблицы для вставки всех ваших данных и выбора, отличного от этого, для вставки в сотню таблиц.

Такой высокий уровень был бы. Я предполагаю, что все три столбца различны в моем примере, поэтому для шага 3 измените соединение NOT EXITS, чтобы объединить только уникальные столбцы в таблице сотен.

  1. Создать временную таблицу. Смотрите документы здесь .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. Вставить данные во временную таблицу.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Добавьте любые индексы во временную таблицу.

  4. Сделать основной стол вставкой.

    INSERT INTO hundred(name, name_slug, status) 
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X' 
            FROM temp_data
            WHERE 
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );
Kuberchaun
источник
3
Это самый быстрый способ сделать массовые вставки, когда я не знаю, существует ли уже строка.
конец
выберите «Х»? кто-то может уточнить? Это просто правильное утверждение: SELECT name,name_slug,statusили*
roberthuttinger
3
Найти коррелированный подзапрос. «X» может быть изменено на 1 или даже «SadClown». SQL требует, чтобы что-то было, а «X» - обычное дело. Он небольшой и делает очевидным, что используется коррелированный подзапрос и соответствует требованиям того, что требует SQL.
Kuberchaun
Вы упомянули «вставьте все свои данные (при условии временной таблицы) и сделайте выбор, отличный от этого». В таком случае, не должно бытьSELECT DISTINCT name, name_slug, status FROM temp_data ?
gibbz00
17

К сожалению, не PostgreSQLподдерживает ни то, MERGEни другое ON DUPLICATE KEY UPDATE, поэтому вам придется сделать это в двух утверждениях:

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

INSERT
INTO    invoices (invoiceid, billed)
SELECT  '12345', 'TRUE'
WHERE   '12345' NOT IN
        (
        SELECT  invoiceid
        FROM    invoices
        )

Вы можете обернуть это в функцию:

CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
RETURNS VOID
AS
$$
        UPDATE  invoices
        SET     billed = $2
        WHERE   invoices = $1;

        INSERT
        INTO    invoices (invoiceid, billed)
        SELECT  $1, $2
        WHERE   $1 NOT IN
                (
                SELECT  invoiceid
                FROM    invoices
                );
$$
LANGUAGE 'sql';

и просто назовите это:

SELECT  fn_upd_invoices('12345', 'TRUE')
Quassnoi
источник
1
На самом деле, это не работает: я могу звонить INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);любое количество раз, и он продолжает вставлять строку.
AP257
1
@ AP257: CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred. Есть одна запись.
Quassnoi
12

Вы можете использовать ЦЕННОСТИ - доступные в Postgres:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
crististm
источник
12
ВЫБЕРИТЕ имя ИЗ ЛИЧНОСТИ <--- что, если в человеке миллиард строк?
Хенли Чиу
1
Я думаю, что это хороший быстрый способ решить проблему, но только если вы уверены, что исходная таблица никогда не станет огромной. У меня есть таблица, в которой никогда не будет больше 1000 строк, поэтому я могу использовать это решение.
Леонард
ВАУ, это именно то, что мне было нужно. Я волновался, что мне нужно создать функцию или временную таблицу, но это исключает все это - спасибо!
Амальговинус
8

Я знаю, что этот вопрос давно, но подумал, что это может кому-то помочь. Я думаю, что самый простой способ сделать это через триггер. Например:

Create Function ignore_dups() Returns Trigger
As $$
Begin
    If Exists (
        Select
            *
        From
            hundred h
        Where
            -- Assuming all three fields are primary key
            h.name = NEW.name
            And h.hundred_slug = NEW.hundred_slug
            And h.status = NEW.status
    ) Then
        Return NULL;
    End If;
    Return NEW;
End;
$$ Language plpgsql;

Create Trigger ignore_dups
    Before Insert On hundred
    For Each Row
    Execute Procedure ignore_dups();

Выполните этот код из приглашения psql (или, как вам нравится, для выполнения запросов непосредственно в базе данных). Затем вы можете вставить как обычно из Python. Например:

sql = "Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
cursor.execute(sql, (hundred, hundred_slug, status))

Обратите внимание, что, как уже упоминалось в @Thomas_Wouters, приведенный выше код использует преимущества параметров, а не объединяет строку.

КТР
источник
Если кому-то еще было интересно, из документов : «Сработали триггеры на уровне строк, ДО того, как они могут вернуть ноль, чтобы сигнализировать диспетчеру триггеров пропустить оставшуюся операцию для этой строки (т. Е. Последующие триггеры не запускаются, а INSERT / UPDATE»). / DELETE не происходит для этой строки). Если возвращается ненулевое значение, то операция продолжается с этим значением строки. "
Пит
Именно этот ответ я искал. Чистый код, используя функцию + триггер вместо оператора выбора. +1
Яцек Кравчик
Мне нравится этот ответ, используйте функцию и триггер. Теперь я нахожу другой способ выйти из тупика, используя функции и триггеры ...
Сукма Сапутра
7

Есть хороший способ сделать условную INSERT в PostgreSQL с помощью запроса WITH:

WITH a as(
select 
 id 
from 
 schema.table_name 
where 
 column_name = your_identical_column_value
)
INSERT into 
 schema.table_name
(col_name1, col_name2)
SELECT
    (col_name1, col_name2)
WHERE NOT EXISTS (
     SELECT
         id
     FROM
         a
        )
  RETURNING id 
Ритеш Джа
источник
7

Это именно та проблема, с которой я сталкиваюсь, и моя версия 9.5

И я решаю это с помощью SQL-запроса ниже.

INSERT INTO example_table (id, name)
SELECT 1 AS id, 'John' AS name FROM example_table
WHERE NOT EXISTS(
            SELECT id FROM example_table WHERE id = 1
    )
LIMIT 1;

Надеюсь, что это поможет кому-то, кто имеет ту же проблему с версией> = 9.5.

Спасибо за чтение.

tuanngocptn
источник
5

ВСТАВЬТЕ .. ГДЕ НЕ СУЩЕСТВУЕТ - это хороший подход. А условий гонки можно избежать с помощью «конверта» транзакции:

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;
Павел Францирек
источник
2

Это легко с правилами:

CREATE RULE file_insert_defer AS ON INSERT TO file
WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

Но это не удается при одновременной записи ...


источник
1

Подход с наибольшим количеством голосов (от Джона Доу) как-то работает для меня, но в моем случае из ожидаемых 422 строк я получаю только 180. Я не мог найти ничего неправильного и вообще не было ошибок, поэтому я искал другое простой подход.

Использование IF NOT FOUND THENпосле SELECTпросто отлично работает для меня.

(описано в документации PostgreSQL )

Пример из документации:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;
vchrizz
источник
1

Класс курсора psycopgs имеет атрибут rowcount .

Этот атрибут только для чтения указывает количество строк, которые были произведены последним execute * () (для операторов DQL, таких как SELECT) или затронуты (для операторов DML, таких как UPDATE или INSERT).

Таким образом, вы можете сначала попробовать UPDATE и INSERT, только если rowcount равен 0.

Но в зависимости от уровней активности в вашей базе данных вы можете столкнуться с состоянием гонки между UPDATE и INSERT, когда другой процесс может создать эту запись в промежуточный период.

johnbaum
источник
Предположительно, завершение этих запросов в транзакции уменьшит состояние гонки.
Даниэль Лайонс
Спасибо, действительно простое и чистое решение
Александр Малфейт
1

Кажется, что ваш столбец "сто" определен как первичный ключ и поэтому должен быть уникальным, что не так. Проблема не в том, а в ваших данных.

Я предлагаю вам ввести идентификатор в качестве серийного типа для обработки первичного ключа

Boodoo
источник
1

Если вы скажете, что многие ваши строки идентичны, вы прекратите проверку много раз. Вы можете отправить их, и база данных определит, вставить это или нет с предложением ON CONFLICT следующим образом

  INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
  +",'" + hundred_slug + "', " + status + ") ON CONFLICT ON CONSTRAINT
  hundred_pkey DO NOTHING;" cursor.execute(sql_string);
opena
источник
0

Я искал похожее решение, пытаясь найти SQL, который работает как в PostgreSQL, так и в HSQLDB. (Именно HSQLDB сделал это трудным.) Используя ваш пример в качестве основы, этот формат я нашел в другом месте.

sql = "INSERT INTO hundred (name,name_slug,status)"
sql += " ( SELECT " + hundred + ", '" + hundred_slug + "', " + status
sql += " FROM hundred"
sql += " WHERE name = " + hundred + " AND name_slug = '" + hundred_slug + "' AND status = " + status
sql += " HAVING COUNT(*) = 0 );"
Джефф Фэрли
источник
-1

Вот общая функция Python, которая, учитывая имя таблицы, столбцы и значения, генерирует эквивалент upsert для postgresql.

импорт JSON

def upsert(table_name, id_column, other_columns, values_hash):

    template = """
    WITH new_values ($$ALL_COLUMNS$$) as (
      values
         ($$VALUES_LIST$$)
    ),
    upsert as
    (
        update $$TABLE_NAME$$ m
            set
                $$SET_MAPPINGS$$
        FROM new_values nv
        WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
        RETURNING m.*
    )
    INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
    SELECT $$ALL_COLUMNS$$
    FROM new_values
    WHERE NOT EXISTS (SELECT 1
                      FROM upsert up
                      WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
    """

    all_columns = [id_column] + other_columns
    all_columns_csv = ",".join(all_columns)
    all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
    set_mappings = ",".join([ c+ " = nv." +c for c in other_columns])

    q = template
    q = q.replace("$$TABLE_NAME$$", table_name)
    q = q.replace("$$ID_COLUMN$$", id_column)
    q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
    q = q.replace("$$VALUES_LIST$$", all_values_csv)
    q = q.replace("$$SET_MAPPINGS$$", set_mappings)

    return q


def query_value(value):
    if value is None:
        return "NULL"
    if type(value) in [str, unicode]:
        return "'%s'" % value.replace("'", "''")
    if type(value) == dict:
        return "'%s'" % json.dumps(value).replace("'", "''")
    if type(value) == bool:
        return "%s" % value
    if type(value) == int:
        return "%s" % value
    return value


if __name__ == "__main__":

    my_table_name = 'mytable'
    my_id_column = 'id'
    my_other_columns = ['field1', 'field2']
    my_values_hash = {
        'id': 123,
        'field1': "john",
        'field2': "doe"
    }
    print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)
Патрик
источник
-8

Решение в простом, но не сразу.
Если вы хотите использовать эту инструкцию, вы должны внести одно изменение в базу данных:

ALTER USER user SET search_path to 'name_of_schema';

после этих изменений «INSERT» будет работать правильно.

эль фьюзер
источник