Что такое UPSERT в PostgreSQL и когда применять?

JuniorPostgreSQL · Backend·Обновлено 27 июля 2026
Коротко
UPSERT — операция, которая вставляет новую строку, а если строка с таким ключом уже существует — обновляет её. В PostgreSQL реализована через синтаксис INSERT ... ON CONFLICT.

Что такое UPSERT

UPSERT — это комбинация слов UPdate + inSERT. Операция атомарно решает дилемму: если запись с указанным уникальным ключом отсутствует — вставить новую, если существует — обновить существующую.

В PostgreSQL UPSERT реализован через конструкцию INSERT ... ON CONFLICT, появившуюся в версии 9.5.

Синтаксис

INSERT INTO таблица (колонки)
VALUES (значения)
ON CONFLICT (конфликтующая_колонка)
DO UPDATE SET колонка = EXCLUDED.колонка;

Ключевое слово EXCLUDED ссылается на значения, которые пытались вставить, но не смогли из-за конфликта.

Альтернатива — DO NOTHING, если при конфликте нужно просто проигнорировать вставку без ошибки.

Когда применять

  • Синхронизация данных: импорт внешних данных, где дубли — норма, а не исключение.
  • Счётчики и агрегаты: инкрементировать просмотры, лайки, события — без предварительного SELECT.
  • Идемпотентные операции: повторные вызовы API или очередей не должны ломать данные.
  • Кэш-таблицы: обновление снапшотов, которые периодически пересчитываются.

Примеры

Обновление при конфликте

-- Вставляем пользователя или обновляем email, если логин уже занят
INSERT INTO users (username, email, updated_at)
VALUES ('ivan', 'ivan@example.com', NOW())
ON CONFLICT (username)
DO UPDATE SET
  email = EXCLUDED.email,
  updated_at = EXCLUDED.updated_at;

Игнорирование дублей

-- Добавляем тег к статье, дубль просто пропускаем
INSERT INTO article_tags (article_id, tag_id)
VALUES (42, 7)
ON CONFLICT (article_id, tag_id)
DO NOTHING;

Инкрементирование счётчика

-- Увеличиваем счётчик просмотров страницы
INSERT INTO page_views (page_slug, views, last_viewed_at)
VALUES ('/about', 1, NOW())
ON CONFLICT (page_slug)
DO UPDATE SET
  views = page_views.views + 1,
  last_viewed_at = NOW();

Важные ограничения

  • ON CONFLICT срабатывает только на уникальные ограничения (UNIQUE, PRIMARY KEY) — не на произвольные условия.
  • Конфликтная колонка должна быть явно указана, либо можно использовать ON CONFLICT ON CONSTRAINT constraint_name.
  • UPSERT не заменяет MERGE (появился в PostgreSQL 15) — для сложных условий слияния лучше использовать MERGE.

Атомарность

Вся операция выполняется атомарно: не нужно вручную делать SELECT → проверка → INSERT/UPDATE в транзакции. Это исключает race condition в конкурентной среде.

Что хочет услышать интервьюер

Кандидат знает синтаксис INSERT ... ON CONFLICT и понимает, что это атомарная операция

Понимание ключевого слова EXCLUDED и что оно хранит пытавшиеся вставиться значения

Знание разницы между DO UPDATE и DO NOTHING

Понимание, что ON CONFLICT работает только с уникальными ограничениями (UNIQUE/PRIMARY KEY)

Практические примеры применения: счётчики, синхронизация, идемпотентность

Пример: UPSERT через node-postgres (pg)

import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Сохраняем настройки пользователя: создаём или обновляем
async function upsertUserSettings(
  userId: number,
  theme: string,
  language: string
): Promise<void> {
  await pool.query(
    `INSERT INTO user_settings (user_id, theme, language, updated_at)
     VALUES ($1, $2, $3, NOW())
     ON CONFLICT (user_id)
     DO UPDATE SET
       theme = EXCLUDED.theme,
       language = EXCLUDED.language,
       updated_at = EXCLUDED.updated_at`,
    [userId, theme, language]
  );
}

// Атомарно инкрементируем счётчик просмотров
async function incrementPageViews(slug: string): Promise<number> {
  const result = await pool.query<{ views: number }>(
    `INSERT INTO page_views (slug, views)
     VALUES ($1, 1)
     ON CONFLICT (slug)
     DO UPDATE SET views = page_views.views + 1
     RETURNING views`,
    [slug]
  );
  return result.rows[0].views;
}

Типичные ошибки

Путают UPSERT с обычным UPDATE — не понимают, что при отсутствии строки произойдёт INSERT

Забывают, что конфликтная колонка обязана иметь UNIQUE или PRIMARY KEY ограничение

Не знают ключевое слово EXCLUDED и пытаются использовать значения из VALUES напрямую

Используют SELECT + IF EXISTS вместо атомарного UPSERT, создавая race condition

Путают синтаксис PostgreSQL с MySQL (INSERT ... ON DUPLICATE KEY UPDATE)

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее