SQL Теория — Часть 1



SQL Теория 1.1

Продолжение: часть 1.1
ТЕМА 5: ТИПЫ ДАННЫХ
Аналогия: Типы данных — это как контейнеры разного размера и формы. Не храните молоко в дуршлаге (строку в числовом поле), не наливайте литр в стакан (большое число в SMALLINT). Выбирайте контейнер под содержимое.

23. Какие числовые типы есть в PostgreSQL?

Числовые типы определяют диапазон значений, точность и размер в памяти.
📊 Сравнительная таблица

Тип

Размер

Диапазон

Применение

Пример

SMALLINT

2 байта

-32,768 до 32,767

Малые счётчики, статусы

Возраст, рейтинг 1-5

INTEGER (INT)

4 байта

-2,147,483,648 до 2,147,483,647

Стандартный выбор

Количество, ID небольших таблиц

BIGINT

8 байт

-9,223,372,036,854,775,808 до 9,223,372,036,854,775,807

Большие ID, миллисекунды

User ID, timestamps

NUMERIC(p,s)

переменный

до 131072 цифр до и 16383 после запятой

Точные вычисления

Деньги, проценты

REAL

4 байта

6 десятичных знаков точности

Приблизительные научные

Координаты GPS

DOUBLE PRECISION

8 байт

15 десятичных знаков точности

Научные расчёты

Физические константы

SERIAL

4 байта

Автоинкремент INT

Автоинкремент ID

id SERIAL PRIMARY KEY

BIGSERIAL

8 байт

Автоинкремент BIGINT

Автоинкремент для больших таблиц

id BIGSERIAL PRIMARY KEY

🔥 Производственная катастрофа: переполнение INT
Октябрь 2023, социальная сеть:
2018 - Запуск проекта, выбран INT для user_id:
       CREATE TABLE users (id SERIAL PRIMARY KEY);  -- INT, макс 2.1 млрд

2020 - 100,000 пользователей ✅
2021 - 1,000,000 пользователей ✅
2022 - 10,000,000 пользователей ✅
2023 - 2,000,000,000 пользователей ⚠️ близко к лимиту!

Октябрь 2023 - user_id достиг 2,147,483,647 (максимум INT):
INSERT INTO users (name) VALUES ('New User');
-- ERROR: integer out of range ❌

Паника:
• Регистрация сломана
• Нельзя создать новых пользователей
• Миграция INT → BIGINT займёт 6 часов с даунтаймом
• Потеря $500,000 выручки за 6 часов
Причина: Использовали INT (2 млрд) вместо BIGINT (9 квинтиллионов).

Решение (болезненное):
-- Миграция занимает 6 часов на 2 млрд записей:
BEGIN;
    -- 1. Создать новую колонку BIGINT:
    ALTER TABLE users ADD COLUMN id_new BIGINT;

    -- 2. Скопировать данные (долго!):
    UPDATE users SET id_new = id;  -- 6 часов!

    -- 3. Удалить старую колонку, переименовать:
    ALTER TABLE users DROP COLUMN id;
    ALTER TABLE users RENAME COLUMN id_new TO id;

    -- 4. Обновить все FOREIGN KEY (ещё 2 часа!):
    ALTER TABLE orders ALTER COLUMN user_id TYPE BIGINT;
    ALTER TABLE comments ALTER COLUMN user_id TYPE BIGINT;
    -- ... ещё 50 таблиц
COMMIT;

-- Итого: 8 часов даунтайма, миллионные потери
Правильное решение с самого начала:
-- ✅ Используй BIGINT для ID с самого начала:
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,  -- макс 9,223,372,036,854,775,807
    name VARCHAR(100) NOT NULL
);

-- Хватит на:
-- • 9 квинтиллионов пользователей
-- • 293 миллиарда лет при 1 регистрации/секунду
-- • Больше, чем атомов в известной вселенной
💻 Java примеры с типами
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // BIGSERIAL → Java Long (не Integer!)

    private String name;
    private Short age;  // SMALLINT → Java Short (для экономии памяти)
    private BigDecimal balance;  // NUMERIC → Java BigDecimal (для денег!)
}

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // BIGSERIAL

    private String name;

    @Column(precision = 10, scale = 2)  // NUMERIC(10,2)
    private BigDecimal price;  // ❗ BigDecimal, НЕ Double для денег!

    private Integer stockQuantity;  // INTEGER (до 2 млрд штук товара)
}
Service с типами:
@Service
public class ProductService {
    public void updatePrice(Long productId, BigDecimal newPrice) {
        // BigDecimal для точных вычислений:
        BigDecimal discount = new BigDecimal("0.15");  // 15%
        BigDecimal discountedPrice = newPrice.multiply(BigDecimal.ONE.subtract(discount));

        jdbcTemplate.update(
            "UPDATE products SET price = ? WHERE id = ?",
            discountedPrice.setScale(2, RoundingMode.HALF_UP),  // округлить до 2 знаков
            productId
        );
    }

