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

Подготовка: структура таблицы 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
);

-- Таблица для архива удаленных сотрудников
CREATE TABLE employees_archive (
    archive_id SERIAL PRIMARY KEY,
    employee_id INTEGER,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    hire_date DATE,
    salary DECIMAL(10, 2),
    deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    deleted_by VARCHAR(100) DEFAULT CURRENT_USER
);
Базовый синтаксис оператора DELETE
Основная структура команды
sql
DELETE FROM table_name
WHERE condition;

КРИТИЧЕСКИ ВАЖНО: Без условия WHERE оператор DELETE удалит ВСЕ записи из таблицы!

Золотое правило: сначала проверяйте, потом удаляйте
Шаг 1: Проверка данных перед удалением
sql
-- Всегда начинайте с SELECT, чтобы увидеть, что будет удалено
SELECT 
    employee_id,
    first_name,
    last_name,
    hire_date,
    email
FROM employees
WHERE employee_id = 3090;
Шаг 2: Проверка количества записей
sql
-- Узнайте точное количество записей, которые будут удалены
SELECT COUNT(*) as records_to_delete
FROM employees
WHERE employee_id = 3090;
Пример 1: Безопасное удаление конкретной записи
Задача: Удалить сотрудника с конкретным ID
sql
-- Шаг 1: Проверка существования записи
SELECT COUNT(*) 
FROM employees 
WHERE employee_id = 3090;

-- Шаг 2: Удаление с явным условием
DELETE FROM employees
WHERE employee_id = 3090
RETURNING 
    employee_id,
    first_name,
    last_name,
    email;

Возвращаемый результат показывает удаленные данные:

text
 employee_id | first_name | last_name | email
-------------|------------|-----------|--------------------
     3090    | Иван       | Петров    | Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

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

sql
BEGIN;

-- Шаг 1: Архивирование данных
INSERT INTO employees_archive (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
)
SELECT 
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
FROM employees
WHERE employee_id = 3090;

-- Шаг 2: Удаление из основной таблицы
DELETE FROM employees
WHERE employee_id = 3090;

COMMIT;
Пример 2: Удаление по временному условию
Задача: Удалить сотрудников, принятых до 2021 года
sql
-- Шаг 1: Проверка количества записей
SELECT 
    COUNT(*) as total_to_delete,
    MIN(hire_date) as earliest_date,
    MAX(hire_date) as latest_date
FROM employees
WHERE hire_date < '2021-01-01'
  AND is_active = FALSE; -- Удаляем только неактивных

-- Шаг 2: Просмотр данных перед удалением
SELECT 
    employee_id,
    first_name,
    last_name,
    hire_date,
    email,
    is_active
FROM employees
WHERE hire_date < '2021-01-01'
  AND is_active = FALSE
ORDER BY hire_date
LIMIT 10;

-- Шаг 3: Удаление с архивацией
BEGIN;

-- Архивирование
INSERT INTO employees_archive (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
)
SELECT 
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
FROM employees
WHERE hire_date < '2021-01-01'
  AND is_active = FALSE;

-- Удаление
DELETE FROM employees
WHERE hire_date < '2021-01-01'
  AND is_active = FALSE
RETURNING 
    employee_id,
    first_name || ' ' || last_name as full_name,
    hire_date;

COMMIT;
Пример 3: Использование RETURNING для получения информации об удаленных данных
Особенность PostgreSQL: возврат удаленных записей
sql
DELETE FROM employees
WHERE department_id = 5
  AND is_active = FALSE
RETURNING 
    employee_id,
    first_name,
    last_name,
    hire_date,
    email,
    salary;

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

sql
-- Создание таблицы для логирования удалений
CREATE TABLE IF NOT EXISTS deletion_log (
    log_id SERIAL PRIMARY KEY,
    operation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    table_name VARCHAR(100),
    deleted_records JSONB,
    deleted_by VARCHAR(100) DEFAULT CURRENT_USER
);

-- Удаление с сохранением логов
WITH deleted_records AS (
    DELETE FROM employees
    WHERE hire_date < '2020-01-01'
      AND is_active = FALSE
    RETURNING *
)
INSERT INTO deletion_log (
    table_name,
    deleted_records
)
SELECT 
    'employees',
    jsonb_agg(to_jsonb(dr))
FROM deleted_records dr;
Пример 4: Безопасный подход с транзакциями
Наиболее надежный метод для важных операций
sql
BEGIN;  -- Начало транзакции

-- Шаг 1: Проверка данных перед удалением
SELECT 
    COUNT(*) as total_count,
    STRING_AGG(employee_id::TEXT, ', ') as employee_ids
FROM employees
WHERE department_id = 10
  AND is_active = FALSE;

