Обычный UPDATE хорошо подходит, когда новое значение можно задать прямо.
Например:
UPDATE orders
SET status = 'archived'
WHERE created_at < now() - interval '1 year';
Но часто новое значение нужно взять из другой таблицы.
Например:
- записать страну пользователя в его заказы;
- обновить сумму покупок пользователя по таблице
orders;
- проставить категорию товара из справочника;
- обновить цену из staging-таблицы;
- поднять зарплату сотрудникам отделов, где средняя зарплата ниже порога;
- синхронизировать данные из временной таблицы в основную.
Для таких задач в PostgreSQL есть синтаксис:
UPDATE ... FROM
Он позволяет обновлять целевую таблицу, используя данные из другой таблицы, подзапроса или агрегата.
Главная идея:
UPDATE говорит, какую таблицу обновляем.
FROM говорит, откуда берём дополнительные данные.
WHERE связывает строки целевой таблицы со строками источника.
Базовый пример
Допустим, есть таблица users:
id | email | country
---+----------------+--------
1 | ann@mail.com | DE
2 | bob@mail.com | US
3 | kate@mail.com | ES
И таблица orders:
id | user_id | amount | country
----+---------+--------+---------
101 | 1 | 1000 | NULL
102 | 2 | 1500 | NULL
103 | 3 | 700 | NULL
Хотим записать страну пользователя в каждый заказ.
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
После запроса:
id | user_id | amount | country
----+---------+--------+--------
101 | 1 | 1000 | DE
102 | 2 | 1500 | US
103 | 3 | 700 | ES
Что здесь происходит:
UPDATE orders o
означает, что обновляем таблицу orders.
SET country = u.country
говорит, что в колонку orders.country нужно записать значение из users.country.
FROM users u
подключает таблицу users как источник данных.
WHERE o.user_id = u.id
связывает заказ с пользователем.
UPDATE ... FROM похож на JOIN
UPDATE ... FROM можно мысленно читать как JOIN.
Обычный SELECT:
SELECT
o.id,
o.user_id,
u.country
FROM orders o
JOIN users u ON o.user_id = u.id;
Обновление по той же связи:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Разница в синтаксисе:
- в
SELECT связь пишется через JOIN ... ON;
- в
UPDATE ... FROM связь пишется через WHERE.
Это важный момент.
В PostgreSQL нет отдельного ON между целевой таблицей и таблицей из FROM. Поэтому условие связи нужно не забыть написать в WHERE.
Главная ловушка: забыли WHERE
Самая опасная ошибка в UPDATE ... FROM — забыть условие связи.
Опасный запрос:
UPDATE orders o
SET country = u.country
FROM users u;
Синтаксически он валиден.
PostgreSQL не скажет:
вы забыли связь между orders и users
Он построит декартово произведение: каждая строка orders соединится с каждой строкой users.
В итоге все строки orders будут обновлены, но каким именно u.country — полагаться нельзя. Если справа несколько пользователей, значение может оказаться произвольным с точки зрения бизнес-логики.
Правильный запрос:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Главное правило:
В UPDATE ... FROM всегда проверяйте, есть ли условие связи между целевой таблицей и источником.
Как безопасно проверять UPDATE ... FROM
Перед опасным обновлением лучше сначала написать SELECT с той же логикой.
Например, вместо того чтобы сразу выполнять:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
сначала проверьте:
SELECT
o.id AS order_id,
o.user_id,
o.country AS old_country,
u.country AS new_country
FROM orders o
JOIN users u ON o.user_id = u.id;
Так вы увидите:
- какие строки будут затронуты;
- какое было старое значение;
- какое новое значение будет записано;
- нет ли лишних совпадений.
Для реальных данных полезно ещё проверить количество строк:
SELECT count(*)
FROM orders o
JOIN users u ON o.user_id = u.id;
А само обновление лучше делать в транзакции:
BEGIN;
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
COMMIT;
Строки без пары не обновляются
UPDATE ... FROM ведёт себя как INNER JOIN.
Если строке целевой таблицы не нашлось пары в источнике, она не обновится.
Например, есть заказ:
id | user_id | amount | country
----+---------+--------+---------
104 | 999 | 500 | NULL
А пользователя с id = 999 в таблице users нет.
Запрос:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
не тронет этот заказ.
Почему?
Потому что для него не нашлось строки users, которая проходит условие:
o.user_id = u.id
Это важная особенность:
есть совпадение в FROM -> строка обновляется
нет совпадения -> строка не трогается
Обновление из агрегата
Очень частый сценарий — обновить таблицу значениями из подзапроса с GROUP BY.
Например, в таблице users хотим хранить общую сумму оплаченных заказов.
ALTER TABLE users
ADD COLUMN total_spent numeric DEFAULT 0;
Посчитаем сумму по orders и запишем её в users.total_spent.
UPDATE users u
SET total_spent = s.total_spent
FROM (
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY user_id
) s
WHERE u.id = s.user_id;
Что здесь происходит:
- подзапрос
s считает сумму оплаченных заказов по каждому пользователю;
UPDATE users берёт эту сумму;
WHERE u.id = s.user_id связывает пользователя с агрегатом;
SET total_spent = s.total_spent записывает результат.
Подзапрос:
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY user_id
может вернуть:
user_id | total_spent
--------+------------
1 | 2500
2 | 1500
И эти значения попадут в users.
Что будет с пользователями без заказов
В примере выше пользователи без оплаченных заказов не попадут в подзапрос s.
Значит, они не обновятся.
Если у них уже было total_spent = 0, всё хорошо.
Но если там было старое значение, оно останется.
Например:
id | total_spent
---+------------
3 | 999
Если у пользователя 3 больше нет оплаченных заказов, подзапрос s его не вернёт, и 999 останется.
Иногда это ошибка.
Чтобы явно обнулить пользователей без оплаченных заказов, можно использовать источник, который возвращает всех пользователей.
Например:
UPDATE users u
SET total_spent = COALESCE(s.total_spent, 0)
FROM (
SELECT
u2.id AS user_id,
SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent
FROM users u2
LEFT JOIN orders o ON o.user_id = u2.id
GROUP BY u2.id
) s
WHERE u.id = s.user_id;
Здесь подзапрос возвращает всех пользователей, а COALESCE превращает отсутствие суммы в 0.
Можно также сделать двумя командами:
UPDATE users
SET total_spent = 0;
UPDATE users u
SET total_spent = s.total_spent
FROM (
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY user_id
) s
WHERE u.id = s.user_id;
Но такой вариант нужно использовать аккуратно, особенно в конкурентной системе.
Обновление из staging-таблицы
UPDATE ... FROM часто используют для синхронизации данных из staging-таблицы.
Допустим, есть основная таблица products:
id | sku | price
---+-------+------
1 | A-100 | 100
2 | B-200 | 200
И staging-таблица с новыми ценами:
sku | new_price
------+----------
A-100 | 120
B-200 | 210
Обновим цены:
UPDATE products p
SET price = s.new_price
FROM staging_prices s
WHERE p.sku = s.sku;
Это читается так:
для каждого товара
найди строку в staging_prices по sku
запиши новую цену
Если в staging нет строки для товара, товар не обновится.
Обновление нескольких колонок
В SET можно обновлять несколько колонок сразу.
UPDATE products p
SET
price = s.new_price,
updated_at = now()
FROM staging_prices s
WHERE p.sku = s.sku;
Или перенести сразу несколько значений:
UPDATE users u
SET
name = s.name,
country = s.country,
updated_at = now()
FROM staging_users s
WHERE u.email = s.email;
Это удобно для импорта данных.
Но важно помнить: если в staging-таблице есть дубли по email, результат может стать недетерминированным.
Опасность дублей в источнике
Одна строка целевой таблицы должна находить не больше одной подходящей строки в источнике.
Например:
UPDATE users u
SET country = s.country
FROM staging_users s
WHERE u.email = s.email;
Если в staging_users есть две строки с одним email:
email | country
----------------+--------
ada@mail.com | DE
ada@mail.com | ES
то для одного пользователя найдутся две строки-источника.
PostgreSQL выполнит обновление, но какое именно значение попадёт в country, не стоит считать надёжным.
Перед таким UPDATE лучше проверить дубли:
SELECT
email,
COUNT(*) AS cnt
FROM staging_users
GROUP BY email
HAVING COUNT(*) > 1;
Если дубли есть, сначала нужно выбрать одну строку.
Например, через DISTINCT ON:
WITH deduped AS (
SELECT DISTINCT ON (email)
email,
country
FROM staging_users
ORDER BY email, updated_at DESC
)
UPDATE users u
SET country = d.country
FROM deduped d
WHERE u.email = d.email;
Здесь для каждого email берётся самая свежая строка по updated_at.
Главное правило:
Источник для UPDATE ... FROM должен давать одну понятную строку на одну строку цели.
Условное обновление по агрегату
Иногда подзапрос нужен не для нового значения, а для отбора строк.
Например:
поднять зарплату на 10% сотрудникам тех отделов, где средняя зарплата ниже 50000.
UPDATE employees e
SET salary = salary * 1.10
FROM (
SELECT dept
FROM employees
GROUP BY dept
HAVING AVG(salary) < 50000
) low
WHERE e.dept = low.dept;
Подзапрос:
SELECT dept
FROM employees
GROUP BY dept
HAVING AVG(salary) < 50000
возвращает отделы, где средняя зарплата ниже порога.
А внешний UPDATE обновляет всех сотрудников этих отделов.
Это пример, где условие зависит не от одной строки, а от свойства всей группы.
Обычным WHERE salary < 50000 такую логику не заменить.
Обновление по подзапросу с оконной функцией
UPDATE ... FROM можно использовать и с оконными функциями.
Например, хотим сохранить ранг сотрудника по зарплате внутри отдела.
ALTER TABLE employees
ADD COLUMN salary_rank int;
Обновим ранг:
UPDATE employees e
SET salary_rank = r.rnk
FROM (
SELECT
id,
RANK() OVER (
PARTITION BY dept
ORDER BY salary DESC
) AS rnk
FROM employees
) r
WHERE e.id = r.id;
Подзапрос r считает ранг для каждого сотрудника.
Затем UPDATE записывает этот ранг в таблицу.
Такой подход полезен, когда нужно материализовать результат расчёта.
Но не стоит злоупотреблять: если ранг можно считать на лету в отчёте, возможно, хранить его в таблице не нужно.
UPDATE ... FROM с RETURNING
В PostgreSQL можно добавить RETURNING, чтобы увидеть, какие строки обновились и какие значения получили.
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
RETURNING
o.id,
o.user_id,
o.country;
RETURNING вернёт уже обновлённые значения.
Например:
id | user_id | country
----+---------+--------
101 | 1 | DE
102 | 2 | US
103 | 3 | ES
Можно вернуть и значения из таблицы-источника:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
RETURNING
o.id AS order_id,
u.email,
o.country AS new_country;
Это удобно для проверки и логирования.
Как увидеть старое и новое значение
В UPDATE ... RETURNING колонка целевой таблицы уже содержит новое значение.
Например:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
RETURNING o.country;
o.country — это уже новое значение.
Если нужно увидеть старое и новое, можно сначала сохранить старые значения в CTE.
WITH old_rows AS (
SELECT
o.id,
o.country AS old_country,
u.country AS new_country
FROM orders o
JOIN users u ON o.user_id = u.id
),
updated AS (
UPDATE orders o
SET country = old_rows.new_country
FROM old_rows
WHERE o.id = old_rows.id
RETURNING
o.id,
old_rows.old_country,
o.country AS new_country
)
SELECT *
FROM updated
ORDER BY id;
Так вы получите:
id | old_country | new_country
----+-------------+------------
101 | NULL | DE
102 | NULL | US
103 | NULL | ES
UPDATE ... FROM и self-update
Иногда источник — та же самая таблица.
Например, нужно проставить manager_dept сотруднику по его менеджеру.
Таблица employees:
id | name | manager_id | dept | manager_dept
---+------+------------+------+-------------
1 | Ann | NULL | eng | NULL
2 | Bob | 1 | qa | NULL
Обновим:
UPDATE employees e
SET manager_dept = m.dept
FROM employees m
WHERE e.manager_id = m.id;
Здесь одна и та же таблица участвует дважды:
e — сотрудник, которого обновляем;
m — его менеджер, из которого берём dept.
Алиасы в таких запросах обязательны для читаемости.
UPDATE ... FROM и LEFT JOIN
В самом UPDATE ... FROM нет прямой записи вида:
UPDATE ...
FROM ...
LEFT JOIN ...
между целевой таблицей и источником, как в обычном SELECT.
Если нужно поведение LEFT JOIN, обычно строят источник так, чтобы он возвращал все нужные строки.
Например, обнулить total_spent пользователям без заказов:
UPDATE users u
SET total_spent = COALESCE(s.total_spent, 0)
FROM (
SELECT
u2.id AS user_id,
SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent
FROM users u2
LEFT JOIN orders o ON o.user_id = u2.id
GROUP BY u2.id
) s
WHERE u.id = s.user_id;
Здесь LEFT JOIN находится внутри подзапроса s.
А внешний UPDATE всё равно соединяется с s через обычное условие:
WHERE u.id = s.user_id
Так как s содержит всех пользователей, обновятся все пользователи.
UPDATE ... FROM против коррелированного подзапроса
Иногда ту же задачу можно решить через подзапрос в SET.
Например:
UPDATE orders o
SET country = (
SELECT u.country
FROM users u
WHERE u.id = o.user_id
);
Это коррелированный подзапрос.
Вариант через FROM:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Оба варианта могут работать.
Но UPDATE ... FROM часто удобнее, когда:
- нужно обновить несколько колонок из источника;
- источник — сложный подзапрос;
- нужно подключить несколько таблиц;
- хочется писать в стиле
JOIN;
- нужно использовать
RETURNING с полями источника.
Индексы для UPDATE ... FROM
UPDATE ... FROM может быть дорогим, если таблицы большие и нет индексов на колонках связи.
Для запроса:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
обычно полезны:
CREATE INDEX orders_user_id_idx
ON orders (user_id);
и primary key на users.id, который чаще всего уже есть:
users.id PRIMARY KEY
Для staging-обновления:
UPDATE products p
SET price = s.new_price
FROM staging_prices s
WHERE p.sku = s.sku;
полезны индексы:
CREATE INDEX products_sku_idx
ON products (sku);
CREATE INDEX staging_prices_sku_idx
ON staging_prices (sku);
Для больших обновлений также важно помнить:
- обновление создаёт новые версии строк;
- таблица может раздуться;
- индексы нужно обновлять;
- autovacuum потом должен убрать старые версии;
- иногда лучше обновлять батчами.
Большие UPDATE лучше делать батчами
Если нужно обновить миллионы строк, один огромный UPDATE может быть тяжёлым.
Он может:
- долго держать блокировки;
- создать много dead tuples;
- раздуть таблицу;
- нагрузить индексы;
- долго откатываться при ошибке.
Иногда лучше обновлять порциями.
Например:
WITH batch AS (
SELECT o.id
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.country IS DISTINCT FROM u.country
ORDER BY o.id
LIMIT 10000
)
UPDATE orders o
SET country = u.country
FROM users u, batch b
WHERE o.id = b.id
AND o.user_id = u.id;
Такой запрос можно запускать повторно, пока не останется строк для обновления.
Условие:
o.country IS DISTINCT FROM u.country
безопасно сравнивает значения с учётом NULL.
Оно возвращает true, если значения реально отличаются, включая случаи с NULL.
Не обновляйте строки без необходимости
Если новое значение совпадает со старым, лучше не обновлять строку.
Плохо:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Этот запрос обновит строки даже там, где country уже правильная.
Лучше:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
AND o.country IS DISTINCT FROM u.country;
Так PostgreSQL обновит только строки, где значение действительно изменилось.
Это уменьшает:
- количество изменённых строк;
- нагрузку на индексы;
- объём dead tuples;
- работу autovacuum;
- риск лишних триггеров.
MySQL: UPDATE ... JOIN
В MySQL нет PostgreSQL-синтаксиса UPDATE ... FROM.
Там используют UPDATE ... JOIN.
PostgreSQL:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
MySQL:
UPDATE orders o
JOIN users u ON o.user_id = u.id
SET o.country = u.country;
Для агрегата:
UPDATE users u
JOIN (
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY user_id
) s ON u.id = s.user_id
SET u.total_spent = s.total_spent;
Главная логика та же:
- в PostgreSQL источник находится в
FROM, связь в WHERE;
- в MySQL источник подключается через
JOIN, связь в ON.
И там, и там ошибка в условии соединения может обновить слишком много строк.
ClickHouse
В ClickHouse подход к обновлениям другой.
PostgreSQL — OLTP-база, где UPDATE меняет строки привычным образом.
ClickHouse — аналитическая колоночная СУБД. Массовые изменения там обычно выполняются через mutations.
Например:
ALTER TABLE orders
UPDATE status = 'archived'
WHERE created_at < now() - INTERVAL 1 YEAR;
Такие обновления могут выполняться асинхронно и быть дорогими на больших объёмах.
Прямого удобного аналога PostgreSQL UPDATE ... FROM с join-семантикой обычно нет.
Часто в ClickHouse вместо обновления:
- пересобирают таблицу;
- используют materialized view;
- хранят версионные данные;
- делают join на чтении;
- используют словари;
- проектируют таблицу так, чтобы не обновлять строки часто.
При переносе запросов из PostgreSQL в ClickHouse нужно менять не только синтаксис, но и подход к данным.
Практические шаблоны
Обновить заказы страной пользователя
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Обновить только если значение изменилось
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
AND o.country IS DISTINCT FROM u.country;
Обновить сумму покупок пользователя
UPDATE users u
SET total_spent = s.total_spent
FROM (
SELECT
user_id,
SUM(amount) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY user_id
) s
WHERE u.id = s.user_id;
Обнулить пользователей без покупок
UPDATE users u
SET total_spent = COALESCE(s.total_spent, 0)
FROM (
SELECT
u2.id AS user_id,
SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent
FROM users u2
LEFT JOIN orders o ON o.user_id = u2.id
GROUP BY u2.id
) s
WHERE u.id = s.user_id;
Обновить цены из staging
UPDATE products p
SET
price = s.new_price,
updated_at = now()
FROM staging_prices s
WHERE p.sku = s.sku;
Удалить дубли в источнике перед обновлением
WITH deduped AS (
SELECT DISTINCT ON (email)
email,
country
FROM staging_users
ORDER BY email, updated_at DESC
)
UPDATE users u
SET country = d.country
FROM deduped d
WHERE u.email = d.email;
Обновить по агрегату группы
UPDATE employees e
SET salary = salary * 1.10
FROM (
SELECT dept
FROM employees
GROUP BY dept
HAVING AVG(salary) < 50000
) low
WHERE e.dept = low.dept;
Записать ранг зарплаты
UPDATE employees e
SET salary_rank = r.rnk
FROM (
SELECT
id,
RANK() OVER (
PARTITION BY dept
ORDER BY salary DESC
) AS rnk
FROM employees
) r
WHERE e.id = r.id;
Вернуть обновлённые строки
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id
RETURNING
o.id,
o.user_id,
o.country AS new_country;
Получить старое и новое значение
WITH old_rows AS (
SELECT
o.id,
o.country AS old_country,
u.country AS new_country
FROM orders o
JOIN users u ON o.user_id = u.id
),
updated AS (
UPDATE orders o
SET country = old_rows.new_country
FROM old_rows
WHERE o.id = old_rows.id
RETURNING
o.id,
old_rows.old_country,
o.country AS new_country
)
SELECT *
FROM updated
ORDER BY id;
MySQL-аналог
UPDATE orders o
JOIN users u ON o.user_id = u.id
SET o.country = u.country;
Частые ошибки
Забыли условие связи
Опасно:
UPDATE orders o
SET country = u.country
FROM users u;
Правильно:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Источник возвращает несколько строк на одну целевую строку
Опасно:
UPDATE users u
SET country = s.country
FROM staging_users s
WHERE u.email = s.email;
если в staging_users несколько строк с одним email.
Сначала проверьте:
SELECT email, COUNT(*)
FROM staging_users
GROUP BY email
HAVING COUNT(*) > 1;
Ждут, что строки без пары обнулятся сами
Запрос:
UPDATE users u
SET total_spent = s.total_spent
FROM (
SELECT user_id, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
) s
WHERE u.id = s.user_id;
не тронет пользователей без заказов.
Если их нужно обнулить, источник должен вернуть их тоже, или нужен отдельный UPDATE.
Обновляют строки, где значение уже совпадает
Лучше добавлять:
AND o.country IS DISTINCT FROM u.country
чтобы не создавать лишние обновления.
Не проверяют SELECT перед UPDATE
Перед большим обновлением полезно сделать:
SELECT ...
FROM target
JOIN source ON ...
WHERE ...
и убедиться, что строки и новые значения правильные.
Что важно запомнить
UPDATE ... FROM обновляет целевую таблицу данными из другой таблицы или подзапроса.
Пример:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
Главные правила:
- целевая таблица указывается после
UPDATE;
- источник данных указывается в
FROM;
- связь между целью и источником пишется в
WHERE;
- это похоже на
JOIN, но без отдельного ON;
- забытое условие связи может обновить всю таблицу;
- строки цели без пары в источнике не обновляются;
- источник должен возвращать не больше одной подходящей строки на одну строку цели;
- для агрегатов удобно использовать подзапрос в
FROM;
RETURNING показывает уже обновлённые значения;
- чтобы увидеть старое и новое, старые значения нужно сохранить заранее;
- для больших обновлений важны индексы, батчи и проверка плана;
- в MySQL аналог пишется через
UPDATE ... JOIN.
Короткий вывод
UPDATE ... FROM нужен, когда новое значение для строки зависит от другой таблицы или подзапроса.
Например:
UPDATE orders o
SET country = u.country
FROM users u
WHERE o.user_id = u.id;
читается так:
обнови заказы
возьми страну из users
свяжи orders.user_id с users.id
Главная мысль:
UPDATE ... FROM — это обновление в стиле JOIN, где источник данных лежит в FROM, а связь пишется в WHERE.
Используйте его для синхронизации, staging-таблиц, агрегатов и обновлений по связанным данным. Но перед запуском всегда проверяйте условие связи: в UPDATE ошибка обычно дороже, чем в SELECT.
Обычный
UPDATEхорошо подходит, когда новое значение можно задать прямо.Например:
UPDATE orders SET status = 'archived' WHERE created_at < now() - interval '1 year';Но часто новое значение нужно взять из другой таблицы.
Например:
orders;Для таких задач в PostgreSQL есть синтаксис:
UPDATE ... FROMОн позволяет обновлять целевую таблицу, используя данные из другой таблицы, подзапроса или агрегата.
Главная идея:
Базовый пример
Допустим, есть таблица
users:И таблица
orders:Хотим записать страну пользователя в каждый заказ.
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;После запроса:
Что здесь происходит:
UPDATE orders oозначает, что обновляем таблицу
orders.SET country = u.countryговорит, что в колонку
orders.countryнужно записать значение изusers.country.FROM users uподключает таблицу
usersкак источник данных.WHERE o.user_id = u.idсвязывает заказ с пользователем.
UPDATE ... FROM похож на JOIN
UPDATE ... FROMможно мысленно читать какJOIN.Обычный
SELECT:SELECT o.id, o.user_id, u.country FROM orders o JOIN users u ON o.user_id = u.id;Обновление по той же связи:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Разница в синтаксисе:
SELECTсвязь пишется черезJOIN ... ON;UPDATE ... FROMсвязь пишется черезWHERE.Это важный момент.
В PostgreSQL нет отдельного
ONмежду целевой таблицей и таблицей изFROM. Поэтому условие связи нужно не забыть написать вWHERE.Главная ловушка: забыли WHERE
Самая опасная ошибка в
UPDATE ... FROM— забыть условие связи.Опасный запрос:
UPDATE orders o SET country = u.country FROM users u;Синтаксически он валиден.
PostgreSQL не скажет:
Он построит декартово произведение: каждая строка
ordersсоединится с каждой строкойusers.В итоге все строки
ordersбудут обновлены, но каким именноu.country— полагаться нельзя. Если справа несколько пользователей, значение может оказаться произвольным с точки зрения бизнес-логики.Правильный запрос:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Главное правило:
Как безопасно проверять UPDATE ... FROM
Перед опасным обновлением лучше сначала написать
SELECTс той же логикой.Например, вместо того чтобы сразу выполнять:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;сначала проверьте:
SELECT o.id AS order_id, o.user_id, o.country AS old_country, u.country AS new_country FROM orders o JOIN users u ON o.user_id = u.id;Так вы увидите:
Для реальных данных полезно ещё проверить количество строк:
SELECT count(*) FROM orders o JOIN users u ON o.user_id = u.id;А само обновление лучше делать в транзакции:
BEGIN; UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id; -- check the result -- if everything looks fine: COMMIT; -- if something is wrong: -- ROLLBACK;Строки без пары не обновляются
UPDATE ... FROMведёт себя какINNER JOIN.Если строке целевой таблицы не нашлось пары в источнике, она не обновится.
Например, есть заказ:
А пользователя с
id = 999в таблицеusersнет.Запрос:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;не тронет этот заказ.
Почему?
Потому что для него не нашлось строки
users, которая проходит условие:o.user_id = u.idЭто важная особенность:
Обновление из агрегата
Очень частый сценарий — обновить таблицу значениями из подзапроса с
GROUP BY.Например, в таблице
usersхотим хранить общую сумму оплаченных заказов.ALTER TABLE users ADD COLUMN total_spent numeric DEFAULT 0;Посчитаем сумму по
ordersи запишем её вusers.total_spent.UPDATE users u SET total_spent = s.total_spent FROM ( SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'paid' GROUP BY user_id ) s WHERE u.id = s.user_id;Что здесь происходит:
sсчитает сумму оплаченных заказов по каждому пользователю;UPDATE usersберёт эту сумму;WHERE u.id = s.user_idсвязывает пользователя с агрегатом;SET total_spent = s.total_spentзаписывает результат.Подзапрос:
SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'paid' GROUP BY user_idможет вернуть:
И эти значения попадут в
users.Что будет с пользователями без заказов
В примере выше пользователи без оплаченных заказов не попадут в подзапрос
s.Значит, они не обновятся.
Если у них уже было
total_spent = 0, всё хорошо.Но если там было старое значение, оно останется.
Например:
Если у пользователя
3больше нет оплаченных заказов, подзапросsего не вернёт, и999останется.Иногда это ошибка.
Чтобы явно обнулить пользователей без оплаченных заказов, можно использовать источник, который возвращает всех пользователей.
Например:
UPDATE users u SET total_spent = COALESCE(s.total_spent, 0) FROM ( SELECT u2.id AS user_id, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent FROM users u2 LEFT JOIN orders o ON o.user_id = u2.id GROUP BY u2.id ) s WHERE u.id = s.user_id;Здесь подзапрос возвращает всех пользователей, а
COALESCEпревращает отсутствие суммы в0.Можно также сделать двумя командами:
UPDATE users SET total_spent = 0; UPDATE users u SET total_spent = s.total_spent FROM ( SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'paid' GROUP BY user_id ) s WHERE u.id = s.user_id;Но такой вариант нужно использовать аккуратно, особенно в конкурентной системе.
Обновление из staging-таблицы
UPDATE ... FROMчасто используют для синхронизации данных из staging-таблицы.Допустим, есть основная таблица
products:И staging-таблица с новыми ценами:
Обновим цены:
UPDATE products p SET price = s.new_price FROM staging_prices s WHERE p.sku = s.sku;Это читается так:
Если в staging нет строки для товара, товар не обновится.
Обновление нескольких колонок
В
SETможно обновлять несколько колонок сразу.UPDATE products p SET price = s.new_price, updated_at = now() FROM staging_prices s WHERE p.sku = s.sku;Или перенести сразу несколько значений:
UPDATE users u SET name = s.name, country = s.country, updated_at = now() FROM staging_users s WHERE u.email = s.email;Это удобно для импорта данных.
Но важно помнить: если в staging-таблице есть дубли по
email, результат может стать недетерминированным.Опасность дублей в источнике
Одна строка целевой таблицы должна находить не больше одной подходящей строки в источнике.
Например:
UPDATE users u SET country = s.country FROM staging_users s WHERE u.email = s.email;Если в
staging_usersесть две строки с одним email:то для одного пользователя найдутся две строки-источника.
PostgreSQL выполнит обновление, но какое именно значение попадёт в
country, не стоит считать надёжным.Перед таким
UPDATEлучше проверить дубли:SELECT email, COUNT(*) AS cnt FROM staging_users GROUP BY email HAVING COUNT(*) > 1;Если дубли есть, сначала нужно выбрать одну строку.
Например, через
DISTINCT ON:WITH deduped AS ( SELECT DISTINCT ON (email) email, country FROM staging_users ORDER BY email, updated_at DESC ) UPDATE users u SET country = d.country FROM deduped d WHERE u.email = d.email;Здесь для каждого email берётся самая свежая строка по
updated_at.Главное правило:
Условное обновление по агрегату
Иногда подзапрос нужен не для нового значения, а для отбора строк.
Например:
UPDATE employees e SET salary = salary * 1.10 FROM ( SELECT dept FROM employees GROUP BY dept HAVING AVG(salary) < 50000 ) low WHERE e.dept = low.dept;Подзапрос:
SELECT dept FROM employees GROUP BY dept HAVING AVG(salary) < 50000возвращает отделы, где средняя зарплата ниже порога.
А внешний
UPDATEобновляет всех сотрудников этих отделов.Это пример, где условие зависит не от одной строки, а от свойства всей группы.
Обычным
WHERE salary < 50000такую логику не заменить.Обновление по подзапросу с оконной функцией
UPDATE ... FROMможно использовать и с оконными функциями.Например, хотим сохранить ранг сотрудника по зарплате внутри отдела.
ALTER TABLE employees ADD COLUMN salary_rank int;Обновим ранг:
UPDATE employees e SET salary_rank = r.rnk FROM ( SELECT id, RANK() OVER ( PARTITION BY dept ORDER BY salary DESC ) AS rnk FROM employees ) r WHERE e.id = r.id;Подзапрос
rсчитает ранг для каждого сотрудника.Затем
UPDATEзаписывает этот ранг в таблицу.Такой подход полезен, когда нужно материализовать результат расчёта.
Но не стоит злоупотреблять: если ранг можно считать на лету в отчёте, возможно, хранить его в таблице не нужно.
UPDATE ... FROM с RETURNING
В PostgreSQL можно добавить
RETURNING, чтобы увидеть, какие строки обновились и какие значения получили.UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id RETURNING o.id, o.user_id, o.country;RETURNINGвернёт уже обновлённые значения.Например:
Можно вернуть и значения из таблицы-источника:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id RETURNING o.id AS order_id, u.email, o.country AS new_country;Это удобно для проверки и логирования.
Как увидеть старое и новое значение
В
UPDATE ... RETURNINGколонка целевой таблицы уже содержит новое значение.Например:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id RETURNING o.country;o.country— это уже новое значение.Если нужно увидеть старое и новое, можно сначала сохранить старые значения в CTE.
WITH old_rows AS ( SELECT o.id, o.country AS old_country, u.country AS new_country FROM orders o JOIN users u ON o.user_id = u.id ), updated AS ( UPDATE orders o SET country = old_rows.new_country FROM old_rows WHERE o.id = old_rows.id RETURNING o.id, old_rows.old_country, o.country AS new_country ) SELECT * FROM updated ORDER BY id;Так вы получите:
UPDATE ... FROM и self-update
Иногда источник — та же самая таблица.
Например, нужно проставить
manager_deptсотруднику по его менеджеру.Таблица
employees:Обновим:
UPDATE employees e SET manager_dept = m.dept FROM employees m WHERE e.manager_id = m.id;Здесь одна и та же таблица участвует дважды:
e— сотрудник, которого обновляем;m— его менеджер, из которого берёмdept.Алиасы в таких запросах обязательны для читаемости.
UPDATE ... FROM и LEFT JOIN
В самом
UPDATE ... FROMнет прямой записи вида:UPDATE ... FROM ... LEFT JOIN ...между целевой таблицей и источником, как в обычном
SELECT.Если нужно поведение
LEFT JOIN, обычно строят источник так, чтобы он возвращал все нужные строки.Например, обнулить
total_spentпользователям без заказов:UPDATE users u SET total_spent = COALESCE(s.total_spent, 0) FROM ( SELECT u2.id AS user_id, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent FROM users u2 LEFT JOIN orders o ON o.user_id = u2.id GROUP BY u2.id ) s WHERE u.id = s.user_id;Здесь
LEFT JOINнаходится внутри подзапросаs.А внешний
UPDATEвсё равно соединяется сsчерез обычное условие:WHERE u.id = s.user_idТак как
sсодержит всех пользователей, обновятся все пользователи.UPDATE ... FROM против коррелированного подзапроса
Иногда ту же задачу можно решить через подзапрос в
SET.Например:
UPDATE orders o SET country = ( SELECT u.country FROM users u WHERE u.id = o.user_id );Это коррелированный подзапрос.
Вариант через
FROM:UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Оба варианта могут работать.
Но
UPDATE ... FROMчасто удобнее, когда:JOIN;RETURNINGс полями источника.Индексы для UPDATE ... FROM
UPDATE ... FROMможет быть дорогим, если таблицы большие и нет индексов на колонках связи.Для запроса:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;обычно полезны:
CREATE INDEX orders_user_id_idx ON orders (user_id);и primary key на
users.id, который чаще всего уже есть:users.id PRIMARY KEYДля staging-обновления:
UPDATE products p SET price = s.new_price FROM staging_prices s WHERE p.sku = s.sku;полезны индексы:
CREATE INDEX products_sku_idx ON products (sku); CREATE INDEX staging_prices_sku_idx ON staging_prices (sku);Для больших обновлений также важно помнить:
Большие UPDATE лучше делать батчами
Если нужно обновить миллионы строк, один огромный
UPDATEможет быть тяжёлым.Он может:
Иногда лучше обновлять порциями.
Например:
WITH batch AS ( SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE o.country IS DISTINCT FROM u.country ORDER BY o.id LIMIT 10000 ) UPDATE orders o SET country = u.country FROM users u, batch b WHERE o.id = b.id AND o.user_id = u.id;Такой запрос можно запускать повторно, пока не останется строк для обновления.
Условие:
o.country IS DISTINCT FROM u.countryбезопасно сравнивает значения с учётом
NULL.Оно возвращает
true, если значения реально отличаются, включая случаи сNULL.Не обновляйте строки без необходимости
Если новое значение совпадает со старым, лучше не обновлять строку.
Плохо:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Этот запрос обновит строки даже там, где
countryуже правильная.Лучше:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id AND o.country IS DISTINCT FROM u.country;Так PostgreSQL обновит только строки, где значение действительно изменилось.
Это уменьшает:
MySQL: UPDATE ... JOIN
В MySQL нет PostgreSQL-синтаксиса
UPDATE ... FROM.Там используют
UPDATE ... JOIN.PostgreSQL:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;MySQL:
UPDATE orders o JOIN users u ON o.user_id = u.id SET o.country = u.country;Для агрегата:
UPDATE users u JOIN ( SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'paid' GROUP BY user_id ) s ON u.id = s.user_id SET u.total_spent = s.total_spent;Главная логика та же:
FROM, связь вWHERE;JOIN, связь вON.И там, и там ошибка в условии соединения может обновить слишком много строк.
ClickHouse
В ClickHouse подход к обновлениям другой.
PostgreSQL — OLTP-база, где
UPDATEменяет строки привычным образом.ClickHouse — аналитическая колоночная СУБД. Массовые изменения там обычно выполняются через mutations.
Например:
ALTER TABLE orders UPDATE status = 'archived' WHERE created_at < now() - INTERVAL 1 YEAR;Такие обновления могут выполняться асинхронно и быть дорогими на больших объёмах.
Прямого удобного аналога PostgreSQL
UPDATE ... FROMс join-семантикой обычно нет.Часто в ClickHouse вместо обновления:
При переносе запросов из PostgreSQL в ClickHouse нужно менять не только синтаксис, но и подход к данным.
Практические шаблоны
Обновить заказы страной пользователя
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Обновить только если значение изменилось
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id AND o.country IS DISTINCT FROM u.country;Обновить сумму покупок пользователя
UPDATE users u SET total_spent = s.total_spent FROM ( SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'paid' GROUP BY user_id ) s WHERE u.id = s.user_id;Обнулить пользователей без покупок
UPDATE users u SET total_spent = COALESCE(s.total_spent, 0) FROM ( SELECT u2.id AS user_id, SUM(o.amount) FILTER (WHERE o.status = 'paid') AS total_spent FROM users u2 LEFT JOIN orders o ON o.user_id = u2.id GROUP BY u2.id ) s WHERE u.id = s.user_id;Обновить цены из staging
UPDATE products p SET price = s.new_price, updated_at = now() FROM staging_prices s WHERE p.sku = s.sku;Удалить дубли в источнике перед обновлением
WITH deduped AS ( SELECT DISTINCT ON (email) email, country FROM staging_users ORDER BY email, updated_at DESC ) UPDATE users u SET country = d.country FROM deduped d WHERE u.email = d.email;Обновить по агрегату группы
UPDATE employees e SET salary = salary * 1.10 FROM ( SELECT dept FROM employees GROUP BY dept HAVING AVG(salary) < 50000 ) low WHERE e.dept = low.dept;Записать ранг зарплаты
UPDATE employees e SET salary_rank = r.rnk FROM ( SELECT id, RANK() OVER ( PARTITION BY dept ORDER BY salary DESC ) AS rnk FROM employees ) r WHERE e.id = r.id;Вернуть обновлённые строки
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id RETURNING o.id, o.user_id, o.country AS new_country;Получить старое и новое значение
WITH old_rows AS ( SELECT o.id, o.country AS old_country, u.country AS new_country FROM orders o JOIN users u ON o.user_id = u.id ), updated AS ( UPDATE orders o SET country = old_rows.new_country FROM old_rows WHERE o.id = old_rows.id RETURNING o.id, old_rows.old_country, o.country AS new_country ) SELECT * FROM updated ORDER BY id;MySQL-аналог
UPDATE orders o JOIN users u ON o.user_id = u.id SET o.country = u.country;Частые ошибки
Забыли условие связи
Опасно:
UPDATE orders o SET country = u.country FROM users u;Правильно:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Источник возвращает несколько строк на одну целевую строку
Опасно:
UPDATE users u SET country = s.country FROM staging_users s WHERE u.email = s.email;если в
staging_usersнесколько строк с одним email.Сначала проверьте:
SELECT email, COUNT(*) FROM staging_users GROUP BY email HAVING COUNT(*) > 1;Ждут, что строки без пары обнулятся сами
Запрос:
UPDATE users u SET total_spent = s.total_spent FROM ( SELECT user_id, SUM(amount) AS total_spent FROM orders GROUP BY user_id ) s WHERE u.id = s.user_id;не тронет пользователей без заказов.
Если их нужно обнулить, источник должен вернуть их тоже, или нужен отдельный
UPDATE.Обновляют строки, где значение уже совпадает
Лучше добавлять:
AND o.country IS DISTINCT FROM u.countryчтобы не создавать лишние обновления.
Не проверяют SELECT перед UPDATE
Перед большим обновлением полезно сделать:
SELECT ... FROM target JOIN source ON ... WHERE ...и убедиться, что строки и новые значения правильные.
Что важно запомнить
UPDATE ... FROMобновляет целевую таблицу данными из другой таблицы или подзапроса.Пример:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;Главные правила:
UPDATE;FROM;WHERE;JOIN, но без отдельногоON;FROM;RETURNINGпоказывает уже обновлённые значения;UPDATE ... JOIN.Короткий вывод
UPDATE ... FROMнужен, когда новое значение для строки зависит от другой таблицы или подзапроса.Например:
UPDATE orders o SET country = u.country FROM users u WHERE o.user_id = u.id;читается так:
Главная мысль:
Используйте его для синхронизации, staging-таблиц, агрегатов и обновлений по связанным данным. Но перед запуском всегда проверяйте условие связи: в
UPDATEошибка обычно дороже, чем вSELECT.