UPSERT: вмъкни или обнови
Накратко: Upsert записва нов ред, а ако ред с този ключ вече има, го обновява или го пропуска; в SQLite и PostgreSQL се пише INSERT … ON CONFLICT.
Проблем
Клиентът се връща
Георги Петров се премести в София. Той поръчва, без да влиза в профила си, и формата праща име, имейл и град. Сайтът опитва да го запише като нов клиент:
Имейлът му вече е в базата, а той е уникален. Сайтът трябва да реши: нов клиент или стар. Може първо да провери със SELECT и после да избере между INSERT и UPDATE. Но това са две заявки, а между тях друг посетител може да запише същия имейл.
Идеята
ON CONFLICT … DO UPDATE
ON CONFLICT казва на INSERT какво да прави, ако редът нарушава уникална колона. С DO UPDATE обновява реда, който вече е в таблицата:
Двата реда минават по различен път. Георги го има: редът му остава с номер 2 и с датата на регистрация от 2025 г., а се сменя само градът. Весела я няма: тя се вмъква като клиент 21.
excluded е редът, който заявката е опитала да вмъкне. excluded.city е градът от VALUES, а city без нищо пред него е старата стойност в таблицата.
Това се казва upsert, от „update“ и „insert“.
По-точно, за любопитните
В PostgreSQL заявката е същата: ON CONFLICT (email) DO UPDATE SET city = EXCLUDED.city. MySQL има свой запис: INSERT … ON DUPLICATE KEY UPDATE. SQL Server и Oracle използват MERGE, по-обща команда, която сравнява две таблици и решава за всеки ред: вмъкни, обнови или изтрий. PostgreSQL има MERGE от версия 15. В SQLite MERGE няма.
Пробвай
Нов адрес
Елена Димитрова (elena.d@example.com) се премести във Варна. Формата праща име, имейл, град Варна и днешната дата 2026-10-25. Запиши я така, че да се смени само градът. Датата на регистрация остава старата.
Решение
INSERT INTO customers (name, email, city, joined_at)
VALUES ('Елена Димитрова', 'elena.d@example.com', 'Варна', '2026-10-25')
ON CONFLICT (email) DO UPDATE SET city = excluded.city;INSERT INTO customers (name, email, city, joined_at) VALUES ('Елена Димитрова', 'elena.d@example.com', 'Варна', '2026-10-25') ON CONFLICT (email) DO UPDATE SET city = excluded.city. Ако сложиш и joined_at = excluded.joined_at, Елена ще изглежда като клиент от днес. Проверява се цялата таблица customers.
Идеята
Наличности от доставчика
Сутрин пристига доставка: 30 баници, 40 краставици и нов продукт, айрян. Доставчикът ползва номерата на продуктите от магазина, а айрянът вече има номер 26 в каталога. Познатите продукти трябва да получат още бройки, а новият трябва да се появи:
Конфликтът е по id, първичния ключ. За баницата и краставиците products.stock + excluded.stock събира старата наличност с доставената. Датата added_at им остава старата, защото не е в SET. Айрянът го няма, затова се вмъква с всичките си колони.
Тук пред stock пише products., защото в израза има две наличности: тази в таблицата и тази от доставката.
Пробвай
Още в количката
Георги добавя 2 пакета масло (продукт 9, 3,20 €) в новата си поръчка 57. Ако го има, количеството трябва да се увеличи, а ако го няма, да се добави ред.
Решение
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (57, 9, 2, 3.2)
ON CONFLICT (order_id, product_id) DO UPDATE SET quantity = quantity + excluded.quantity;INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (57, 9, 2, 3.2) ON CONFLICT (order_id, product_id) DO UPDATE SET quantity = quantity + excluded.quantity. В order_items ключът е двойката поръчка и продукт, затова конфликтът е по двете колони. Маслото беше 3, става 5.
Виж го
DO NOTHING
Понякога при конфликт не трябва да се променя нищо. От изложение идва списък с хора, които искат да са клиенти. Който вече е клиент, се пропуска:
Мария остава, каквато си беше, с град София. Весела е нов клиент. Без ON CONFLICT цялата заявка щеше да спре на Мария и Весела нямаше да влезе.
По-точно, за любопитните
SQLite има и по-стар, свой запис: INSERT OR IGNORE прави същото като DO NOTHING.
INSERT OR REPLACE изглежда като upsert, но не е. При конфликт той изтрива стария ред и вмъква нов. Теодора получава нов номер и нова дата на регистрация:
При Георги изтриването изобщо не минава, защото поръчките му сочат към стария ред:
Затова за upsert ползвай ON CONFLICT … DO UPDATE. Той сменя само колоните в SET.
Упражнение
Доставката
Доставчикът праща доставката като таблица delivery: номер на продукта, име, категория, цена и количество. За урока таблицата вече е в базата. Пусни началния SELECT, за да я видиш.
Запиши доставката в products. Познатите продукти получават доставеното количество към наличността си и цената от доставчика. Новите се вмъкват с дата 2026-10-25.
Доставката в products:
Решение
INSERT INTO products (id, name, category_id, price, stock, added_at)
SELECT product_id, name, category_id, price, quantity, '2026-10-25'
FROM delivery
WHERE true
ON CONFLICT (id) DO UPDATE SET
stock = products.stock + excluded.stock,
price = excluded.price;INSERT INTO products (id, name, category_id, price, stock, added_at) SELECT product_id, name, category_id, price, quantity, '2026-10-25' FROM delivery WHERE true ON CONFLICT (id) DO UPDATE SET stock = products.stock + excluded.stock, price = excluded.price. Маслото става 3,40 € и 40 бройки, кафето 25 бройки, а зеленият чай е нов продукт 26.
WHERE true изглежда излишно, но е нужно. Без него SQLite чете ON като началото на JOIN … ON и спира с грешка. Когато редовете за upsert идват от SELECT, слагай WHERE, дори и само WHERE true.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Първичен и външен ключ · INSERT: нови редове · UPDATE: промяна на редове · DELETE: изтриване на редове
- Продължава в: Потоци от събития · Пекарната, етап 4: промените
- Задачи по темата: Промяна на данните