sqlpostgresqlmysqlupdate

UPDATE ... FROM: Bulk Updates Driven by Joins and Subqueries

Update a table from another table or an aggregate, and avoid the missing-WHERE trap that overwrites every row.

9 min čteníReferencesql · postgresql · mysql · update · joins
Tento článek je momentálně v ruštině — anglický překlad se připravuje.

Обычный 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;

-- check the result
-- if everything looks fine:
COMMIT;

-- if something is wrong:
-- ROLLBACK;

Строки без пары не обновляются

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;

Что здесь происходит:

  1. подзапрос s считает сумму оплаченных заказов по каждому пользователю;
  2. UPDATE users берёт эту сумму;
  3. WHERE u.id = s.user_id связывает пользователя с агрегатом;
  4. 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.

Procvičujte na reálných úlohách

Řešte úlohy v SQL trenéru s okamžitým hodnocením a nápovědami.

Otevřít trenéra