Постгреси: ВСТАВИТИ, якщо вона ще не існує


361

Я використовую 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"

Як я можу написати "INSERT, якщо цей рядок вже не існує" SQL-оператор?

Я бачив такі складні твердження, як рекомендується:

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

Але, по-перше, чи є це надмір для того, що мені потрібно, по-друге, як я можу виконати один із них як простий рядок?


56
Незалежно від того, як ви вирішите цю проблему, ви не повинні генерувати такий запит. Використовуйте параметри у своєму запиті та передайте значення окремо; дивіться stackoverflow.com/questions/902408/…
Thomas Wouters

3
Чому б не зловити виняток і проігнорувати його?
Меттью Мітчелл

5
Щодо Posgres 9.5 (зараз на beta2) є нова функція, подібна до перегляду, див: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
Чи думали ви прийняти відповідь на це? =]
допит

Відповіді:


512

Postgres 9.5 (випускається з 2016-01-07) пропонує команду INSERT команду "upsert" , також відому як пункт ON CONFLICT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

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


14
9,5 звільнено.
luckydonald

2
@TusharJain до PostgreSQL 9.5, ви можете зробити «старомодний» UPSERT (з CTE), але у вас можуть виникнути проблеми з умовами перегонів, і він не буде виконаний як стиль 9.5. У цьому блозі (в оновленому районі внизу) є хороша деталь про програму, включаючи деякі посилання, якщо ви хочете прочитати більше про деталі.
Skyguard

16
Для тих, хто потребує, ось два простих приклади. (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, а не вказівку на звільнення. Посилання на doc: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

Як я можу написати "INSERT, якщо цей рядок вже не існує" SQL-оператор?

Існує хороший спосіб зробити умовний 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самі. Він може провалитися за таких умов.


Наскільки це безпечно, якщо припустити, що поле "ім'я" має єдине обмеження? Чи коли-небудь не вдасться з унікальним порушенням?
agnsaft

2
Це чудово працює. Єдиною проблемою є з'єднання, яке я думаю: що робити, якщо модифікувати таблицю таким чином, щоб більше стовпців було унікальним. У цьому випадку всі сценарії повинні бути змінені. Було б добре, якби був більш загальний спосіб зробити це ...
Віллем Ван Онсем

1
Чи можна використовувати його, RETURNS idнаприклад, для того, щоб отримати idвставку чи ні?
Олів'є Понс

2
@OlivierPons так, це можливо. Додайте RETURNING idзапит на і та, і він поверне або новий ідентифікатор рядка, або нічого, якщо жодна рядок не вставлена.
AlexM

4
Я визнав це ненадійним. Здається, Postgres іноді виконує вставку перед тим, як виконати вибір, і я закінчую порушенням дублюючого ключа, навіть якщо запис ще не вставлений. Спробуйте використовувати версію => 9.5 за допомогою КОНФЛІКТУ.
Майкл Сілвер

51

Одним із підходів було б створити необмежену таблицю (без унікальних індексів), щоб вставити всі ваші дані і зробити вибране відмінне від того, щоб зробити свою вставку у вашу сто таблицю.

Так високий рівень був би. Я припускаю, що в моєму прикладі всі три стовпчики відрізняються, тому для зміни розміру step3 НЕ ВИХОДИ приєднуйтесь лише до унікальних стовпців таблиці сто.

  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
        );

3
Це найшвидший спосіб, який я знайшов робити масові вставки, коли я не знаю, чи рядок вже існує.
nate c

виберіть "X"? хтось може уточнити? Це просто SELECT name,name_slug,status*
обране

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')

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;

12
ВИБІРТЕ ім’я від людини <--- що робити, якщо в людини є мільярд рядків?
Henley Chiu

1
Я думаю, що це приємний швидкий спосіб вирішити проблему, але лише тоді, коли ви впевнені, що джерельна таблиця ніколи не зробиться величезною. У мене є таблиця, яка ніколи не матиме більше 1000 рядків, тому я можу використовувати це рішення.
Леонард

WOW, це саме те, що мені було потрібно. Я хвилювався, що мені потрібно створити функцію або темп-таблицю, але це виключає все це - дякую!
Амальговінус

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, наведений вище код використовує переваги параметрів, а не об'єднання рядка.


Якщо хтось теж цікавився, з документів : "Тригери рівня рядків спрацьовують ДО ПЕРЕД, можна повернути нуль, щоб сигналізувати менеджеру тригерів пропустити решту операцій для цього рядка (тобто наступні тригери не спрацьовують, і ВСТАВКА / ОНОВЛЕННЯ / DELETE не зустрічається для цього рядка). Якщо повернене ненульове значення, операція продовжується із цим значенням рядка. "
Піт

Саме цю відповідь я шукав. Очистити код, використовуючи функцію + тригер замість оператора select. +1
Яцек Кравчик

Я люблю цю відповідь, використовую функцію та тригер. Тепер я знаходжу інший спосіб вийти з глухого кута за допомогою функцій та тригерів ...
Sukma Saputra

7

Існує хороший спосіб зробити умовний INSERT в PostgreSQL, використовуючи ЗО запиту: Like:

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

Дякуємо за прочитане


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після aSELECT просто працює для мене ідеально.

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

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

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;

1

Клас курсору psycopgs має атрибут кількість рядків .

Цей атрибут лише для читання визначає кількість рядків, які останній виконаний * () видав (для операторів DQL на зразок SELECT) або вплинув (для операторів DML, таких як UPDATE або INSERT).

Таким чином, ви можете спробувати ОНОВИТИ спочатку та ВСТАВИТИ, лише якщо кількість рядків дорівнює 0.

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


Імовірно, загортання цих запитів в транзакції полегшить умову гонки.
Даніель Ліонс

Дякую, дійсно просте і чисте рішення
Олександр Мальфайт

1

Здається, що ваш стовпець "сотня" визначається як первинний ключ, і тому він повинен бути унікальним, а це не так. Проблема не в тому, що у ваших даних.

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


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);

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

Рішення просте, але не негайно.
Якщо ви хочете скористатися цією інструкцією, ви повинні внести одну зміну в db:

ALTER USER user SET search_path to 'name_of_schema';

після цих змін "INSERT" запрацює правильно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.