EasyТеория15 min

Обзор хранилищ данных

Классификация баз данных: SQL, NoSQL, NewSQL, time-series -- выбор правильного инструмента для задачи

Зачем нужны разные типы БД

Не существует универсальной базы данных. Каждый тип оптимизирован для определённых паттернов доступа, объёмов данных и требований к консистентности.

Классификация баз данных

По модели данных

Тип Модель Примеры Когда использовать
Реляционные (SQL) Таблицы, связи PostgreSQL, MySQL Структурированные данные, транзакции
Документные JSON/BSON документы MongoDB, CouchDB Полуструктурированные, гибкая схема
Key-Value Ключ-значение Redis, DynamoDB Кэш, сессии, простые структуры
Колоночные Столбцы, семейства Cassandra, HBase Широкие таблицы, временные ряды
Графовые Узлы, рёбра Neo4j, ArangoDB Связи между сущностями
Time-Series Временные ряды TimescaleDB, InfluxDB Метрики, IoT, мониторинг
Поисковые Инвертированный индекс Elasticsearch, Meilisearch Полнотекстовый поиск
Векторные Embeddings Pinecone, pgvector AI/ML, семантический поиск

По теореме CAP

         Consistency
            /\
           /  \
          /    \
         / CA   \
        /________\
       /\   CP   /\
      /  \      /  \
     / AP \    /    \
    /______\  /______\
  Availability    Partition
                  Tolerance
Категория Описание Примеры
CA Консистентность + Доступность PostgreSQL (single-node)
CP Консистентность + Устойчивость к разделению MongoDB, HBase
AP Доступность + Устойчивость к разделению Cassandra, DynamoDB

Важно: CAP-теорема -- упрощение. На практике выбор между C и A -- это спектр, а не бинарный выбор. Система может жертвовать консистентностью для части операций.

SQL vs NoSQL

<?php

declare(strict_types=1);

/**
 * SQL approach: structured, normalized data
 */
