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

Приведи пример, когда проектировал структуру БД

1.7 Middle🔥 191 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

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

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

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

Приведи пример, когда проектировал структуру БД

Рассмотрим реальный пример проектирования БД для платформы интернет-магазина с подписками.

Контекст и требования

Создаём платформу e-commerce с поддержкой:

  • Каталога товаров
  • Заказов и платежей
  • Подписок
  • Уведомлений пользователям
  • История действий

Шаг 1: Анализ требований

Функциональность:

  • Пользователи могут создавать заказы
  • Товары могут быть в категориях
  • Поддерживаются подписки (месячная, годовая)
  • Нужна история всех платежей
  • Заказ может содержать несколько товаров
  • Нужны скидки и промокоды

Нагрузка:

  • 1M пользователей
  • 100k товаров
  • 10k заказов в день
  • Высокая частота чтения каталога

Шаг 2: Проектирование схемы

-- Таблица пользователей
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(255),
    phone VARCHAR(20),
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);

-- Таблица категорий товаров
CREATE TABLE categories (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,
    icon_url VARCHAR(500),
    parent_id UUID REFERENCES categories(id) ON DELETE CASCADE,
    display_order INT,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_categories_slug ON categories(slug);
CREATE INDEX idx_categories_parent_id ON categories(parent_id);

-- Таблица товаров (с денормализацией для производительности)
CREATE TABLE products (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    sku VARCHAR(100) UNIQUE NOT NULL,
    category_id UUID NOT NULL REFERENCES categories(id),
    price_cents BIGINT NOT NULL,  -- Используем целые числа (копейки)
    cost_cents BIGINT,
    description TEXT,
    image_url VARCHAR(500),
    stock_quantity INT NOT NULL DEFAULT 0,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_products_category_id ON products(category_id);
CREATE INDEX idx_products_sku ON products(sku);
CREATE INDEX idx_products_is_active ON products(is_active);

-- Таблица заказов
CREATE TABLE orders (
    id UUID PRIMARY KEY,
    order_number VARCHAR(50) UNIQUE NOT NULL,  -- Номер для пользователя
    user_id UUID NOT NULL REFERENCES users(id),
    total_price_cents BIGINT NOT NULL,
    discount_cents BIGINT DEFAULT 0,
    tax_cents BIGINT DEFAULT 0,
    status VARCHAR(50) NOT NULL,  -- pending, confirmed, shipped, delivered, cancelled
    payment_status VARCHAR(50) NOT NULL,  -- unpaid, paid, refunded
    delivery_address TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_order_number ON orders(order_number);

-- Таблица деталей заказа (Order Items)
CREATE TABLE order_items (
    id UUID PRIMARY KEY,
    order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES products(id),
    quantity INT NOT NULL,
    unit_price_cents BIGINT NOT NULL,  -- Цена на момент заказа
    total_price_cents BIGINT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);

-- Таблица платежей
CREATE TABLE payments (
    id UUID PRIMARY KEY,
    order_id UUID NOT NULL REFERENCES orders(id),
    payment_method VARCHAR(50) NOT NULL,  -- card, paypal, yandex_kassa
    amount_cents BIGINT NOT NULL,
    status VARCHAR(50) NOT NULL,  -- pending, success, failed, refunded
    transaction_id VARCHAR(255),  -- ID от платёжного шлюза
    error_message TEXT,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_payments_order_id ON payments(order_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);

-- Таблица подписок
CREATE TABLE subscriptions (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    plan_type VARCHAR(50) NOT NULL,  -- basic, premium, enterprise
    price_cents BIGINT NOT NULL,
    billing_period VARCHAR(50) NOT NULL,  -- monthly, yearly
    start_date TIMESTAMP WITH TIME ZONE NOT NULL,
    end_date TIMESTAMP WITH TIME ZONE NOT NULL,
    auto_renew BOOLEAN DEFAULT true,
    status VARCHAR(50) NOT NULL,  -- active, paused, cancelled
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_end_date ON subscriptions(end_date);

-- Таблица промокодов
CREATE TABLE promo_codes (
    id UUID PRIMARY KEY,
    code VARCHAR(50) UNIQUE NOT NULL,
    discount_percent DECIMAL(5, 2),
    discount_fixed_cents BIGINT,
    max_uses INT,
    current_uses INT DEFAULT 0,
    valid_from TIMESTAMP WITH TIME ZONE NOT NULL,
    valid_to TIMESTAMP WITH TIME ZONE NOT NULL,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_promo_codes_code ON promo_codes(code);
CREATE INDEX idx_promo_codes_valid_to ON promo_codes(valid_to);

-- Таблица истории (для аудита)
CREATE TABLE audit_logs (
    id UUID PRIMARY KEY,
    entity_type VARCHAR(100) NOT NULL,  -- order, payment, user
    entity_id UUID NOT NULL,
    action VARCHAR(50) NOT NULL,  -- created, updated, deleted
    old_values JSONB,  -- Предыдущие значения
    new_values JSONB,  -- Новые значения
    user_id UUID REFERENCES users(id),
    created_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);

Шаг 3: Ключевые решения и обоснование

1. UUID вместо auto-increment

// Преимущества:
// - Не раскрываем внутренние ID клиентам
// - Можно генерировать на клиенте (offline-first)
// - Легче при распределённых системах
// - Безопаснее

2. Денормализация (total_price в заказах)

-- Берём total из order_items (денормализовано)
SELECT total_price_cents FROM orders WHERE id = ?;
-- Вместо:
SELECT SUM(total_price_cents) FROM order_items WHERE order_id = ?;

3. Целые числа вместо DECIMAL для денег

// ❌ Плохо (precision issues)
DECIMAL(10, 2) price

// ✅ Хорошо (целые числа, в копейках)
BIGINT price_cents

public class Money {
    private long cents;
    
    public Money(String rubles, String kopiykas) {
        this.cents = Long.parseLong(rubles) * 100 + Long.parseLong(kopiykas);
    }
    
    public String toRubles() {
        return (cents / 100) + "." + (cents % 100);
    }
}

4. Мягкое удаление (soft delete)

-- Вместо DELETE, используем deleted_at
ALTER TABLE users ADD deleted_at TIMESTAMP WITH TIME ZONE;

-- При выборе:
SELECT * FROM users WHERE deleted_at IS NULL;

5. История действий (JSONB)

-- Сохраняем полную историю изменений
INSERT INTO audit_logs (entity_type, entity_id, action, old_values, new_values)
VALUES ('order', '123e4567', 'updated',
  jsonb_build_object('status', 'pending', 'total_price_cents', 10000),
  jsonb_build_object('status', 'confirmed', 'total_price_cents', 10000));

Шаг 4: Индексы (critical для производительности)

-- Поиск по пользователю (частая операция)
CREATE INDEX idx_orders_user_id_created_at ON orders(user_id, created_at DESC);

-- Поиск активных подписок
CREATE INDEX idx_subscriptions_status_end_date ON subscriptions(status, end_date);

-- Аналитика по датам
CREATE INDEX idx_orders_created_at_status ON orders(created_at, status);

Шаг 5: ORM маппинг (Hibernate/JPA)

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private UUID id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<Payment> payments = new ArrayList<>();
    
    private Long totalPriceCents;
    private Long discountCents;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    private UUID id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;
    
    private Integer quantity;
    private Long unitPriceCents;
}

Ключевые моменты моего проектирования

  • Нормализация vs Денормализация — денормализовал total в orders для скорости
  • Индексы — добавил индексы для частых запросов по user_id и created_at
  • JSONB для истории — позволяет гибко хранить изменения
  • UUID вместо int — безопаснее и удобнее
  • Целые числа для денег — избегаем ошибок точности
  • Soft delete — сохраняем данные для аудита
  • Связи в ORM — использую LAZY loading, чтобы не N+1