-- Шаг 2: Архивирование
INSERT INTO employees_archive (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
)
SELECT 
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary
FROM employees
WHERE department_id = 10
  AND is_active = FALSE;

-- Шаг 3: Удаление
DELETE FROM employees
WHERE department_id = 10
  AND is_active = FALSE;

-- Шаг 4: Проверка результата
SELECT 
    'Удалено записей: ' || COUNT(*) as result
FROM employees_archive
WHERE archive_id > (SELECT COALESCE(MAX(archive_id), 0) - COUNT(*) 
                    FROM employees_archive);

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

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

Использование точек сохранения (SAVEPOINT):

sql
BEGIN;

-- Точка сохранения перед удалением
SAVEPOINT before_deletion;

-- Удаление части данных
DELETE FROM employees
WHERE employee_id IN (1001, 1002, 1003);

-- Проверка
SELECT COUNT(*) FROM employees WHERE department_id = 3;
-- Если удалено слишком много, откатываемся к точке
-- ROLLBACK TO before_deletion;

-- Продолжаем удаление
DELETE FROM employees
WHERE employee_id = 1004;

COMMIT;
ОПАСНЫЕ ЗАПРОСЫ: Чего никогда нельзя делать
⚠️ ОПАСНЫЙ ЗАПРОС 1: DELETE без WHERE
sql
-- НИКОГДА не выполняйте это в рабочей базе!
-- Удалит ВСЕ данные из таблицы без возможности отмены
DELETE FROM employees;

Безопасная альтернатива:

sql
-- Всегда добавляйте условие WHERE
DELETE FROM employees 
WHERE is_active = FALSE 
  AND last_activity_date < CURRENT_DATE - INTERVAL '2 years';
⚠️ ОПАСНЫЙ ЗАПРОС 2: TRUNCATE TABLE
sql
-- НИКОГДА не используйте без необходимости!
-- Удаляет ВСЕ данные БЕЗ возможности ROLLBACK в той же транзакции
-- Сбрасывает последовательности (SERIAL)
TRUNCATE TABLE employees;

Безопасная альтернатива с каскадным удалением:

sql
-- Удаление с явным указанием зависимостей
DELETE FROM employees 
WHERE employee_id IN (
    SELECT employee_id 
    FROM employees 
    WHERE is_active = FALSE 
      AND termination_date < '2022-01-01'
);

-- Очистка зависимых таблиц
DELETE FROM employee_benefits 
WHERE employee_id NOT IN (SELECT employee_id FROM employees);
Пример 5: Удаление с использованием подзапросов
Задача: Удалить сотрудников без активности за последний год
sql
-- Шаг 1: Создание временного представления для проверки
CREATE TEMPORARY VIEW employees_to_delete AS
SELECT 
    e.employee_id,
    e.first_name,
    e.last_name,
    e.hire_date,
    MAX(a.activity_date) as last_activity
FROM employees e
LEFT JOIN employee_activity a ON e.employee_id = a.employee_id
WHERE e.is_active = TRUE
GROUP BY e.employee_id, e.first_name, e.last_name, e.hire_date
HAVING MAX(a.activity_date) < CURRENT_DATE - INTERVAL '1 year'
   OR MAX(a.activity_date) IS NULL;

-- Шаг 2: Проверка данных
SELECT COUNT(*) as total_to_delete FROM employees_to_delete;
SELECT * FROM employees_to_delete LIMIT 10;

-- Шаг 3: Безопасное удаление
BEGIN;

-- Архивирование
INSERT INTO employees_archive (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary,
    deletion_reason
)
SELECT 
    e.employee_id,
    e.first_name,
    e.last_name,
    e.email,
    e.hire_date,
    e.salary,
    'Отсутствие активности более 1 года'
FROM employees e
WHERE e.employee_id IN (SELECT employee_id FROM employees_to_delete);

-- Удаление
DELETE FROM employees
WHERE employee_id IN (SELECT employee_id FROM employees_to_delete)
RETURNING 
    employee_id,
    first_name || ' ' || last_name as full_name,
    hire_date;

COMMIT;

-- Очистка временного представления
DROP VIEW employees_to_delete;
Пример 6: Каскадное удаление связанных данных
Задача: Удалить сотрудника со всеми связанными записями
sql
BEGIN;

-- Создание таблицы для отслеживания удаленных зависимостей
CREATE TEMPORARY TABLE deleted_dependencies (
    table_name VARCHAR(100),
    deleted_count INTEGER
);

-- 1. Удаление истории должностей
WITH deleted_positions AS (
    DELETE FROM position_history
    WHERE employee_id = 3090
    RETURNING *
)
INSERT INTO deleted_dependencies (table_name, deleted_count)
SELECT 'position_history', COUNT(*) FROM deleted_positions;

-- 2. Удаление отпусков
WITH deleted_vacations AS (
    DELETE FROM employee_vacations
    WHERE employee_id = 3090
    RETURNING *
)
INSERT INTO deleted_dependencies (table_name, deleted_count)
SELECT 'employee_vacations', COUNT(*) FROM deleted_vacations;

-- 3. Удаление оценок производительности
WITH deleted_reviews AS (
    DELETE FROM performance_reviews
    WHERE employee_id = 3090
    RETURNING *
)
INSERT INTO deleted_dependencies (table_name, deleted_count)
SELECT 'performance_reviews', COUNT(*) FROM deleted_reviews;

-- 4. Архивирование основного сотрудника
INSERT INTO employees_archive (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary,
    deletion_reason
)
SELECT 
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary,
    'Полное удаление со всеми зависимостями'
FROM employees
WHERE employee_id = 3090;

-- 5. Удаление основного сотрудника
DELETE FROM employees
WHERE employee_id = 3090
RETURNING 
    employee_id,
    first_name,
    last_name;

-- 6. Отчет об удаленных данных
SELECT * FROM deleted_dependencies;

COMMIT;

-- Очистка временной таблицы
DROP TABLE deleted_dependencies;
Безопасные паттерны удаления данных
Паттерн 1: Мягкое удаление (soft delete)
sql
-- Вместо физического удаления помечаем запись как удаленную
UPDATE employees
SET 
    is_active = FALSE,
    deleted_at = CURRENT_TIMESTAMP,
    deleted_by = CURRENT_USER,
    email = CONCAT('deleted_', employee_id, '_', email)
WHERE employee_id = 3090;

-- Для выборки только активных записей
SELECT * FROM employees WHERE is_active = TRUE;
Паттерн 2: Периодическая очистка архивных данных
sql
-- Удаление архивных записей старше 7 лет
DELETE FROM employees_archive
WHERE deleted_at < CURRENT_DATE - INTERVAL '7 years'
  AND employee_id NOT IN (
    SELECT employee_id 
    FROM legal_hold_records  -- Исключаем записи под юридическим удержанием
);

-- С предварительной проверкой
SELECT 
    COUNT(*) as to_delete,
    MIN(deleted_at) as oldest_record,
    MAX(deleted_at) as newest_record
FROM employees_archive
WHERE deleted_at < CURRENT_DATE - INTERVAL '7 years';
Паттерн 3: Пакетное удаление больших объемов
sql
-- Удаление по частям для снижения нагрузки
DO $$
DECLARE
    batch_size INTEGER := 1000;
    deleted_count INTEGER := 0;
    total_deleted INTEGER := 0;
BEGIN
    LOOP
        DELETE FROM employees_archive
        WHERE deleted_at < '2015-01-01'
          AND archive_id IN (
              SELECT archive_id
              FROM employees_archive
              WHERE deleted_at < '2015-01-01'
              ORDER BY archive_id
              LIMIT batch_size
          );
        
        GET DIAGNOSTICS deleted_count = ROW_COUNT;
        total_deleted := total_deleted + deleted_count;
        
        -- Пауза между партиями
        PERFORM pg_sleep(0.5);
        
        -- Выход из цикла при отсутствии удаленных записей
        EXIT WHEN deleted_count = 0;
        
        -- Логирование прогресса
        RAISE NOTICE 'Удалено записей: %, всего: %', 
                     deleted_count, total_deleted;
    END LOOP;
    
    RAISE NOTICE 'Всего удалено записей: %', total_deleted;
END $$;
Мониторинг и аудит операций DELETE
Создание системы аудита удалений
sql
-- Таблица для аудита
CREATE TABLE delete_audit_log (
    audit_id SERIAL PRIMARY KEY,
    operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    table_name VARCHAR(100),
    operation_type VARCHAR(20),
    affected_rows INTEGER,
    condition_used TEXT,
    executed_by VARCHAR(100),
    client_address INET,
    application_name VARCHAR(100)
);

-- Функция для логирования
CREATE OR REPLACE FUNCTION log_delete_operation()
RETURNS TRIGGER AS $$
DECLARE
    v_condition TEXT;
BEGIN
    -- Получение условия WHERE (упрощенно)
    v_condition := current_query();
    
    INSERT INTO delete_audit_log (
        table_name,
        operation_type,
        affected_rows,
        condition_used,
        executed_by,
        client_address,
        application_name
    ) VALUES (
        TG_TABLE_NAME,
        TG_OP,
        (SELECT COUNT(*) FROM OLD),
        v_condition,
        CURRENT_USER,
        inet_client_addr(),
        current_setting('application_name')
    );
    
    RETURN OLD;