final class SqlOrderRepository
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    public function findOrderWithItems(string $orderId): array
    {
        // Normalized: data in separate tables, joined at query time
        $stmt = $this->db->prepare(<<<SQL
            SELECT
                o.id, o.user_id, o.status, o.total_amount, o.created_at,
                oi.product_id, oi.quantity, oi.price,
                p.name as product_name, p.category
            FROM orders o
            JOIN order_items oi ON o.id = oi.order_id
            JOIN products p ON oi.product_id = p.id
            WHERE o.id = :order_id
        SQL);

        $stmt->execute(['order_id' => $orderId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

/**
 * NoSQL approach: denormalized, document-oriented
 */
final class NoSqlOrderRepository
{
    public function __construct(
        private readonly MongoDB\Collection $collection,
    ) {}

    public function findOrder(string $orderId): ?array
    {
        // Denormalized: everything in one document
        return $this->collection->findOne(['_id' => $orderId]);
        /*
         * Returns:
         * {
         *   "_id": "ord_123",
         *   "user_id": "user_456",
         *   "status": "completed",
         *   "total_amount": 99.99,
         *   "items": [
         *     {"product_id": "p1", "name": "Widget", "category": "electronics", "qty": 2, "price": 49.99}
         *   ],
         *   "created_at": "2024-01-15T10:30:00Z"
         * }
         */
    }
}
| Критерий | SQL (Реляционные) | NoSQL | |----------|------------------|-------| | Схема | Строгая (schema-on-write) | Гибкая (schema-on-read) | | Масштабирование | Вертикальное (+ read replicas) | Горизонтальное | | Транзакции | ACID | BASE (eventually consistent) | | Связи | JOIN | Денормализация / embedding | | Запросы | SQL (стандарт) | Специфичные API | | Подходит для | OLTP, финансы | Большие объёмы, гибкая схема |

NewSQL

NewSQL -- системы, которые сочетают масштабируемость NoSQL с ACID-гарантиями SQL.

БД Описание Особенности
CockroachDB Geo-distributed SQL Совместим с PostgreSQL wire protocol
TiDB MySQL-совместимый Горизонтально масштабируемый
YugabyteDB PostgreSQL-совместимый Распределённый
Vitess Шардинг для MySQL YouTube scale
Google Spanner Глобальная БД TrueTime, внешняя консистентность

Когда NewSQL

  • Нужны ACID-транзакции на распределённой системе
  • Объём данных перерос одиночный сервер PostgreSQL/MySQL
  • Нужна geo-распределённость с сильной консистентностью
  • Команда знает SQL и не хочет переучиваться

Time-Series базы данных

Оптимизированы для записи и чтения данных с временной меткой.

БД Описание PHP-поддержка
TimescaleDB Расширение PostgreSQL PDO (PostgreSQL)
InfluxDB Нативная TSDB HTTP API
Prometheus Мониторинг HTTP API
QuestDB Высокопроизводительная REST/PostgreSQL wire
<?php

declare(strict_types=1);

/**
 * TimescaleDB: PostgreSQL extension for time-series
 */
final class MetricsRepository
{
    public function __construct(
        private readonly \PDO $db,
    ) {}

    /**
     * Setup hypertable for metrics
     */
    public function createMetricsTable(): void
    {
        $this->db->exec(<<<SQL
            CREATE TABLE IF NOT EXISTS metrics (
                time TIMESTAMPTZ NOT NULL,
                host TEXT NOT NULL,
                metric_name TEXT NOT NULL,
                value DOUBLE PRECISION NOT NULL
            );

            -- Convert to hypertable (TimescaleDB)
            SELECT create_hypertable('metrics', 'time',
                chunk_time_interval => INTERVAL '1 day',
                if_not_exists => TRUE
            );

            -- Compression policy
            ALTER TABLE metrics SET (
                timescaledb.compress,
                timescaledb.compress_segmentby = 'host,metric_name'
            );

            SELECT add_compression_policy('metrics', INTERVAL '7 days');
        SQL);
    }

    /**
     * Insert metrics in batch
     */
    public function insertBatch(array $metrics): void
    {
        $stmt = $this->db->prepare(<<<SQL
            INSERT INTO metrics (time, host, metric_name, value)
            VALUES (:time, :host, :metric_name, :value)
        SQL);

        foreach ($metrics as $metric) {
            $stmt->execute($metric);
        }
    }

    /**
     * Time-bucketed aggregation
     */
    public function getAverageByMinute(string $host, string $metricName, int $hours = 1): array
    {
        $stmt = $this->db->prepare(<<<SQL
            SELECT
                time_bucket('1 minute', time) as bucket,
                AVG(value) as avg_value,
                MAX(value) as max_value,
                MIN(value) as min_value
            FROM metrics
            WHERE host = :host
              AND metric_name = :metric_name
              AND time > NOW() - INTERVAL ':hours hours'
            GROUP BY bucket
            ORDER BY bucket DESC
        SQL);

        $stmt->execute([
            'host' => $host,
            'metric_name' => $metricName,
            'hours' => $hours,
        ]);

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}
## OLTP vs OLAP
Свойство OLTP OLAP
Назначение Транзакции Аналитика
Запросы Простые, по индексу Сложные, агрегации
Объём данных запроса Строки/единицы Миллионы строк
Модель Нормализованная (3NF) Денормализованная (Star)
Обновления Частые Редкие (batch load)
Примеры PostgreSQL, MySQL ClickHouse, BigQuery
<?php

declare(strict_types=1);

/**
 * OLTP vs OLAP query patterns
 */
final class QueryPatternComparison
{
    public function __construct(
        private readonly \PDO $oltpDb,
        private readonly \PDO $olapDb,
    ) {}

    /**
     * OLTP: get single order (milliseconds)
     * Point lookup by primary key
     */
    public function getOrder(string $orderId): array
    {
        $stmt = $this->oltpDb->prepare(
            'SELECT * FROM orders WHERE id = :id'
        );
        $stmt->execute(['id' => $orderId]);
        return $stmt->fetch(\PDO::FETCH_ASSOC);
    }

    /**
     * OLAP: aggregate revenue by month (seconds)
     * Full table scan with aggregation
     */
    public function getMonthlyRevenue(int $year): array
    {
        $stmt = $this->olapDb->prepare(<<<SQL
            SELECT
                toMonth(created_at) as month,
                sum(total_amount) as revenue,
                count() as order_count,
                avg(total_amount) as avg_order
            FROM orders
            WHERE toYear(created_at) = :year
            GROUP BY month
            ORDER BY month
        SQL);

        $stmt->execute(['year' => $year]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}
## Паттерны использования разных БД

Polyglot Persistence

Использование разных БД для разных задач в одном приложении:

┌─────────────────────────────────────────────┐
│              Application Layer              │
├──────────┬──────────┬──────────┬────────────┤
│ Orders   │ Sessions │ Search   │ Analytics  │
│ Payments │ Cache    │ Catalog  │ Metrics    │
├──────────┼──────────┼──────────┼────────────┤
│PostgreSQL│  Redis   │Elastic   │ClickHouse  │
│ (ACID)   │ (Fast)   │(Search)  │ (OLAP)     │
└──────────┴──────────┴──────────┴────────────┘

Предостережение: Polyglot persistence усложняет операционную нагрузку. Начинайте с PostgreSQL и добавляйте специализированные БД только когда PostgreSQL не справляется с конкретной задачей.

Итоги

  • Реляционные БД (PostgreSQL) -- выбор по умолчанию для большинства задач
  • NoSQL решает конкретные проблемы: масштабирование, гибкая схема, geo-распределение
  • NewSQL даёт SQL + горизонтальное масштабирование, но сложнее в эксплуатации
  • Time-series БД необходимы для метрик и мониторинга
  • Выбирайте БД исходя из паттернов доступа к данным, а не модных трендов