Оператор UPDATE в PostgreSQL — это важный инструмент для модификации существующих записей в таблицах. В этой статье мы подробно разберем все аспекты использования UPDATE: от базового синтаксиса до сложных сценариев с подзапросами, транзакциями и возвратом обновленных данных.

Подготовка: структура таблицы employees

Для примеров будем использовать таблицу сотрудников:

sql
CREATE TABLE employees (
    employee_id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    hire_date DATE NOT NULL,
    salary DECIMAL(10, 2),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP
);
Базовый синтаксис оператора UPDATE
Основная структура команды
sql
UPDATE table_name
SET column1 = value1, 
    column2 = value2,
    ...
WHERE condition;

Важное предупреждение: Без условия WHERE обновятся ВСЕ записи в таблице!

Пример 1: Базовое обновление одного поля
Задача: Изменить зарплату конкретного сотрудника
sql
UPDATE employees
SET salary = 60000.00,
    updated_at = CURRENT_TIMESTAMP
WHERE employee_id = 14;

Разбор запроса:

  • UPDATE employees — указываем таблицу для обновления

  • SET salary = 60000.00 — устанавливаем новое значение зарплаты

  • updated_at = CURRENT_TIMESTAMP — обновляем метку времени изменения

  • WHERE employee_id = 14 — условие: только сотрудник с ID 14

Проверка результата:

sql
SELECT employee_id, first_name, last_name, salary, updated_at
FROM employees
WHERE employee_id = 14;
Пример 2: Обновление на основе текущих значений
Задача: Увеличить зарплату на 10% для всех активных сотрудников
sql
UPDATE employees
SET salary = salary * 1.10,
    updated_at = CURRENT_TIMESTAMP
WHERE is_active = TRUE;

Что происходит:

  • salary * 1.10 — увеличиваем текущую зарплату на 10%

  • WHERE is_active = TRUE — обновляем только активных сотрудников

Альтернативный вариант с округлением:

sql
UPDATE employees
SET salary = ROUND(salary * 1.10, 2),
    updated_at = CURRENT_TIMESTAMP
WHERE is_active = TRUE;
Пример 3: Обновление с подзапросами
Задача: Установить сотрудникам с фамилией 'Сергеев' максимальную зарплату среди всех активных сотрудников
sql
UPDATE employees
SET salary = (
    SELECT MAX(salary) 
    FROM employees 
    WHERE is_active = TRUE
),
updated_at = CURRENT_TIMESTAMP
WHERE last_name = 'Сергеев'
  AND is_active = TRUE;

Пошаговое выполнение:

  1. Подзапрос (SELECT MAX(salary)...) выполняется первым

  2. Возвращает максимальную зарплату среди активных сотрудников

  3. Основной UPDATE использует это значение для обновления

Более безопасный вариант с проверкой:

sql
UPDATE employees e1
SET salary = (
    SELECT MAX(salary) 
    FROM employees 
    WHERE is_active = TRUE
),
updated_at = CURRENT_TIMESTAMP
WHERE EXISTS (
    SELECT 1 
    FROM employees e2 
    WHERE e2.last_name = 'Сергеев' 
      AND e2.is_active = TRUE
      AND e1.employee_id = e2.employee_id
);
Пример 4: Массовое обновление с оператором IN
Задача: Деактивировать несколько сотрудников
sql
UPDATE employees
SET is_active = FALSE,
    updated_at = CURRENT_TIMESTAMP
WHERE employee_id IN (3000, 3001, 3002);

Альтернативный вариант с диапазоном:

sql
UPDATE employees
SET is_active = FALSE,
    updated_at = CURRENT_TIMESTAMP
WHERE employee_id BETWEEN 3000 AND 3002;

Обновление с подзапросом в IN:

sql
UPDATE employees SET is_active = FALSE, updated_at = CURRENT_TIMESTAMP WHERE employee_id IN ( SELECT employee_id FROM employees WHERE hire_date < '2023-01-01' AND salary < 50000 );
Пример 5: Дифференцированное обновление с оператором CASE
Задача: Применить разный процент повышения зарплаты в зависимости от текущего уровня
sql
UPDATE employees
SET salary = CASE
    WHEN salary < 40000 THEN salary * 1.15  -- 15% для низких зарплат
    WHEN salary BETWEEN 40000 AND 70000 THEN salary * 1.10  -- 10%
    WHEN salary > 70000 THEN salary * 1.05  -- 5% для высоких зарплат
    ELSE salary
END,
updated_at = CURRENT_TIMESTAMP
WHERE is_active = TRUE;

Расширенный пример с несколькими условиями:

sql
UPDATE employees
SET salary = CASE
    WHEN salary < 40000 AND years_of_service > 5 THEN salary * 1.20
    WHEN salary < 40000 THEN salary * 1.15
    WHEN salary BETWEEN 40000 AND 70000 AND performance_score > 80 THEN salary * 1.12
    WHEN salary BETWEEN 40000 AND 70000 THEN salary * 1.10
    WHEN salary > 70000 AND performance_score > 90 THEN salary * 1.08
    WHEN salary > 70000 THEN salary * 1.05
    ELSE salary
END,
bonus_eligibility = CASE
    WHEN salary * 1.10 > 80000 THEN TRUE
    ELSE FALSE
END,
updated_at = CURRENT_TIMESTAMP
WHERE is_active = TRUE;
Пример 6: Возврат обновленных данных с RETURNING
Особенность PostgreSQL: возможность получить обновленные данные сразу
sql
UPDATE employees
SET salary = salary * 1.10,
    updated_at = CURRENT_TIMESTAMP
WHERE department_id = 3
  AND is_active = TRUE
RETURNING 
    employee_id,
    first_name,
    last_name,
    salary AS new_salary,
    (salary / 1.10) AS old_salary,
    ((salary / 1.10) * 0.10) AS increase_amount;

Результат будет содержать:

text
 employee_id | first_name | last_name | new_salary | old_salary | increase_amount
-------------|------------|-----------|------------|------------|-----------------
      101    | Иван       | Петров    |  55000.00  |  50000.00  |   5000.00
      102    | Мария      | Сидорова  |  66000.00  |  60000.00  |   6000.00

Сохранение результатов в таблицу:

sql
UPDATE employees
SET salary = salary * 1.10,
    updated_at = CURRENT_TIMESTAMP
WHERE is_active = TRUE
RETURNING 
    employee_id,
    first_name,
    last_name,
    salary AS new_salary
INTO TEMP TABLE salary_update_log;
Пример 7: Обновление с использованием CTE (Common Table Expressions)
Задача: Обновить только первые три записи с наибольшей зарплатой
sql
WITH top_employees AS (
    SELECT employee_id
    FROM employees
    WHERE is_active = TRUE
    ORDER BY salary DESC
    LIMIT 3
)
UPDATE employees e
SET salary = salary * 1.15,
    updated_at = CURRENT_TIMESTAMP
FROM top_employees te
WHERE e.employee_id = te.employee_id;

Более сложный пример CTE:

sql
WITH department_stats AS (
    SELECT 
        department_id,
        AVG(salary) as avg_salary,
        COUNT(*) as employee_count
    FROM employees
    WHERE is_active = TRUE
    GROUP BY department_id
),
underpaid_employees AS (
    SELECT 
        e.employee_id,
        e.salary,
        ds.avg_salary,
        (ds.avg_salary - e.salary) as salary_gap
    FROM employees e
    JOIN department_stats ds ON e.department_id = ds.department_id
    WHERE e.salary < ds.avg_salary * 0.8
      AND e.is_active = TRUE
)
UPDATE employees e
SET salary = ue.avg_salary,
    updated_at = CURRENT_TIMESTAMP
FROM underpaid_employees ue
WHERE e.employee_id = ue.employee_id
RETURNING 
    e.employee_id,
    e.first_name,
    e.last_name,
    ue.salary AS old_salary,
    e.salary AS new_salary,
    ue.salary_gap AS corrected_gap;
Пример 8: Безопасное обновление с транзакциями
Рекомендуемый подход для критически важных данных
sql
BEGIN;  -- Начало транзакции

-- Обновление данных
UPDATE employees
SET salary = salary * 1.10,
    updated_at = CURRENT_TIMESTAMP
WHERE department_id = 5
  AND is_active = TRUE;

-- Проверка результата перед подтверждением
SELECT 
    COUNT(*) as updated_count,
    SUM(salary) as total_salary_after,
    AVG(salary) as avg_salary_after
FROM employees
WHERE department_id = 5
  AND is_active = TRUE;

-- Если все корректно, подтверждаем изменения
COMMIT;

-- Если что-то не так, откатываем
-- ROLLBACK;

Расширенный пример с точками сохранения:

sql
BEGIN;

-- Точка сохранения 1
SAVEPOINT before_salary_update;

UPDATE employees
SET salary = salary * 1.10
WHERE department_id = 3;

-- Проверка
SELECT COUNT(*) FROM employees WHERE salary > 200000;
-- Если слишком много высоких зарплат, откатываемся к точке сохранения
ROLLBACK TO before_salary_update;

-- Альтернативное обновление
UPDATE employees
SET salary = LEAST(salary * 1.10, 150000)
WHERE department_id = 3;

COMMIT;
Продвинутые техники обновления
Техника 1: Обновление с объединением таблиц (JOIN)
sql
UPDATE employees e
SET 
    salary = d.target_salary,
    department_id = d.new_department_id
FROM department_updates d
WHERE e.employee_id = d.employee_id
  AND e.is_active = TRUE;
Техника 2: Обновление на основе агрегатных функций
sql
UPDATE employees e
SET 
    salary_rank = sub.rank,
    department_avg_salary = sub.avg_dept_salary
FROM (
    SELECT 
        employee_id,
        RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) as rank,
        AVG(salary) OVER (PARTITION BY department_id) as avg_dept_salary
    FROM employees
    WHERE is_active = TRUE
) sub
WHERE e.employee_id = sub.employee_id;
Техника 3: Обновление с обработкой JSON данных
sql
UPDATE employees
SET 
    contact_info = contact_info || '{"phone": "+7-999-123-45-67"}',
    skills = skills || '["PostgreSQL", "Python"]'::jsonb,
    updated_at = CURRENT_TIMESTAMP
WHERE employee_id = 42;
Безопасность и валидация при обновлении
Валидация данных перед обновлением
sql
CREATE OR REPLACE FUNCTION safe_salary_update(
    p_employee_id INTEGER,
    p_new_salary DECIMAL
)
RETURNS VOID AS $$
DECLARE
    v_current_salary DECIMAL;
    v_max_increase_percent DECIMAL := 50.0;
BEGIN
    -- Получение текущей зарплаты
    SELECT salary INTO v_current_salary
    FROM employees
    WHERE employee_id = p_employee_id
      AND is_active = TRUE;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Сотрудник не найден или неактивен';
    END IF;

    -- Проверка увеличения зарплаты
    IF (p_new_salary - v_current_salary) / v_current_salary * 100 > v_max_increase_percent THEN
        RAISE EXCEPTION 'Увеличение зарплаты более чем на % процентов запрещено', 
                        v_max_increase_percent;
    END IF;

    -- Проверка минимальной зарплаты
    IF p_new_salary < 30000 THEN
        RAISE EXCEPTION 'Зарплата не может быть меньше 30000';
    END IF;

    -- Выполнение обновления
    UPDATE employees
    SET salary = p_new_salary,
        updated_at = CURRENT_TIMESTAMP
    WHERE employee_id = p_employee_id;

    -- Логирование
    INSERT INTO salary_change_log (
        employee_id,
        old_salary,
        new_salary,
        changed_by,
        change_date
    ) VALUES (
        p_employee_id,
        v_current_salary,
        p_new_salary,
        CURRENT_USER,
        CURRENT_TIMESTAMP
    );

END;
$$ LANGUAGE plpgsql;
Использование триггеров для аудита изменений
sql
CREATE TABLE employee_audit (
    audit_id SERIAL PRIMARY KEY,
    employee_id INTEGER,
    changed_column VARCHAR(50),
    old_value TEXT,
    new_value TEXT,
    changed_by VARCHAR(100),
    change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION log_employee_changes()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.salary != NEW.salary THEN
        INSERT INTO employee_audit (
            employee_id,
            changed_column,
            old_value,
            new_value,
            changed_by
        ) VALUES (
            NEW.employee_id,
            'salary',
            OLD.salary::TEXT,
            NEW.salary::TEXT,
            CURRENT_USER
        );
    END IF;

    IF OLD.is_active != NEW.is_active THEN
        INSERT INTO employee_audit (
            employee_id,
            changed_column,
            old_value,
            new_value,
            changed_by
        ) VALUES (
            NEW.employee_id,
            'is_active',
            OLD.is_active::TEXT,
            NEW.is_active::TEXT,
            CURRENT_USER
        );
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_employee_audit
AFTER UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION log_employee_changes();
Оптимизация производительности UPDATE
Рекомендация 1: Использование индексов для условий WHERE
sql
-- Создание индексов для часто используемых условий
CREATE INDEX idx_employees_active ON employees(is_active);
CREATE INDEX idx_employees_department ON employees(department_id);
CREATE INDEX idx_employees_salary ON employees(salary);
Рекомендация 2: Пакетное обновление для больших объемов
sql
-- Обновление по частям
DO $$
DECLARE
    batch_size INTEGER := 1000;
    offset_val INTEGER := 0;
    total_updated INTEGER := 0;
BEGIN
    LOOP
        UPDATE employees
        SET salary = salary * 1.05
        WHERE employee_id IN (
            SELECT employee_id
            FROM employees
            WHERE is_active = TRUE
            ORDER BY employee_id
            LIMIT batch_size
            OFFSET offset_val
        );

        GET DIAGNOSTICS total_updated = ROW_COUNT;
        offset_val := offset_val + batch_size;
        
        EXIT WHEN total_updated = 0;
        
        -- Пауза для снижения нагрузки
        PERFORM pg_sleep(0.1);
    END LOOP;
END;
$$;
Рекомендация 3: Отключение триггеров при массовом обновлении
sql
-- Временное отключение триггеров
ALTER TABLE employees DISABLE TRIGGER ALL;

-- Массовое обновление
UPDATE employees
SET salary = salary * 1.10
WHERE is_active = TRUE;

-- Включение триггеров обратно
ALTER TABLE employees ENABLE TRIGGER ALL;
Лучшие практики работы с UPDATE
Практика 1: Всегда используйте WHERE
sql
-- НЕПРАВИЛЬНО (обновит все записи!):
UPDATE employees SET salary = 50000;

-- ПРАВИЛЬНО:
UPDATE employees 
SET salary = 50000
WHERE employee_id = 42;
Практика 2: Тестируйте условия SELECT перед UPDATE
sql
-- Сначала проверьте, какие записи будут обновлены
SELECT COUNT(*), employee_id, first_name, last_name
FROM employees
WHERE department_id = 3
  AND hire_date < '2023-01-01';

-- Затем выполняйте UPDATE
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = 3
  AND hire_date < '2023-01-01';
Практика 3: Используйте транзакции для связанных операций
sql
BEGIN;

UPDATE employees
SET salary = salary * 1.10
WHERE department_id = 5;

UPDATE department_budgets
SET salary_budget = salary_budget * 1.10
WHERE department_id = 5;

INSERT INTO budget_audit (
    department_id,
    old_budget,
    new_budget,
    change_date
) VALUES (
    5,
    (SELECT salary_budget FROM department_budgets WHERE department_id = 5 FOR UPDATE),
    (SELECT salary_budget * 1.10 FROM department_budgets WHERE department_id = 5),
    CURRENT_TIMESTAMP
);

COMMIT;
Практика 4: Ведение журнала изменений
sql
-- Создание таблицы для журналирования
CREATE TABLE update_history (
    history_id SERIAL PRIMARY KEY,
    table_name VARCHAR(100),
    operation VARCHAR(10),
    old_data JSONB,
    new_data JSONB,
    executed_by VARCHAR(100),
    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Функция для логирования
CREATE OR REPLACE FUNCTION log_update_changes()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO update_history (
        table_name,
        operation,
        old_data,
        new_data,
        executed_by
    ) VALUES (
        TG_TABLE_NAME,
        TG_OP,
        row_to_json(OLD),
        row_to_json(NEW),
        CURRENT_USER
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Мониторинг и отладка операций UPDATE
Просмотр последних обновлений
sql
-- Недавние обновления зарплаты
SELECT 
    e.employee_id,
    e.first_name,
    e.last_name,
    sc.old_salary,
    sc.new_salary,
    sc.change_date,
    sc.changed_by
FROM employees e
JOIN salary_change_log sc ON e.employee_id = sc.employee_id
ORDER BY sc.change_date DESC
LIMIT 10;
Анализ производительности UPDATE
sql
-- Статистика выполнения UPDATE-запросов
SELECT 
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM pg_stat_statements
WHERE query LIKE 'UPDATE%employees%'
ORDER BY total_time DESC
LIMIT 10;
Отслеживание блокировок
sql
-- Активные блокировки при UPDATE
SELECT 
    pid,
    usename,
    query,
    age(clock_timestamp(), query_start) as query_age,
    wait_event_type,
    wait_event
FROM pg_stat_activity
WHERE query LIKE 'UPDATE%'
  AND state = 'active'
ORDER BY query_start;
Заключение

Оператор UPDATE в PostgreSQL — это гибкий и эффективный инструмент для изменения существующих данных. Ключевые моменты для успешной работы:

  1. Всегда используйте WHERE — чтобы избежать обновления всех записей

  2. Тестируйте условия — сначала SELECT, потом UPDATE

  3. Используйте транзакции — для согласованности важных изменений

  4. Применяйте RETURNING — для получения обновленных данных

  5. Ведите аудит — для отслеживания изменений

  6. Оптимизируйте производительность — через индексы и пакетную обработку

Начните с простых обновлений, постепенно осваивая более сложные сценарии с подзапросами, CTE и транзакциями. Помните, что грамотное использование UPDATE — залог целостности и согласованности данных в вашей базе.

В следующих материалах мы изучим оператор DELETE для удаления данных и создание триггеров для автоматизации бизнес-логики.

Picture background

Статья подготовлена по материалам обучающего видео для вашего удобства.