    public void sellProduct(Long productId, Integer quantity) {
        jdbcTemplate.update(
            "UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?",
            quantity,  // Integer для количества (до 2 млрд)
            productId
        );
    }
}
🎯 Когда что использовать?
SMALLINT (2 байта, -32 768 до 32 767):
CREATE TABLE users (
    age SMALLINT CHECK (age >= 0 AND age <= 120),  -- возраст
    rating SMALLINT CHECK (rating BETWEEN 1 AND 5)  -- рейтинг 1-5 звёзд
);
-- Экономия: 2 байта вместо 4 (INTEGER) на каждую строку
INTEGER (4 байта, ±2.1 млрд):
CREATE TABLE products (
    stock_quantity INTEGER CHECK (stock_quantity >= 0),  -- количество товара
    views_count INTEGER DEFAULT 0  -- счётчик просмотров
);
-- Стандартный выбор для счётчиков и количеств
BIGINT (8 байт, ±9 квинтиллионов):
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,  -- ❗ ВСЕГДА для ID
    created_at_ms BIGINT  -- Unix timestamp в миллисекундах
);

CREATE TABLE events (
    user_id BIGINT REFERENCES users(id),  -- FOREIGN KEY тоже BIGINT
    timestamp_ns BIGINT  -- наносекунды с 1970 года
);
-- Правило: ID = BIGINT, точка.
NUMERIC (точная арифметика):
CREATE TABLE invoices (
    amount NUMERIC(10, 2),  -- до 99,999,999.99 (10 цифр, 2 после запятой)
    tax_rate NUMERIC(5, 4),  -- 0.1850 = 18.50%
    total NUMERIC(12, 2)
);
-- ❗ ВСЕГДА для денег и процентов
⚡ Производительность и размер
Benchmark на таблице с 10 млн записей:
-- SMALLINT (2 байта):
CREATE TABLE test_small (id BIGSERIAL, value SMALLINT);
INSERT INTO test_small SELECT generate_series(1, 10000000), 100;
-- Размер: 342 MB
-- SELECT AVG(value): 234 ms

-- INTEGER (4 байта):
CREATE TABLE test_int (id BIGSERIAL, value INTEGER);
INSERT INTO test_int SELECT generate_series(1, 10000000), 100;
-- Размер: 422 MB (+23%)
-- SELECT AVG(value): 256 ms (+9%)

-- BIGINT (8 байт):
CREATE TABLE test_bigint (id BIGSERIAL, value BIGINT);
INSERT INTO test_bigint SELECT generate_series(1, 10000000), 100;
-- Размер: 574 MB (+68%)
-- SELECT AVG(value): 289 ms (+23%)

-- NUMERIC (переменный размер):
CREATE TABLE test_numeric (id BIGSERIAL, value NUMERIC(10,2));
INSERT INTO test_numeric SELECT generate_series(1, 10000000), 100.50;
-- Размер: 498 MB (+45%)
-- SELECT AVG(value): 1247 ms (+433%!) ← NUMERIC медленнее для вычислений
Вывод:

  • SMALLINT экономит место, но разница небольшая (~20%)
  • NUMERIC медленный для агрегаций (4−5x), но необходим для денег
  • Для ID ВСЕГДА используй BIGINT (безопасность важнее 4 байт)
🚨 Частые ошибки
1️⃣ FLOAT/DOUBLE для денег (КАТАСТРОФА!):
-- ❌ НЕПРАВИЛЬНО:
CREATE TABLE orders (
    total DOUBLE PRECISION  -- ❌❌❌ НИКОГДА для денег!
);

INSERT INTO orders (total) VALUES (0.1 + 0.2);  -- ожидаем 0.3
SELECT total FROM orders;  -- получаем 0.30000000000000004 ❌

-- Проблема: накопление ошибок округления:
SELECT SUM(total) FROM orders;  -- может отличаться на центы/рубли!

-- ✅ ПРАВИЛЬНО:
CREATE TABLE orders (
    total NUMERIC(10, 2)  -- точная арифметика
);
2️⃣ INTEGER для ID (переполнение):
-- ❌ ОПАСНО:
CREATE TABLE users (id SERIAL PRIMARY KEY);  -- INT, лимит 2.1 млрд

-- ✅ БЕЗОПАСНО:
CREATE TABLE users (id BIGSERIAL PRIMARY KEY);  -- BIGINT, 9 квинтиллионов
3️⃣ BIGINT где достаточно SMALLINT (расточительство):
-- ⚠️ Неоптимально:
CREATE TABLE users (
    age BIGINT  -- 8 байт для числа 0-120?!
);
-- На 10 млн пользователей = 80 MB вместо 20 MB (SMALLINT)

-- ✅ Оптимально:
CREATE TABLE users (
    age SMALLINT CHECK (age >= 0 AND age <= 120)  -- 2 байта
);
✅ Чеклист выбора типа
Для чего нужно число?

┌──────────────────────────────────────────────────────────┐
│ PRIMARY KEY или FOREIGN KEY?                             │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          BIGSERIAL (для PK) / BIGINT (для FK)
          Не экономь 4 байта — переполнение INT обойдётся дороже!

┌──────────────────────────────────────────────────────────┐
│ Деньги, цены, проценты?                                  │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          NUMERIC(10, 2) для денег
          NUMERIC(5, 4) для процентов
          НИКОГДА FLOAT/DOUBLE!

┌──────────────────────────────────────────────────────────┐
│ Малые числа (возраст, рейтинг, счётчик до 30k)?          │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          SMALLINT (экономия памяти)

┌──────────────────────────────────────────────────────────┐
│ Счётчики, количества, обычные числа?                     │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          INTEGER (стандартный выбор)

┌──────────────────────────────────────────────────────────┐
│ Научные вычисления, координаты, приблизительные данные?  │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          DOUBLE PRECISION (быстрая арифметика)
Золотые правила:

💰 Деньги = NUMERIC, всегда и везде
🔑 ID = BIGSERIAL/BIGINT, без исключений
📊 Счётчики = INTEGER, стандартный выбор
🎯 Малые числа = SMALLINT, для экономии
Никогда FLOAT/DOUBLE для денег!
24. Когда использовать NUMERIC vs FLOAT?

NUMERIC и FLOAT решают разные задачи: точность vs скорость.
📊 Сравнительная таблица

Характеристика

NUMERIC(p,s)

FLOAT/DOUBLE PRECISION

Точность

Абсолютная (десятичная арифметика)

Приблизительная (ошибки округления)

Хранение

Десятичное (как люди пишут)

Двоичное (IEEE 754)

Скорость

Медленнее (4-5x)

Быстрее (аппаратная поддержка)

Размер

Переменный (2-16 байт)

4 байта (REAL) / 8 байт (DOUBLE)

Применение

Деньги, финансы, бизнес

Научные расчёты, координаты

Примеры

Цены, балансы, проценты

Температура, GPS, физика

🔥 Производственная катастрофа с FLOAT
Декабрь 2023, финтех-стартап:
День 1 - Разработчик создал таблицу платежей:
         CREATE TABLE payments (amount DOUBLE PRECISION);  -- ❌ FLOAT для денег!

Месяц 1 - 10,000 транзакций:
          SELECT SUM(amount) FROM payments;
          -- Ожидаем: 1,000,000.00 руб.
          -- Получаем: 999,999.9999999876 руб.
          -- Разница: 0.0000000124 руб. (игнорируют)

Месяц 6 - 500,000 транзакций:
          SELECT SUM(amount) FROM payments;
          -- Ожидаем: 50,000,000.00 руб.
          -- Получаем: 49,999,987.34 руб.
          -- Разница: -12.66 руб. ⚠️

Год 1 - 5,000,000 транзакций:
        SELECT SUM(amount) FROM payments;
        -- Ожидаем: 500,000,000.00 руб.
        -- Получаем: 499,997,856.12 руб.
        -- Разница: -2,143.88 руб.! ❌

Проблема обнаружена:
• Бухгалтерия: "В балансе недостача 2,143.88 руб.!"
• ЦБ РФ: "Штраф за неточность финансовой отчётности: 100,000 руб."
• Клиенты: "С моего счёта списалось 100.03 вместо 100.00!"
Причина: FLOAT хранит числа в двоичном формате → неизбежные ошибки округления.
💣 Демонстрация проблемы FLOAT
-- Проблема 1: 0.1 + 0.2 ≠ 0.3
SELECT 0.1::float + 0.2::float = 0.3::float;
-- FALSE ❌

SELECT 0.1::float + 0.2::float;
-- 0.30000000000000004 (не 0.3!)

-- NUMERIC точен:
SELECT 0.1::numeric + 0.2::numeric = 0.3::numeric;
-- TRUE ✅

SELECT 0.1::numeric + 0.2::numeric;
-- 0.3 (ровно!)
Проблема 2: накопление ошибок:
-- FLOAT: ошибки накапливаются
WITH RECURSIVE float_sum AS (
    SELECT 1 AS n, 0.1::float AS sum
    UNION ALL
    SELECT n + 1, sum + 0.1::float
    FROM float_sum
    WHERE n < 10
)
SELECT n, sum, (n * 0.1) AS expected, sum - (n * 0.1) AS error
FROM float_sum;

