← Назад к вопросам

Зачем нужен TCL в БД?

1.8 Middle🔥 171 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

TCL в базах данных

TCL (Transaction Control Language) — это подмножество SQL команд для управления транзакциями в базах данных. Транзакции критичны для обеспечения консистентности данных и целостности БД, особенно в production системах.

Что такое транзакция

Транзакция — это логическая единица работы, состоящая из нескольких операций (INSERT, UPDATE, DELETE, SELECT), которые либо все выполняются успешно, либо все откатываются назад.

Примеры TCL команд:
- BEGIN / START TRANSACTION — начать транзакцию
- COMMIT — подтвердить (сохранить) все изменения
- ROLLBACK — отменить все изменения
- SAVEPOINT — промежуточная точка в транзакции

Классический пример: Перевод денег между счетами

Без транзакции (грозит потерей денег!)

import sqlite3

conn = sqlite3.connect(':memory:')
c = conn.cursor()

# Создаем счета
c.execute('CREATE TABLE accounts (id INT, balance DECIMAL)')
c.execute('INSERT INTO accounts VALUES (1, 1000)')  # Alice: 1000
c.execute('INSERT INTO accounts VALUES (2, 500)')   # Bob: 500

# ❌ Без транзакции — опасно!
def transfer_money_bad(from_id, to_id, amount):
    # Шаг 1: Снимаем со счета отправителя
    c.execute(f'UPDATE accounts SET balance = balance - ? WHERE id = ?',
              (amount, from_id))
    
    # ОПАСНОСТЬ: Если здесь произойдет ошибка (отключится электричество,
    # упадет приложение и т.д.), то деньги снимут, но не добавят!
    
    # Шаг 2: Добавляем на счет получателя
    c.execute(f'UPDATE accounts SET balance = balance + ? WHERE id = ?',
              (amount, to_id))
    
    conn.commit()

# Если происходит ошибка между шагами:
# Alice: 1000 → 900 (снимают)
# [ОШИБКА! Приложение упало]
# Bob: 500 → 500 (не добавляют)
# ПОТЕРЯ 100 руб!

С транзакцией (правильно)

import sqlite3

conn = sqlite3.connect(':memory:')
c = conn.cursor()

# ✅ С транзакцией
def transfer_money_good(from_id, to_id, amount):
    try:
        c.execute('BEGIN')  # Начинаем транзакцию
        
        # Шаг 1: Снимаем
        c.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?',
                  (amount, from_id))
        
        # Проверяем, достаточно ли денег
        balance = c.execute('SELECT balance FROM accounts WHERE id = ?',
                           (from_id,)).fetchone()[0]
        if balance < 0:
            raise ValueError('Insufficient funds')
        
        # Шаг 2: Добавляем
        c.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?',
                  (amount, to_id))
        
        c.execute('COMMIT')  # Подтверждаем ВСЕ изменения
        print('Transfer successful')
        
    except Exception as e:
        c.execute('ROLLBACK')  # Откатываем ВСЕ изменения
        print(f'Transfer failed: {e}')

transfer_money_good(1, 2, 100)
# Результат: ЛИБО обе операции выполнены, ЛИБО ни одна

Основные свойства транзакций: ACID

A — Atomicity (Атомарность)

Транзакция либо выполняется полностью, либо не выполняется совсем.
Нет "полуготовых" состояний.

Примеры:
✓ Transfer 100 AND add 100 both succeed
✗ Transfer 100 but fail to add 100 — ROLLBACK both

C — Consistency (Консистентность)

БД переходит из одного корректного состояния в другое.
Все правила и ограничения соблюдаются.

Примеры:
✓ account.balance >= 0  (никогда не отрицательное)
✓ total(all_accounts) не меняется при передаче денег

I — Isolation (Изоляция)

Параллельные транзакции не мешают друг другу.

Проблема без изоляции:

Транзакция 1: SELECT balance FROM account WHERE id=1  // 1000
Транзакция 2: UPDATE account SET balance=500 WHERE id=1  // Меняет
Транзакция 2: COMMIT
Транзакция 1: Видит 500 вместо 1000 (dirty read!)

D — Durability (Надежность)

Одна коммитленные данные сохраняются навсегда.
Даже если БД упадет, данные не потеряются.

Примеры:
Коммитили транзакцию → данные на диске
Упала БД → восстанавливает из диска

Уровни изоляции в PostgreSQL

1. Read Uncommitted (самый небезопасный)

