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

Как правильно хранить цены в базе данных?

2.0 Middle🔥 182 комментариев
#Базы данных и SQL

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

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

Правильное хранение цен в базе данных

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

Основные принципы

1. Используйте целочисленный тип данных Цены никогда не следует хранить как числа с плавающей точкой (FLOAT, DOUBLE) из-за проблем с округлением и точностью. Вместо этого используйте целочисленные типы, представляющие цену в минимальных единицах (копейки, центы, сатоши).

-- ПЛОХОЙ ПРИМЕР (НЕ ДЕЛАЙТЕ ТАК)
CREATE TABLE products (
    price DECIMAL(10, 2) -- Потенциальные проблемы с округлением
);

-- ХОРОШИЙ ПРИМЕР
CREATE TABLE products (
    price_in_cents BIGINT NOT NULL -- Цена в копейках/центах
);

2. Выбирайте подходящий целочисленный тип

  • INT (4 байта): до ~21 млн. единиц (подходит для большинства товаров)
  • BIGINT (8 байта): для очень высоких цен или микро-операций
  • DECIMAL/NUMERIC: когда требуется фиксированная точность (но помните о производительности)

Структура таблиц для сложных сценариев

Для систем с историей изменений цен:

CREATE TABLE products (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    sku VARCHAR(50) NOT NULL UNIQUE,
    base_price_in_cents BIGINT NOT NULL,
    currency_code CHAR(3) NOT NULL DEFAULT 'USD',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE price_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    price_in_cents BIGINT NOT NULL,
    effective_from DATETIME NOT NULL,
    effective_to DATETIME NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Поддержка разных валют

3. Храните валюту и цену раздельно

CREATE TABLE product_prices (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    amount_in_minor_units BIGINT NOT NULL,
    currency_code CHAR(3) NOT NULL, -- ISO 4217 код
    is_active BOOLEAN DEFAULT TRUE,
    INDEX idx_currency (currency_code),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Обработка в PHP

4. Используйте объект-значение (Value Object) для цен:

<?php

declare(strict_types=1);

class Money
{
    private int $amount;
    private string $currency;
    
    public function __construct(int $amount, string $currency)
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        
        if (!preg_match('/^[A-Z]{3}$/', $currency)) {
            throw new InvalidArgumentException('Invalid currency code');
        }
        
        $this->amount = $amount;
        $this->currency = $currency;
    }
    
    public function getAmount(): int
    {
        return $this->amount;
    }
    
    public function getCurrency(): string
    {
        return $this->currency;
    }
    
    public function add(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new CurrencyMismatchException();
        }
        
        return new self($this->amount + $other->amount, $this->currency);
    }
    
    public function toFloat(): float
    {
        return $this->amount / 100;
    }
    
    public static function fromFloat(float $amount, string $currency): self
    {
        return new self((int) round($amount * 100), $currency);
    }
}

Дополнительные рекомендации

5. Учитывайте налоговые требования:

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

6. Для систем с акциями и скидками:

CREATE TABLE price_modifiers (
    id BIGINT PRIMARY KEY,
    product_id BIGINT NOT NULL,
    modifier_type ENUM('PERCENTAGE', 'FIXED_AMOUNT'),
    modifier_value_in_cents BIGINT NOT NULL,
    valid_from DATETIME NOT NULL,
    valid_to DATETIME NULL
);

7. Индексы для производительности:

CREATE INDEX idx_price_currency ON product_prices(amount_in_minor_units, currency_code);
CREATE INDEX idx_price_active ON product_prices(is_active, product_id);

Практические советы

  • Всегда валидируйте входные данные на уровне приложения
  • Используйте транзакции при обновлении цен
  • Реализуйте логирование всех изменений цен
  • Учитывайте требования GDPR/PCI DSS для финансовых данных
  • Для high-load систем рассмотрите кэширование цен в Redis
  • Используйте миграции БД для всех изменений структуры

Тестирование

<?php

test('Money addition works correctly', function () {
    $price1 = new Money(1000, 'USD'); // $10.00
    $price2 = new Money(500, 'USD');  // $5.00
    
    $result = $price1->add($price2);
    
    expect($result->getAmount())->toBe(1500);
    expect($result->toFloat())->toBe(15.0);
});

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