END;
$$ LANGUAGE plpgsql;

-- Триггер для основных таблиц
CREATE TRIGGER trg_audit_employees_delete
AFTER DELETE ON employees
FOR EACH STATEMENT
EXECUTE FUNCTION log_delete_operation();
Просмотр истории удалений
sql
-- Последние 10 операций удаления
SELECT 
    audit_id,
    operation_time,
    table_name,
    affected_rows,
    executed_by,
    client_address
FROM delete_audit_log
WHERE operation_type = 'DELETE'
ORDER BY operation_time DESC
LIMIT 10;

-- Статистика по пользователям
SELECT 
    executed_by,
    COUNT(*) as delete_operations,
    SUM(affected_rows) as total_deleted_rows,
    MIN(operation_time) as first_operation,
    MAX(operation_time) as last_operation
FROM delete_audit_log
WHERE operation_type = 'DELETE'
GROUP BY executed_by
ORDER BY total_deleted_rows DESC;
Восстановление ошибочно удаленных данных
Стратегия восстановления из архива
sql
-- Восстановление конкретного сотрудника
BEGIN;

-- Проверка существования в архиве
SELECT 
    employee_id,
    first_name,
    last_name,
    deleted_at
FROM employees_archive
WHERE employee_id = 3090
ORDER BY deleted_at DESC
LIMIT 1;

-- Восстановление
INSERT INTO employees (
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary,
    is_active,
    created_at
)
SELECT 
    employee_id,
    first_name,
    last_name,
    email,
    hire_date,
    salary,
    TRUE,
    CURRENT_TIMESTAMP
FROM employees_archive
WHERE employee_id = 3090
ORDER BY deleted_at DESC
LIMIT 1;

-- Удаление из архива (опционально)
DELETE FROM employees_archive
WHERE employee_id = 3090
  AND deleted_at = (
      SELECT MAX(deleted_at)
      FROM employees_archive
      WHERE employee_id = 3090
  );

COMMIT;
Лучшие практики безопасного удаления
Практика 1: Всегда используйте WHERE с уникальными идентификаторами
sql
-- НЕПРАВИЛЬНО:
DELETE FROM employees WHERE last_name = 'Иванов';

-- ПРАВИЛЬНО:
DELETE FROM employees WHERE employee_id = 3090;
Практика 2: Используйте транзакции для групп операций
sql
BEGIN;
-- 1. Архивирование
INSERT INTO archive SELECT * FROM table WHERE condition;
-- 2. Удаление
DELETE FROM table WHERE condition;
-- 3. Проверка
SELECT COUNT(*) FROM archive WHERE condition;
COMMIT;
Практика 3: Ограничивайте права на DELETE
sql
-- Создание роли с ограниченными правами
CREATE ROLE data_cleaner;

-- Предоставление прав только на определенные таблицы
GRANT DELETE ON employees TO data_cleaner;
GRANT DELETE ON old_records TO data_cleaner;

-- Запрет удаления в критических таблицах
REVOKE DELETE ON financial_transactions FROM data_cleaner;
REVOKE DELETE ON user_accounts FROM data_cleaner;
Практика 4: Регулярное резервное копирование
sql
-- Автоматическое резервное копирование перед удалением
CREATE OR REPLACE FUNCTION backup_before_delete()
RETURNS TRIGGER AS $$
BEGIN
    EXECUTE format('CREATE TABLE %I_%s AS SELECT * FROM %I WHERE employee_id = %s',
                   TG_TABLE_NAME,
                   to_char(CURRENT_TIMESTAMP, 'YYYYMMDD_HH24MISS'),
                   TG_TABLE_NAME,
                   OLD.employee_id);
    RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Заключение

Оператор DELETE в PostgreSQL — это инструмент, требующий максимальной осторожности и дисциплины. Ключевые принципы безопасной работы:

  1. Всегда проверяйте данные перед удалением — сначала SELECT, потом DELETE

  2. Никогда не используйте DELETE без WHERE — это удалит все данные

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

  4. Ведите архив удаленных данных — для возможного восстановления

  5. Используйте RETURNING — для подтверждения удаленных записей

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

  7. Регулярно делайте резервные копии — последняя линия защиты

Помните: данные, удаленные через DELETE (без предварительного архивирования), восстановить крайне сложно, а часто — невозможно. Безопасность данных — это ваша прямая ответственность.

Начните внедрять безопасные практики уже сегодня, и вы избежите многих проблем в будущем. Лучше потратить дополнительное время на проверки и архивацию, чем столкнуться с безвозвратной потерей критически важной информации.

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

Picture background

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