-- Результат:
--  n  │  sum (FLOAT)         │ expected │ error
-- ────┼──────────────────────┼──────────┼─────────────────
--  1  │ 0.1                  │ 0.1      │ 0.0
--  2  │ 0.2                  │ 0.2      │ 0.0
--  3  │ 0.30000000000000004  │ 0.3      │ 4e-18  ← начало ошибки
--  5  │ 0.5000000000000001   │ 0.5      │ 1e-16
-- 10  │ 1.0000000000000007   │ 1.0      │ 7e-16  ← ошибка растёт!

-- NUMERIC: точность сохраняется
WITH RECURSIVE numeric_sum AS (
    SELECT 1 AS n, 0.1::numeric AS sum
    UNION ALL
    SELECT n + 1, sum + 0.1::numeric
    FROM numeric_sum
    WHERE n < 10
)
SELECT n, sum, (n * 0.1) AS expected
FROM numeric_sum;

-- Результат:
--  n  │ sum (NUMERIC) │ expected
-- ────┼───────────────┼──────────
--  1  │ 0.1           │ 0.1
--  2  │ 0.2           │ 0.2
--  3  │ 0.3           │ 0.3      ← точно!
--  5  │ 0.5           │ 0.5
-- 10  │ 1.0           │ 1.0      ← идеально!
Проблема 3: деньги с FLOAT:
-- ❌ FLOAT для денег (катастрофа):
CREATE TABLE accounts_wrong (
    id SERIAL PRIMARY KEY,
    balance DOUBLE PRECISION
);

INSERT INTO accounts_wrong (balance) VALUES (100.00);
UPDATE accounts_wrong SET balance = balance - 0.10 WHERE id = 1;  -- снять 10 копеек
-- Повторить 1000 раз...

SELECT balance FROM accounts_wrong WHERE id = 1;
-- Ожидаем: 0.00
-- Получаем: -0.0000000000000142 ❌ (отрицательный баланс из-за ошибок!)

-- ✅ NUMERIC для денег (правильно):
CREATE TABLE accounts_correct (
    id SERIAL PRIMARY KEY,
    balance NUMERIC(10, 2)
);

INSERT INTO accounts_correct (balance) VALUES (100.00);
UPDATE accounts_correct SET balance = balance - 0.10 WHERE id = 1;
-- Повторить 1000 раз...

SELECT balance FROM accounts_correct WHERE id = 1;
-- 0.00 ✅ (идеально!)
💻 Java примеры: BigDecimal vs Double
Сценарий: расчёт стоимости заказа
@Service
public class OrderCalculationService {

    // ❌ НЕПРАВИЛЬНО: Double для денег
    public Double calculateTotalWrong(List<OrderItem> items) {
        Double total = 0.0;
        for (OrderItem item : items) {
            total += item.getPrice() * item.getQuantity();  // накопление ошибок!
        }
        return total;
    }

    // ✅ ПРАВИЛЬНО: BigDecimal для денег
    public BigDecimal calculateTotalCorrect(List<OrderItem> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (OrderItem item : items) {
            BigDecimal itemTotal = item.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()));
            total = total.add(itemTotal);
        }
        return total.setScale(2, RoundingMode.HALF_UP);  // округлить до копеек
    }

    // Пример использования:
    public void processOrder(Long orderId) {
        List<OrderItem> items = getOrderItems(orderId);

        // Расчёт с BigDecimal:
        BigDecimal total = calculateTotalCorrect(items);

        // Применить скидку 15%:
        BigDecimal discount = new BigDecimal("0.15");
        BigDecimal discountAmount = total.multiply(discount)
            .setScale(2, RoundingMode.HALF_UP);
        BigDecimal finalTotal = total.subtract(discountAmount);

        // Сохранить в БД (NUMERIC):
        jdbcTemplate.update(
            "UPDATE orders SET total = ?, discount = ? WHERE id = ?",
            finalTotal, discountAmount, orderId
        );
    }
}
Демонстрация проблемы в Java:
public class FloatProblemDemo {
    public static void main(String[] args) {
        // ❌ Double (FLOAT):
        double sumDouble = 0.0;
        for (int i = 0; i < 10; i++) {
            sumDouble += 0.1;
        }
        System.out.println("Double: " + sumDouble);  // 0.9999999999999999 ❌
        System.out.println("Equals 1.0? " + (sumDouble == 1.0));  // false ❌

        // ✅ BigDecimal (NUMERIC):
        BigDecimal sumDecimal = BigDecimal.ZERO;
        for (int i = 0; i < 10; i++) {
            sumDecimal = sumDecimal.add(new BigDecimal("0.1"));
        }
        System.out.println("BigDecimal: " + sumDecimal);  // 1.0 ✅
        System.out.println("Equals 1.0? " + sumDecimal.compareTo(BigDecimal.ONE) == 0);  // true ✅
    }
}
⚡ Производительность: NUMERIC vs FLOAT
Benchmark: SUM () на 10 млн записей
-- Тест 1: FLOAT
CREATE TABLE test_float (id BIGSERIAL, amount REAL);
INSERT INTO test_float SELECT generate_series(1, 10000000), random() * 1000;
EXPLAIN ANALYZE SELECT SUM(amount) FROM test_float;
-- Execution Time: 387 ms

