Как «вставить, если не существует» в MySQL?

838

Я начал с поиска в Google и нашел эту статью, в которой говорится о таблицах мьютексов.

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

uniqueГарантирует ли ограничение на поле insertпровал, если он уже там?

Кажется, что с простым ограничением, когда я запускаю вставку через php, скрипт скрипит.

кроличий садок
источник
2
dev.mysql.com/doc/refman/5.0/ru/if.html
Угур Гюмюшан,
См. Stackoverflow.com/questions/44550788/… для обсуждения о том, чтобы не записывать значения auto_inc.
Рик Джеймс
@RickJames - это интересный д .. но не уверен , что это напрямую связано с этим д :)
садок
1
Это было упомянуто в комментарии, и этот другой Вопрос утверждал, что этот Вопрос был «точным дубликатом». Итак, я чувствовал, что было бы неплохо связать вопросы вместе на благо других.
Рик Джеймс
1
О, я никогда не думаю, чтобы посмотреть на боковую панель.
Рик Джеймс

Ответы:

808

использование 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, REPLACE или INSERT… ON DUPLICATE KEY UPDATE.

Представьте, что у нас есть стол:

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

Это очень просто:

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 или значение PRIMARY KEY в таблице вызывает ошибку дубликата ключа, и оператор отменяется. ».) Если запись еще не существует, она будет создана.

Этот второй метод имеет несколько потенциальных недостатков, в том числе не прерывание запроса в случае возникновения любой другой проблемы (см. Руководство). Таким образом, его следует использовать, если он был предварительно протестирован без ключевого слова IGNORE.

Способ 3: использование INSERT… ON DUPLICATE KEY UPDATE:

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

В качестве последнего уведомления: этот пост был вдохновлен Xaprb. Я бы также посоветовал обратиться к его другому посту по написанию гибких SQL-запросов.

knittl
источник
3
и могу ли я объединить это с задержкой, чтобы ускорить сценарий?
Уоррен
3
да, вставка с задержкой может ускорить процесс для вас. попробуйте
knittl
32
Да, и имейте в виду, что REPLACE INTO выполняет DELETE, а затем INSERT, а не UPDATE
bobobobo
10
INSERT … ON DUPLICATE KEY UPDATEлучше, так как он не удаляет строки, сохраняя любые auto_incrementстолбцы и другие данные.
благоухающий
15
Просто чтобы сообщить всем. Использование INSERT … ON DUPLICATE KEY UPDATEметода действительно увеличивает любой столбец AUTO_INCREMENT с ошибочной вставкой. Вероятно, потому что это не совсем не удалось, но ОБНОВЛЕНИЕ.
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
Если вы используете «from dual» в строке 2 вместо «from table», тогда вам не нужно условие «limit 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] ... ]
Zed
источник
24

Любое простое ограничение должно выполнять работу, если исключение допустимо. Примеры :

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

Извините, это кажется обманчиво простым. Я знаю, что это плохо смотрится со ссылкой, которую вы нам предоставляете. ;-(

Но я все же даю этот ответ, потому что он, кажется, удовлетворяет ваши потребности. (Если нет, это может привести к тому, что вы обновите свои требования, что также будет «хорошая вещь» (TM)).

Отредактировано : если вставка нарушит ограничение уникальности базы данных, исключение - выброс на уровне базы данных, ретранслируемый драйвером. Это, безусловно, остановит ваш сценарий с ошибкой. В PHP должно быть возможно решить этот случай ...

KLE
источник
1
я добавил уточнение к вопросу - ваш ответ по-прежнему применим?
Уоррен
2
Я верю, что это так. Уникальное ограничение приведет к сбою неправильных вставок. Примечание: вы должны иметь дело с этой ошибкой в ​​вашем коде, но это вполне стандартно.
KLE
1
сейчас я собираюсь придерживаться принятого мной решения, но в дальнейшем буду разбираться с обработкой ошибок INSERT и т. д. по мере роста приложения
warren
3
INSERT IGNOREв основном все ошибки превращаются в предупреждения, чтобы ваш сценарий не прерывался. Затем вы можете просмотреть любые предупреждения с помощью команды SHOW WARNINGS. И еще одно важное замечание : уникальные ограничения не работают со значениями NULL, т.е. row1 (1, NULL) и row2 (1, NULL) оба будут вставлены (если не нарушено другое ограничение, такое как первичный ключ). Несчастная.
Саймон Ист
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'
  )
);
?>
Jrm
источник
5
Довольно дорого, если у вас огромный груз вставок.
Эџад Дьдуляңмaи
верно, но эффективно, если вам нужно добавить определенные проверки
Чарльз Форест
1
Предупреждение: mysql_* расширение устарело с PHP 5.5.0 и удалено с PHP 7.0.0. Вместо этого следует использовать расширение mysqli или PDO_MySQL . Смотрите также MySQL API Overview для получения дополнительной помощи при выборе MySQL API.
Дхарман
17
REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Если запись существует, она будет перезаписана; если он еще не существует, он будет создан.

Росио
источник
10
REPLACEможет удалить строку, а затем вставить вместо обновления. Побочным эффектом является то, что ограничения могут удалять другие объекты и запускать триггеры удаления.
xmedeko
1
Из руководства по MySQL: «REPLACE имеет смысл, только если таблица имеет индекс 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 является a, INSERTкоторый зависит от a NOT EXISTS, то INSERTесть выполняется только тогда, когда набор данных не существует.

wortwart
источник
3

Стоит отметить, что INSERT IGNORE по-прежнему будет увеличивать первичный ключ независимо от того, был ли оператор успешным или нет, как обычный INSERT.

Это вызовет пробелы в ваших первичных ключах, что может сделать программиста психически нестабильным. Или, если ваше приложение плохо спроектировано и зависит от совершенных добавочных первичных ключей, оно может стать головной болью.

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

Джилли
источник
Почему «пробелы в ваших первичных ключах» - даже потенциально - «делают программиста психически неуравновешенным»? В первичных ключах постоянно возникают пробелы - например, при каждом удалении записи.
садок
Начиная с SELECTпоражений, вся цель состоит в том, чтобы передать большую партию INSERTи не беспокоиться о дубликатах.
садок
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формат. Почему ты тоже?
садок
2
@ warren Либо вы не прочитали мой ответ, либо не поняли его, либо я не объяснил должным образом. В любом случае позвольте мне подчеркнуть следующее: это не просто обычное INSERT INTO... SELECT FROM...решение. Пожалуйста, обратитесь ко мне по ссылке на тот же ответ, если вы сможете найти его, я удалю этот ответ, в противном случае вы проголосуете за мой ответ (сделка?). Убедитесь, что в ответе, который вы собираетесь связать, используется только 1 запрос (для обновления + вставка), нет транзакции, и он может быть нацелен на любую комбинацию столбцов, которые, как известно, являются уникальными (поэтому отдельно столбцы не должен быть уникальным).
Йети