# Видим "грязные" данные (не коммиченные еще)

Транзакция 1:
  BEGIN;
  UPDATE account SET balance = 100 WHERE id = 1;
  -- Еще не COMMIT

Транзакция 2:
  BEGIN;
  SELECT balance FROM account WHERE id = 1;  -- Видит 100 (dirty!)
  COMMIT;

Транзакция 1:
  ROLLBACK;  -- Откатываем! Трансакция 2 работала с фантомом

2. Read Committed (по умолчанию в большинстве БД)

# Видим только коммиченные данные

Транзакция 1:
  BEGIN;
  UPDATE account SET balance = 100 WHERE id = 1;
  -- Еще не COMMIT

Транзакция 2:
  BEGIN;
  SELECT balance FROM account WHERE id = 1;  -- Видит старое значение
  COMMIT;

Транзакция 1:
  COMMIT;  # Теперь видно новое значение

3. Repeatable Read

Внутри транзакции всегда видим одну версию данных.
Даже если другая транзакция их изменила и коммитила.

4. Serializable (самый безопасный, медленный)

Транзакции выполняются так, как будто идут одна за другой.
Без параллельных конфликтов.

Примеры TCL в Python

С использованием контекстного менеджера

import psycopg2

conn = psycopg2.connect('dbname=mydb user=postgres')

try:
    with conn:
        cur = conn.cursor()
        # Транзакция автоматически начита
        cur.execute('UPDATE accounts SET balance = balance - 100 WHERE id = 1')
        cur.execute('UPDATE accounts SET balance = balance + 100 WHERE id = 2')
        # Если здесь ошибка — автоматический ROLLBACK
        # Если успех — автоматический COMMIT
except Exception as e:
    print(f'Error: {e}')
finally:
    conn.close()

С явными COMMIT/ROLLBACK

import psycopg2

conn = psycopg2.connect('dbname=mydb user=postgres')
conn.autocommit = False  # Явное управление транзакциями
cur = conn.cursor()

try:
    cur.execute('BEGIN')
    
    # Операция 1
    cur.execute('UPDATE users SET status = ? WHERE id = ?',
               ('active', 1))
    
    # Проверка
    cur.execute('SELECT * FROM users WHERE id = 1')
    result = cur.fetchone()
    if not result:
        raise ValueError('User not found!')
    
    # Операция 2
    cur.execute('INSERT INTO audit_log VALUES (?, ?)',
               (1, 'status_changed'))
    
    conn.commit()  # Все успешно
    print('Success')
    
except Exception as e:
    conn.rollback()  # Откатываем все
    print(f'Failed: {e}')
finally:
    cur.close()
    conn.close()

Savepoints для частичных откатов

import psycopg2

conn = psycopg2.connect('dbname=mydb')
cur = conn.cursor()

try:
    cur.execute('BEGIN')
    
    # Операция 1
    cur.execute('INSERT INTO users VALUES (1, "Alice")')
    
    # Сохраняем точку
    cur.execute('SAVEPOINT sp1')
    
    # Операция 2 (может пойти не по плану)
    try:
        cur.execute('INSERT INTO users VALUES (1, "Bob")')  # Дублicate!
    except:
        # Откатываем только до SAVEPOINT, не всю транзакцию
        cur.execute('ROLLBACK TO SAVEPOINT sp1')
    
    # Операция 3
    cur.execute('INSERT INTO users VALUES (2, "Charlie")')
    
    conn.commit()
    # Результат: Alice и Charlie сохранены, Bob откачен
    
except Exception as e:
    conn.rollback()
    print(f'Error: {e}')
finally:
    cur.close()
    conn.close()

Когда особенно важны транзакции

1. Финансовые системы

# Переводы денег ДОЛЖНЫ быть в транзакции
# Иначе деньги могут потеряться или продублироваться

2. Заказы в e-commerce

# Все операции должны выполниться вместе:
# 1. Снять со счета
# 2. Зарезервировать товар
# 3. Создать заказ
# 4. Отправить уведомление (если нужно)

3. Синхронизация связанных таблиц

# Если обновляем users и заодно обновляем users_metadata,
# нужно, чтобы оба обновления либо прошли, либо откатились

Общее правило

Любая логически связанная группа операций должна быть в одной транзакции.

Если одна операция важна для другой, они должны успешны вместе или откатиться вместе. Иначе БД окажется в несогласованном состоянии.

Зачем нужен TCL в БД? | PrepBro