-- Тест 2: DOUBLE PRECISION
CREATE TABLE test_double (id BIGSERIAL, amount DOUBLE PRECISION);
INSERT INTO test_double SELECT generate_series(1, 10000000), random() * 1000;
EXPLAIN ANALYZE SELECT SUM(amount) FROM test_double;
-- Execution Time: 412 ms

-- Тест 3: NUMERIC
CREATE TABLE test_numeric (id BIGSERIAL, amount NUMERIC(10,2));
INSERT INTO test_numeric SELECT generate_series(1, 10000000), (random() * 1000)::numeric(10,2);
EXPLAIN ANALYZE SELECT SUM(amount) FROM test_numeric;
-- Execution Time: 1847 ms  ← в 4.5x медленнее!
Вывод:
  • FLOAT быстрее в 4−5 раз
  • Но для денег скорость не важна — важна точность!
  • NUMERIC медленнее, но правильно работает с деньгами
🎯 Когда что использовать?
✅ NUMERIC — для точных вычислений:
CREATE TABLE financial_data (
    -- Деньги (ВСЕГДА NUMERIC):
    price NUMERIC(10, 2),           -- цена до 99,999,999.99
    balance NUMERIC(15, 2),         -- баланс счёта
    tax_amount NUMERIC(12, 2),      -- сумма налога

    -- Проценты (NUMERIC для точности):
    interest_rate NUMERIC(5, 4),    -- 0.0350 = 3.50%
    tax_rate NUMERIC(4, 2),         -- 18.50%
    discount_percent NUMERIC(5, 2), -- до 100.00%

    -- Точные измерения:
    weight_kg NUMERIC(8, 3),        -- вес до 99999.999 кг
    length_m NUMERIC(10, 4)         -- длина до 999999.9999 м
);
✅ FLOAT/DOUBLE — для приблизительных вычислений:
CREATE TABLE scientific_data (
    -- Координаты (GPS):
    latitude DOUBLE PRECISION,      -- 55.7558° N (не критично для копеек)
    longitude DOUBLE PRECISION,     -- 37.6173° E

    -- Научные измерения:
    temperature REAL,               -- температура °C
    pressure DOUBLE PRECISION,      -- давление Па
    velocity DOUBLE PRECISION,      -- скорость м/с

    -- Статистика (не финансы):
    avg_rating REAL,                -- средний рейтинг 4.7853
    score DOUBLE PRECISION          -- очки в игре
);
🚨 Частые ошибки
1️⃣ FLOAT для денег (КАТАСТРОФА #1):
-- ❌ НИКОГДА:
CREATE TABLE orders (total FLOAT);  -- ❌❌❌

-- ✅ ВСЕГДА:
CREATE TABLE orders (total NUMERIC(10, 2));  -- ✅✅✅
2️⃣ Сравнение FLOAT через = (не работает!):
-- ❌ НЕПРАВИЛЬНО:
SELECT * FROM products WHERE price::float = 19.99;  -- может не найти!
-- (19.99 может быть 19.990000000000002)

-- ✅ ПРАВИЛЬНО: используй диапазон
SELECT * FROM products
WHERE price::float BETWEEN 19.98 AND 20.00;

-- Или лучше: не используй FLOAT для денег!
SELECT * FROM products WHERE price::numeric = 19.99;  -- точно
3️⃣ Преобразование Double → BigDecimal через конструктор:
// ❌ НЕПРАВИЛЬНО:
BigDecimal wrong = new BigDecimal(0.1);  // создаёт 0.1000000000000000055511151231257827021181583404541015625
System.out.println(wrong);  // WTF?!

// ✅ ПРАВИЛЬНО:
BigDecimal correct = new BigDecimal("0.1");  // создаёт ровно 0.1
BigDecimal also_correct = BigDecimal.valueOf(0.1);  // тоже правильно
✅ Чеклист: NUMERIC или FLOAT?
Задай себе вопрос: "Это деньги?"

┌──────────────────────────────────────────────────────────┐
│ Деньги, цены, балансы, финансовые расчёты?               │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          NUMERIC(10, 2) — ВСЕГДА!
          Java: BigDecimal
          Никаких FLOAT/Double для денег!

┌──────────────────────────────────────────────────────────┐
│ Проценты, коэффициенты (нужна точность)?                 │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          NUMERIC(5, 4) — для точности
          18.5000% (не 18.500000000002%)

┌──────────────────────────────────────────────────────────┐
│ Координаты GPS, температура, физические величины?        │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          DOUBLE PRECISION — скорость важнее точности
          Ошибка в 0.0001° = 10 см (приемлемо)

┌──────────────────────────────────────────────────────────┐
│ Статистика, средние, не критичные метрики?               │
└──────────────────┬───────────────────────────────────────┘
                   ↓ ДА
          REAL или DOUBLE PRECISION
          Средний рейтинг 4.7 или 4.700001 — неважно
Золотые правила:

💰 Деньги = NUMERIC, без вариантов
📊 Проценты = NUMERIC, для точности
🌍 Координаты, физика = DOUBLE, скорость > точность
НИКОГДА FLOAT/DOUBLE для денег!
Java: BigDecimal для денег, не Double!

25. Какие строковые типы есть?

Тип

Описание

CHAR (n)

Фиксированная длина, дополняется пробелами

VARCHAR (n)

Переменная длина до n символов

TEXT

Без ограничения длины


В PostgreSQL TEXT = VARCHAR без лимита, нет разницы в производительности.

Совет: Используйте TEXT или VARCHAR без указания длины. Лимит длины — это бизнес-правило, лучше делать его через CHECK.
26. Что такое Collation?

Collation — правила сортировки и сравнения строк (учитывает язык и локаль):
-- Создание колонки с collation:
CREATE TABLE users (
   name VARCHAR(100) COLLATE "ru_RU.UTF-8"
);

-- Сортировка с явным collation:
SELECT * FROM users ORDER BY name COLLATE "ru_RU.UTF-8";

Collation

Применение

"C"

Побайтовое сравнение (быстро)

"ru_RU.UTF-8"

Русский язык

"en_US.UTF-8"

Английский язык


Зачем: Чтобы "ё" сортировалась правильно, а не в конце алфавита.
27. Какие типы дат и времени есть?

Тип

Содержит

Пример

DATE

Только дата

'2024-01-15'

TIME

Только время

'14:30:00'

TIMESTAMP

Дата + время

'2024-01-15 14:30:00'

TIMESTAMPTZ

С timezone

'2024-01-15 14:30:00+03'

INTERVAL

Промежуток

'2 days 3 hours'


Рекомендация: Всегда используйте TIMESTAMPTZ для хранения времени событий. Это спасёт вас от боли с часовыми поясами.
28. Какие ещё типы данных существуют?
-- BOOLEAN
CREATE TABLE users (is_active BOOLEAN DEFAULT TRUE);
SELECT * FROM users WHERE is_active;  -- без = TRUE

-- SERIAL (автоинкремент)
id SERIAL PRIMARY KEY        -- 1, 2, 3, ...
id BIGSERIAL PRIMARY KEY     -- для больших таблиц

💡 **Интересный факт:** SERIAL — это 32-битное число, максимум **2,147,483,647**. Звучит много, но Instagram исчерпал лимит INT для лайков за несколько лет! Для больших таблиц используй BIGSERIAL (до 9 квинтиллионов).

-- UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
id UUID DEFAULT uuid_generate_v4()

-- JSONB
data JSONB NOT NULL

-- Массивы
tags TEXT[]
Когда UUID vs SERIAL:

  • SERIAL — простой, читаемый, для внутреннего использования
  • UUID — для распределённых систем, публичных API (нельзя угадать следующий ID)
ТЕМА 6: SELECT И ФИЛЬТРАЦИЯ
Аналогия: SELECT — это как поиск в интернет-магазине. Вы говорите: «Покажи мне все ноутбуки (FROM), дешевле 50 000 (WHERE), отсортируй по рейтингу (ORDER BY), покажи первые 10 (LIMIT)». База выполняет ваш запрос и возвращает результат.

Интересный факт: Порядок выполнения SQL отличается от порядка написания! Мы пишем SELECT … FROM … WHERE …, но база выполняет: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Поэтому в WHERE нельзя использовать алиасы из SELECT — они ещё не существуют!
29. Как выбрать данные из таблицы?
SELECT * FROM users;                    -- все колонки
SELECT name, email FROM users;          -- конкретные колонки
SELECT name AS user_name FROM users;    -- алиас колонки
SELECT u.name FROM users u;             -- алиас таблицы
Совет: Избегайте SELECT * в production-коде. Указывайте конкретные колонки — это быстрее и понятнее.
30. В каком порядке выполняется SQL-запрос?
FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET
Это объясняет:

  • В WHERE нельзя использовать алиасы из SELECT (SELECT выполняется позже)
  • В HAVING можно использовать агрегаты (GROUP BY уже выполнен)
  • ORDER BY может использовать алиасы (выполняется после SELECT)
Мнемоника: «From John Where Goods Having Some Decent Oranges Left»

31. Почему нельзя использовать алиас из SELECT в WHERE?

WHERE выполняется ДО SELECT в логическом порядке. На момент выполнения WHERE алиас ещё не существует.
-- НЕ работает:
SELECT salary * 12 AS annual
FROM employees
WHERE annual > 100000;
-- ERROR: column "annual" does not exist

-- Обходные пути:

-- 1. Повторить выражение
WHERE salary * 12 > 100000

-- 2. Подзапрос
SELECT * FROM (
   SELECT salary * 12 AS annual FROM employees
) sub WHERE annual > 100000;

-- 3. CTE
WITH salaries AS (
   SELECT salary * 12 AS annual FROM employees
)
SELECT * FROM salaries WHERE annual > 100000;
32. Как фильтровать данные в WHERE?
-- Сравнения
WHERE age > 25
WHERE status = 'active'
WHERE created_at >= '2024-01-01'

-- Логические операторы
WHERE age > 25 AND status = 'active'
WHERE age > 25 OR city = 'Москва'
WHERE NOT is_deleted

-- BETWEEN (включает обе границы)
WHERE price BETWEEN 10 AND 100
-- Эквивалент: price >= 10 AND price <= 100

-- IN
WHERE status IN ('active', 'pending', 'processing')

-- LIKE / ILIKE
WHERE name LIKE 'John%'         -- начинается с John (регистрозависимо)
WHERE email LIKE '%@gmail.com'  -- заканчивается на @gmail.com
WHERE name ILIKE 'john%'        -- регистронезависимо (PostgreSQL)
-- % = любое количество символов
-- _ = ровно один символ
ТЕМА 7: NULL И ТРЁХЗНАЧНАЯ ЛОГИКА
Аналогия: NULL — это не «пусто» и не «ноль». Это «неизвестно». Представьте анкету: если человек не указал возраст, это не значит, что ему 0 лет. Это значит, что мы не знаем его возраст. И любые вычисления с «неизвестным» дают «неизвестно».
33. Что такое NULL?

NULL — отсутствие значения, «неизвестно». Не равен нулю, пустой строке или false.

Важно понять: NULL — это не значение, а маркер отсутствия значения.

🧠 Интересный факт: NULL — источник бесконечных багов! Даже NULL = NULL возвращает… NULL (не TRUE!). Это называется трёхзначная логика (TRUE, FALSE, UNKNOWN). Именно поэтому нужен IS NULL, а не = NULL. Создатель реляционной модели Эдгар Кодд сам критиковал эту концепцию и предлагал заменить NULL на два значения: «применимо, но неизвестно» и «неприменимо».
34. Как проверить на NULL?
WHERE column IS NULL
WHERE column IS NOT NULL

-- НЕ работает:
WHERE column = NULL  -- всегда вернёт NULL (не TRUE)
Почему = NULL не работает: Вопрос «равен ли X неизвестному?» сам по себе имеет неизвестный ответ.
35. Как работает NULL в сравнениях?

Любое сравнение с NULL возвращает NULL:
SELECT NULL = NULL;   -- NULL (не TRUE!)
SELECT NULL <> 5;     -- NULL
SELECT 5 > NULL;      -- NULL
Аналогия: «Выше ли Вася, чем человек, рост которого неизвестен?» — ответ: «Неизвестно».
36. Что такое трёхзначная логика?

В SQL три значения: TRUE, FALSE, NULL

Выражение

Результат

TRUE AND NULL

NULL

FALSE AND NULL

FALSE

TRUE OR NULL

TRUE

FALSE OR NULL

NULL

NOT NULL

NULL


Как запомнить:
  • FALSE AND что_угодно = FALSE (достаточно одного FALSE)
  • TRUE OR что_угодно = TRUE (достаточно одного TRUE)
  • В остальных случаях — NULL
37. Какие функции для работы с NULL?
-- COALESCE: первое не-NULL значение
SELECT COALESCE(phone, mobile, 'Нет контакта') FROM users;

-- NULLIF: NULL если значения равны (защита от деления на 0)
SELECT amount / NULLIF(count, 0) FROM stats;

-- CASE с NULL
SELECT CASE WHEN value IS NULL THEN 'пусто' ELSE value END FROM data;
Совет: COALESCE — ваш лучший друг при работе с NULL.
38. Почему NOT IN опасен с NULL?
-- Если в списке есть NULL:
WHERE id NOT IN (1, 2, NULL)
-- Превращается в: id <> 1 AND id <> 2 AND id <> NULL
-- id <> NULL = NULL, поэтому всё выражение = NULL
-- Результат: 0 строк!

-- Решение: использовать NOT EXISTS или фильтровать NULL
WHERE id NOT IN (SELECT x FROM t WHERE x IS NOT NULL)
Золотое правило: Всегда используйте NOT EXISTS вместо NOT IN для подзапросов. Это безопаснее и часто быстрее.
ТЕМА 8: СОРТИРОВКА И ОГРАНИЧЕНИЕ
Аналогия: ORDER BY — это как сортировка товаров в магазине: по цене, по популярности, по новизне. LIMIT — это «покажи первые 10». OFFSET — «пропусти первые 20 и покажи следующие 10».
39. Как сортировать результаты?
SELECT * FROM products ORDER BY price;         -- по возрастанию (ASC)
SELECT * FROM products ORDER BY price DESC;    -- по убыванию
SELECT * FROM products ORDER BY category, price DESC;  -- несколько колонок
SELECT * FROM products ORDER BY 2;             -- по номеру колонки (не рекомендуется)
40. Куда попадают NULL при сортировке?

По умолчанию в PostgreSQL:

  • ASC: NULL в конце
  • DESC: NULL в начале
ORDER BY column NULLS FIRST
ORDER BY column NULLS LAST
41. Как ограничить количество строк?
SELECT * FROM products LIMIT 10;           -- первые 10
SELECT * FROM products LIMIT 10 OFFSET 20; -- с 21 по 30

-- Стандартный SQL (PostgreSQL тоже поддерживает):
SELECT * FROM products
FETCH FIRST 10 ROWS ONLY;

SELECT * FROM products
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
Важно: OFFSET на больших таблицах медленный! Используйте keyset pagination:
-- Вместо OFFSET 1000000:
WHERE id > last_seen_id ORDER BY id LIMIT 10
42. Как убрать дубликаты?
SELECT DISTINCT city FROM users;

-- DISTINCT ON (PostgreSQL) — первая строка из каждой группы
SELECT DISTINCT ON (department) *
FROM employees
ORDER BY department, salary DESC;
-- Вернёт сотрудника с максимальной зарплатой из каждого отдела
Производительность DISTINCT:

  • DISTINCT сортирует данные для поиска дубликатов — O (N log N)
  • Индекс на колонке ускоряет DISTINCT! Данные уже отсортированы
  • GROUP BY часто быстрее DISTINCT, т.к. может использовать Hash Aggregate
-- Медленно на больших таблицах без индекса:
SELECT DISTINCT city FROM users;

-- Ускоряем с индексом:
CREATE INDEX idx_users_city ON users(city);
-- Теперь PostgreSQL может использовать Index Only Scan

-- GROUP BY может быть быстрее:
SELECT city FROM users GROUP BY city;
-- PostgreSQL может выбрать Hash Aggregate вместо Sort
Совет для собеседования: Если спросят «как ускорить DISTINCT?» — ответ «создать индекс на колонке» или «использовать GROUP BY с Hash Aggregate».
ЗАКЛЮЧЕНИЕ
Поздравляю! Вы освоили фундамент SQL:

  1. СУБД и SQL — как работают реляционные базы данных
  2. DDL — как создавать и изменять структуру (CREATE, ALTER, DROP)
  3. Ключи и ограничения — как обеспечить целостность данных
  4. Типы данных — какой контейнер выбрать для данных
  5. SELECT — как выбирать и фильтровать данные
  6. NULL — почему он особенный и как с ним работать
  7. Сортировка и пагинация — как упорядочить результаты

43. Что дальше?

Переходите к Теории 2, где мы разберём:

  • Агрегатные функции (COUNT, SUM, AVG)
  • GROUP BY и HAVING
  • Все виды JOIN
  • Подзапросы и CTE
ШПАРГАЛКА
44. Создание таблицы
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'cancelled')),
    amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status) WHERE status = 'pending';
45. Типичный SELECT
SELECT
    u.name,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(o.amount), 0) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'
  AND u.status = 'active'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0
ORDER BY total_amount DESC
LIMIT 10;
46. Порядок выполнения
1. FROM      — откуда берём данные
2. JOIN      — соединяем таблицы
3. WHERE     — фильтруем строки
4. GROUP BY  — группируем
5. HAVING    — фильтруем группы
6. SELECT    — выбираем колонки
7. DISTINCT  — убираем дубликаты
8. ORDER BY  — сортируем
9. LIMIT     — ограничиваем
Практическое задание
Для закрепления материала выполните практическое задание в Практика 1 (Основы, DDL, SELECT, NULL).md.