Upsert - вставити рядок, а якщо він уже є, - оновити. У MySQL «вже є» визначається порушенням унікального ключа (первинного чи UNIQUE).
INSERT INTO product_stats (product_id, views)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE views = views + 1;
Перший виклик вставить рядок, наступні - збільшать лічильник. Атомарно, без гонки «перевірити - вставити».
Посилання на значення, яке намагалися вставити - через псевдонім рядка (MySQL 8.0.19+):
INSERT INTO prices (sku, price, updated_at)
VALUES ('A1', 199.00, NOW()), ('B2', 349.00, NOW()) AS new
ON DUPLICATE KEY UPDATE price = new.price, updated_at = new.updated_at;
Стара функція VALUES(price) в UPDATE-частині працює, але застаріла.
Підводні камені:
- Потрібен унікальний ключ саме на тих колонках, за якими визначається «той самий» рядок. Без нього буде звичайна вставка дубліката.
- Кілька унікальних ключів: якщо рядок конфліктує одразу з двома різними рядками за різними ключами, оновиться лише один - результат несподіваний. Upsert краще робити по таблиці з одним унікальним ключем, крім первинного.
- Автоінкремент «з'їдається»: невдала вставка часто все одно забирає значення
AUTO_INCREMENT- звідси дірки в ID. - Кількість змінених рядків: 1 - вставлено, 2 - оновлено, 0 - значення ті самі.
- Блокування: при конфлікті InnoDB бере блокування на запис індексу - при великій конкуренції можливі deadlock'и, і їх варто повторювати.
У Laravel: Model::upsert($rows, uniqueBy: ['sku'], update: ['price', 'updated_at']) генерує саме цей запит для MySQL (і ON CONFLICT для PostgreSQL).
Докладніше в документації: INSERT ... ON DUPLICATE KEY UPDATE