Як "вставити, якщо немає" в MySQL?


838

Я почав з googling, і знайшов цю статтю, в якій йдеться про таблиці мьютекс.

У мене є таблиця з ~ 14 мільйонами записів. Якщо я хочу додати більше даних у тому ж форматі, чи є спосіб переконатися, що запис, який я хочу вставити, вже не існує без використання пари запитів (тобто, один запит для перевірки та один для вставки - це набір результатів порожній)?

Чи є uniqueобмеження на полі гарантією insertневдачі, якщо воно вже є?

Здається, що з лише обмеженням, коли я видаю вставку через php, скрипт прошивається.



Див. Stackoverflow.com/questions/44550788/… для обговорення не записуючих значень auto_inc.
Рік Джеймс

@RickJames - це цікавий питання .. але не впевнений, що це безпосередньо пов’язано з цим q :)
warren

1
Про це було сказано в коментарі, і інше питання, яке стверджувало, що це питання є "точним дублікатом". Отже, я відчув, що було б корисно зв'язати питання разом на благо інших.
Рік Джеймс

1
О, я ніколи не думаю дивитись на бічну планку.
Рік Джеймс

Відповіді:


806

використання INSERT IGNORE INTO table

див. http://bogdan.org.ua/2007/10/18/mysql-insert-if-not-exists-syntax.html

є також INSERT … ON DUPLICATE KEY UPDATEсинтаксис, пояснення ви можете знайти на dev.mysql.com


Публікація з bogdan.org.ua відповідно до веб-кешу Google :

18 жовтня 2007 року

Для початку: станом на останній MySQL синтаксис, представлений у заголовку, неможливий. Але є кілька дуже простих способів досягти того, що очікується, використовуючи існуючу функціональність.

Можливі 3 можливих рішення: використання INSERT IGNORE, ЗАМІНА або ВСТАВКА… НА ДУПЛІКАТИ КЛЮЧНОГО ОНОВЛЕННЯ.

Уявіть, у нас є таблиця:

CREATE TABLE `transcripts` (
`ensembl_transcript_id` varchar(20) NOT NULL,
`transcript_chrom_start` int(10) unsigned NOT NULL,
`transcript_chrom_end` int(10) unsigned NOT NULL,
PRIMARY KEY (`ensembl_transcript_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Тепер уявіть, що у нас є автоматичний конвеєр, який імпортує метадані стенограми з Ensembl, і що через різні причини трубопровід може бути порушений на будь-якому етапі виконання. Таким чином, нам потрібно забезпечити дві речі:

  1. повторне виконання трубопроводу не знищить нашу базу даних

  2. повторні страти не загинуть через помилки "дублювання первинного ключа".

Спосіб 1: використання ЗАМІНИ

Це дуже просто:

REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Якщо запис існує, він буде перезаписаний; якщо його ще не існує, він буде створений. Однак використання цього методу не є ефективним для нашого випадку: нам не потрібно перезаписувати наявні записи, добре просто пропустити їх.

Спосіб 2: використання INSERT IGNORE Також дуже простий:

INSERT IGNORE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Тут, якщо 'ensembl_transcript_id' вже присутній у базі даних, він буде мовчки пропущений (проігнорований). (Точніше, ось цитата з посібника з посилання MySQL: "Якщо ви використовуєте ключове слово IGNORE, помилки, які виникають під час виконання оператора INSERT, замість цього трактуються як попередження. Наприклад, без IGNORE - рядок, що дублює існуючий індекс UNIQUE або ПЕРШИЧНЕ КЛЮЧЕ значення в таблиці викликає помилку повторюваного ключа, а виписку скасовано. ".) Якщо запис ще не існує, він буде створений.

Цей другий метод має декілька потенційних недоліків, включаючи непереривання запиту у випадку виникнення будь-якої іншої проблеми (див. Посібник). Таким чином, його слід використовувати, якщо раніше було протестовано без ключового слова IGNORE.

Спосіб 3: використання INSERT… ON DUPLICATE KEY UPDATE:

Третій варіант - використовувати INSERT … ON DUPLICATE KEY UPDATE синтаксис, і в частині UPDATE просто не робити нічого безглуздої (порожньої) операції, як-от обчислити 0 + 0 (Geoffray пропонує виконувати призначення id = id для механізму оптимізації MySQL, щоб ігнорувати цю операцію). Перевагою цього методу є те, що він ігнорує лише повторювані ключові події та все-таки перериває інші помилки.

Як остаточне зауваження: цю публікацію надихнув Xaprb. Я б також порадив проконсультувати його іншу посаду щодо написання гнучких SQL-запитів.


3
і чи можу я поєднати це із "затримкою", щоб пришвидшити сценарій?
warren

3
так, вставлення із запізненням може пришвидшити вам справи. спробуйте
knittl

32
Так, і майте на увазі, що ЗАМІНА
ВІДМІНУЄ

10
INSERT … ON DUPLICATE KEY UPDATEкраще, оскільки він не видаляє рядок, зберігаючи будь-які auto_incrementстовпці та інші дані.
повторний

14
Просто щоб повідомити всіх. Використовуючи INSERT … ON DUPLICATE KEY UPDATEметод, збільшується будь-який стовпець AUTO_INCREMENT з невдалою вставкою. Можливо, тому, що це насправді не вдалося, але UPDATE мав би.
not2qubit

216

Рішення:

INSERT INTO `table` (`value1`, `value2`) 
SELECT 'stuff for value1', 'stuff for value2' FROM DUAL 
WHERE NOT EXISTS (SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1) 

Пояснення:

Найпотаємніший запит

SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1

використовується як WHERE NOT EXISTSумова, що виявляє, якщо вже існує рядок з даними, які потрібно вставити. Після того, як буде знайдено такий ряд рядків, запит може зупинитися, отже LIMIT 1(мікрооптимізація може бути опущена).

Проміжний запит

SELECT 'stuff for value1', 'stuff for value2' FROM DUAL

представляє значення, які потрібно вставити. DUALпосилається на спеціальний один рядок, одну таблицю стовпців, яка за замовчуванням присутня у всіх базах даних Oracle (див. https://en.wikipedia.org/wiki/DUAL_table ). На MySQL-сервері версії 5.7.26 я отримав дійсний запит при пропуску FROM DUAL, але старіші версії (наприклад, 5.5.60), схоже, потребують FROMінформації. За допомогою WHERE NOT EXISTSпроміжного запиту повертає порожній набір результатів, якщо найпотужніший запит знайшов відповідні дані.

Зовнішній запит

INSERT INTO `table` (`value1`, `value2`) 

вставляє дані, якщо такі є поверненими проміжним запитом.


4
ви можете дати трохи більше інформації про те, як це використовувати?
Олексій V

36
Цей варіант підходить, якщо в таблиці немає унікального ключа ( INSERT IGNOREі INSERT ON DUPLICATE KEYпотрібні унікальні обмеження ключа)
rabudde

2
Якщо ви використовуєте "з подвійного" в другому рядку замість "з таблиці", тоді вам не потрібно "обмеження 1".
Багатий

6
Що робити, якщо stuff for value1і stuff for value2однакові? Це кинуло бDuplicate column name
Робін

1
Я також більше віддаю перевагу SELECT 1замість SELECT *підзапитів. Набагато ймовірніше, що це може бути задоволено індексом.
Арт

58

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


Приклад оновлення дублікатів оновлення ключів на основі mysql.com

INSERT INTO table (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

UPDATE table SET c=c+1 WHERE a=1;

Приклад вставки ігнорування на основі mysql.com

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Або:

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    SET col_name={expr | DEFAULT}, ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Або:

INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    SELECT ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

24

Будь-яке просте обмеження повинно виконувати цю роботу, якщо прийнятний виняток. Приклади:

  • первинний ключ, якщо не сурогат
  • унікальне обмеження на стовпчик
  • унікальне обмеження багато стовпців

Вибачте, це здається оманливо простим. Я знаю, що це погано стикається з посиланням, яким ви ділитесь з нами. ;-(

Але я нескінченно даю цю відповідь, бо, здається, вона наповнює вашу потребу. (Якщо ні, це може спричинити оновлення ваших вимог, що також було б "доброю справою" (TM)).

Відредаговано : Якщо вставка порушить унікальне обмеження для бази даних, виняток - це кидання на рівні бази даних, передане драйвером. Це, безумовно, зупинить ваш сценарій, з відмовою. У PHP повинно бути можливим вирішити цю справу ...


1
я додав роз'яснення до питання - чи все ще діє ваша відповідь?
warren

2
Я вірю, що так і є. Унікальне обмеження призведе до виходу з ладу неправильних вставок. Примітка. Ви повинні зіткнутися з цим збоєм у своєму коді, але це цілком стандартно.
KLE

1
поки що я буду дотримуватися рішення, яке я прийняв - але далі буду розглядати питання щодо відмов INSERT тощо, коли додаток зростає
warren

3
INSERT IGNOREв основному змінює всі помилки на попередження, щоб ваш сценарій не переривався. Потім ви можете переглянути будь-які попередження за допомогою команди SHOW WARNINGS. І ще одна важлива примітка : UNIQUE обмеження не працюють зі значеннями NULL, тобто. row1 (1, NULL) та row2 (1, NULL) будуть вставлені (якщо не порушено інше обмеження, наприклад первинний ключ). Нещасний.
Simon Simon

18

Ось функція PHP, яка буде вставляти рядок, лише якщо всі вказані значення стовпців не існують у таблиці.

  • Якщо один із стовпців відрізняється, рядок буде додано.

  • Якщо таблиця порожня, рядок буде додано.

  • Якщо рядок існує там, де всі вказані стовпці мають вказані значення, рядок не буде додано.

    function insert_unique($table, $vars)
    {
      if (count($vars)) {
        $table = mysql_real_escape_string($table);
        $vars = array_map('mysql_real_escape_string', $vars);
    
        $req = "INSERT INTO `$table` (`". join('`, `', array_keys($vars)) ."`) ";
        $req .= "SELECT '". join("', '", $vars) ."' FROM DUAL ";
        $req .= "WHERE NOT EXISTS (SELECT 1 FROM `$table` WHERE ";
    
        foreach ($vars AS $col => $val)
          $req .= "`$col`='$val' AND ";
    
        $req = substr($req, 0, -5) . ") LIMIT 1";
    
        $res = mysql_query($req) OR die();
        return mysql_insert_id();
      }
    
      return False;
    }

Приклад використання:

<?php
insert_unique('mytable', array(
  'mycolumn1' => 'myvalue1',
  'mycolumn2' => 'myvalue2',
  'mycolumn3' => 'myvalue3'
  )
);
?>

5
Досить дорого, якщо у вас величезне навантаження вставок.
Эџад Дьдулңамаи

правда, але ефективно, якщо вам потрібно додати конкретні перевірки
Charles Forest

1
Попередження: mysql_* розширення застаріло, як у PHP 5.5.0, і видалено з PHP 7.0.0. Натомість слід використовувати або розширення mysqli, або PDO_MySQL . Див. Також Огляд API MySQL для отримання додаткової допомоги при виборі API MySQL.
Дхарман

17
REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Якщо запис існує, він буде перезаписаний; якщо його ще не існує, він буде створений.


10
REPLACEможе видалити рядок, а потім вставити замість оновлення. Побічний ефект полягає в тому, що обмеження можуть видаляти інші об’єкти, а видаляти тригери.
xmedeko

1
З посібника MySQL: "ЗАМІНА має сенс лише у тому випадку, якщо в таблиці є індекс PRIMARY KEY або UNIQUE. В іншому випадку він стає еквівалентним INSERT, оскільки індекс не може використовуватися для визначення того, чи буде новий рядок дублювати інший."
BurninLeo

16

Спробуйте наступне:

IF (SELECT COUNT(*) FROM beta WHERE name = 'John' > 0)
  UPDATE alfa SET c1=(SELECT id FROM beta WHERE name = 'John')
ELSE
BEGIN
  INSERT INTO beta (name) VALUES ('John')
  INSERT INTO alfa (c1) VALUES (LAST_INSERT_ID())
END

5
Спробуйте, що ці відповіді мають низьку цінність для StackOverflow, оскільки вони дуже мало навчають ОП та тисячам майбутніх дослідників. Будь ласка, відредагуйте цю відповідь, щоб вказати, як рішення працює і чому це гарна ідея.
mickmackusa

1
Ідеальне рішення на випадок, якщо поля, що співпадають, не є ключами ..!
Лев

6

Є кілька відповідей, які пояснюють, як вирішити це, якщо у вас є UNIQUEіндекс, який ви можете перевірити за допомогою ON DUPLICATE KEYабо INSERT IGNORE. Це не завжди так, і оскільки UNIQUEмає обмеження довжини (1000 байт), ви, можливо, не зможете це змінити. Наприклад, мені довелося працювати з метаданими в WordPress ( wp_postmeta).

Нарешті я вирішив це двома запитами:

UPDATE wp_postmeta SET meta_value = ? WHERE meta_key = ? AND post_id = ?;
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) SELECT DISTINCT ?, ?, ? FROM wp_postmeta WHERE NOT EXISTS(SELECT * FROM wp_postmeta WHERE meta_key = ? AND post_id = ?);

Запит 1 - це звичайний UPDATEзапит без ефекту, коли відповідного набору даних немає. Запит 2 - це INSERTзалежність від а NOT EXISTS, тобто INSERTвиконується лише тоді, коли набір даних не існує.


2

Щось варто зазначити, що INSERT IGNORE все одно збільшуватиметься первинним ключем, чи твердження було успішним чи не таким, як звичайне INSERT.

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

Погляньте на innodb_autoinc_lock_mode = 0(налаштування сервера та постачається з невеликим враженням від продуктивності), або спочатку використовуйте SELECT, щоб переконатися, що ваш запит не вийде з ладу (що також постачається із зверненням до продуктивності та додатковим кодом).


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

Починаючи з SELECTпоразок, вся мета - просто передати велику партію INSERTs і не бажати турбуватися про дублікати.
warren

2

Оновіть або вставте без відомого первинного ключа

Якщо у вас вже є унікальний або первинний ключ, інші відповіді INSERT INTO ... ON DUPLICATE KEY UPDATE ...або REPLACE INTO ...мають, або повинні добре працювати (зауважте, що заміняйте на видалення, якщо вони є, а потім вставляють - таким чином, частково не оновлюються існуючі значення).

Але якщо у вас є значення для some_column_idі some_type, поєднання яких, як відомо, є унікальними. І ви хочете оновити, some_valueякщо існує, або вставити, якщо немає. І ви хочете зробити це лише за один запит (щоб уникнути використання транзакції). Це може бути рішенням:

INSERT INTO my_table (id, some_column_id, some_type, some_value)
SELECT t.id, t.some_column_id, t.some_type, t.some_value
FROM (
    SELECT id, some_column_id, some_type, some_value
    FROM my_table
    WHERE some_column_id = ? AND some_type = ?
    UNION ALL
    SELECT s.id, s.some_column_id, s.some_type, s.some_value
    FROM (SELECT NULL AS id, ? AS some_column_id, ? AS some_type, ? AS some_value) AS s
) AS t
LIMIT 1
ON DUPLICATE KEY UPDATE
some_value = ?

В основному, запит виконується таким чином (менш складний, ніж може виглядати):

  • Виберіть існуючий рядок через відповідність WHEREпропозицій.
  • sОб'єднайте результат, який створює потенційний новий рядок (таблицю ), де значення стовпців явно вказані (s.id - NULL, тому він генерує новий ідентифікатор автоматичного збільшення).
  • Якщо існуюча рядок знайдена, то потенціал новий рядок з таблиці sвідкидається (з - за LIMIT 1 на столі t), і вона завжди буде ініціювати , ON DUPLICATE KEYякий буде UPDATEв some_valueколоні.
  • Якщо існуючий рядок не знайдено, то потенційний новий рядок вставляється (як зазначено в таблиці s).

Примітка. Кожна таблиця у реляційній базі даних повинна мати принаймні первинний idстовпчик автоматичного збільшення . Якщо у вас цього немає, додайте його навіть тоді, коли він вам не потрібен з першого погляду. Безумовно, потрібен цей «трюк».


Кілька інших відповідачів запропонували INSERT INTO ... SELECT FROMформат. Чому ви також?
warren

2
@warren Або ви не прочитали моєї відповіді, ви її не розумієте, або я не пояснив її належним чином. У будь-якому випадку дозвольте наголосити на наступному: це не просто регулярне INSERT INTO... SELECT FROM...рішення. Будь ласка, перейдіть до мене за посиланням на відповідь, який є однаковим, якщо ви зможете його знайти, я видалю цю відповідь, інакше ви схвалюєте мою відповідь (угода?). Обов’язково переконайтеся, що відповідь, на яку ви збираєтеся зв’язати, використовує лише 1 запит (для оновлення + вставлення), жодної транзакції, і може націлити будь-яку комбінацію стовпців, які, як відомо, є унікальними (тому окремо стовпці не потрібно бути унікальним).
Yeti
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.