База данных для Telegram бота и MiniApp: какую выбрать в 2026 году

База данных для Telegram бота и MiniApp: выбор и архитектура

Выбор базы данных для Telegram-проекта сильно зависит от типа бота и ожидаемой нагрузки. Бот для уведомлений одного сервиса и мультиплеерная игра-MiniApp с 100,000 пользователей требуют принципиально разных решений. Разберём все сценарии с примерами кода.

Типы Telegram-проектов и их требования

Простой бот (напоминания, подписки, уведомления):

  • Данные: user_id, настройки, состояние диалога
  • Нагрузка: низкая (<100 RPS)
  • Решение: SQLite достаточно

Чат-бот с AI (GPT-wrapper, обработка документов):

  • Данные: история диалогов, файлы, токены
  • Нагрузка: средняя, пики при запросах
  • Решение: PostgreSQL + Redis для кэша

MiniApp — игра или SaaS (рейтинги, транзакции, социальные функции):

  • Данные: пользователи, баллы, история, лидерборд
  • Нагрузка: высокая, конкурентная запись
  • Решение: PostgreSQL + Redis

MiniApp — e-commerce (каталог, корзина, заказы):

  • Данные: продукты, пользователи, заказы, платежи
  • Нагрузка: средняя, критична целостность данных
  • Решение: PostgreSQL (ACID-транзакции)

SQLite: для простых ботов без сервера

SQLite — идеальный старт для простого бота. Нет сервера, нет настройки, файл рядом с кодом.

Python + aiogram 3 + aiosqlite

# pip install aiogram aiosqlite

import asyncio
import aiosqlite
from aiogram import Bot, Dispatcher, types
from aiogram.filters import CommandStart

DB_PATH = "bot.db"

async def init_db():
    async with aiosqlite.connect(DB_PATH) as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                user_id INTEGER UNIQUE NOT NULL,
                username TEXT,
                first_name TEXT,
                language TEXT DEFAULT 'ru',
                notifications_enabled BOOLEAN DEFAULT 1,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        """)
        await db.execute("""
            CREATE TABLE IF NOT EXISTS reminders (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                text TEXT NOT NULL,
                remind_at DATETIME NOT NULL,
                sent BOOLEAN DEFAULT 0,
                FOREIGN KEY (user_id) REFERENCES users(id)
            )
        """)
        await db.commit()

async def get_or_create_user(user: types.User) -> dict:
    async with aiosqlite.connect(DB_PATH) as db:
        db.row_factory = aiosqlite.Row
        # Upsert — создать или обновить
        await db.execute("""
            INSERT INTO users (user_id, username, first_name)
            VALUES (?, ?, ?)
            ON CONFLICT(user_id) DO UPDATE SET
                username = excluded.username,
                first_name = excluded.first_name
        """, (user.id, user.username, user.first_name))
        await db.commit()
        
        async with db.execute(
            "SELECT * FROM users WHERE user_id = ?", (user.id,)
        ) as cursor:
            return dict(await cursor.fetchone())

# Инициализация бота
bot = Bot(token="YOUR_BOT_TOKEN")
dp = Dispatcher()

@dp.message(CommandStart())
async def start(message: types.Message):
    user = await get_or_create_user(message.from_user)
    await message.answer(
        f"Привет, {user['first_name']}! Я запомнил тебя 👋"
    )

async def main():
    await init_db()
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

Node.js + telegraf + better-sqlite3

// npm install telegraf better-sqlite3 @types/better-sqlite3

import { Telegraf } from 'telegraf'
import Database from 'better-sqlite3'

const db = new Database('bot.db')
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')

// Инициализация схемы
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    telegram_id INTEGER UNIQUE NOT NULL,
    username TEXT,
    name TEXT,
    points INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
  
  CREATE TABLE IF NOT EXISTS actions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER REFERENCES users(id),
    action TEXT NOT NULL,
    metadata TEXT, -- JSON
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`)

// Prepared statements для производительности
const getUser = db.prepare('SELECT * FROM users WHERE telegram_id = ?')
const createUser = db.prepare(
  'INSERT OR IGNORE INTO users (telegram_id, username, name) VALUES (?, ?, ?)'
)
const addPoints = db.prepare(
  'UPDATE users SET points = points + ? WHERE telegram_id = ?'
)

const bot = new Telegraf(process.env.BOT_TOKEN!)

bot.start((ctx) => {
  const { id, username, first_name } = ctx.from
  createUser.run(id, username ?? null, first_name)
  const user = getUser.get(id) as any
  ctx.reply(`Привет! У тебя ${user.points} баллов 🎯`)
})

bot.launch()

PostgreSQL: для продакшн-бота и MiniApp

PostgreSQL — выбор для серьёзного проекта. ACID-транзакции, конкурентная запись, оконные функции для лидербордов.

Python + aiogram 3 + asyncpg

# pip install aiogram asyncpg python-dotenv

import asyncpg
import asyncio
from aiogram import Bot, Dispatcher

DATABASE_URL = "postgresql://user:pass@host:5432/botdb"

# Connection pool — переиспользование соединений
pool: asyncpg.Pool = None

async def create_pool():
    global pool
    pool = await asyncpg.create_pool(
        DATABASE_URL,
        min_size=5,
        max_size=20,
        command_timeout=10,
    )
    
    # Создание таблиц при старте
    async with pool.acquire() as conn:
        await conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id BIGINT PRIMARY KEY,           -- telegram user_id
                username TEXT,
                first_name TEXT NOT NULL,
                language_code VARCHAR(10) DEFAULT 'ru',
                is_premium BOOLEAN DEFAULT false,
                balance DECIMAL(10,2) DEFAULT 0,
                referral_id BIGINT REFERENCES users(id),
                registered_at TIMESTAMPTZ DEFAULT NOW()
            );
            
            CREATE TABLE IF NOT EXISTS transactions (
                id SERIAL PRIMARY KEY,
                user_id BIGINT REFERENCES users(id),
                amount DECIMAL(10,2) NOT NULL,
                type VARCHAR(50) NOT NULL,  -- deposit, withdraw, bonus
                description TEXT,
                created_at TIMESTAMPTZ DEFAULT NOW()
            );
            
            CREATE INDEX IF NOT EXISTS idx_transactions_user_id 
                ON transactions(user_id);
        """)

async def get_or_create_user(user_id: int, username: str, first_name: str) -> dict:
    async with pool.acquire() as conn:
        # UPSERT — атомарная операция
        row = await conn.fetchrow("""
            INSERT INTO users (id, username, first_name)
            VALUES ($1, $2, $3)
            ON CONFLICT (id) DO UPDATE SET
                username = EXCLUDED.username,
                first_name = EXCLUDED.first_name
            RETURNING *
        """, user_id, username, first_name)
        return dict(row)

async def add_points_atomic(user_id: int, amount: float, description: str):
    """Атомарное добавление баллов + запись в историю"""
    async with pool.acquire() as conn:
        async with conn.transaction():
            await conn.execute(
                "UPDATE users SET balance = balance + $1 WHERE id = $2",
                amount, user_id
            )
            await conn.execute("""
                INSERT INTO transactions (user_id, amount, type, description)
                VALUES ($1, $2, 'bonus', $3)
            """, user_id, amount, description)

async def get_leaderboard(limit: int = 10) -> list:
    async with pool.acquire() as conn:
        rows = await conn.fetch("""
            SELECT 
                first_name,
                balance,
                RANK() OVER (ORDER BY balance DESC) as rank
            FROM users
            ORDER BY balance DESC
            LIMIT $1
        """, limit)
        return [dict(r) for r in rows]

Telegram MiniApp: архитектура бэкенда

MiniApp — React/Vue приложение внутри Telegram. Бэкенд обычно REST или GraphQL API.

// backend/src/routes/miniapp.ts — Node.js/Bun.js + Fastify

import Fastify from 'fastify'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { validateTelegramInitData } from './telegram-auth'

const sql = postgres(process.env.DATABASE_URL!)
const db = drizzle(sql)

const app = Fastify()

// Middleware: проверка Telegram InitData
app.addHook('preHandler', async (request, reply) => {
    const initData = request.headers['x-telegram-init-data'] as string
    
    if (!initData) {
        return reply.status(401).send({ error: 'No auth' })
    }
    
    const user = validateTelegramInitData(initData, process.env.BOT_TOKEN!)
    if (!user) {
        return reply.status(401).send({ error: 'Invalid auth' })
    }
    
    // Прикрепляем пользователя к запросу
    request.telegramUser = user
})

// Endpoint: профиль пользователя
app.get('/api/profile', async (request) => {
    const { id, first_name, username } = request.telegramUser
    
    // Upsert в PostgreSQL
    const [user] = await db
        .insert(users)
        .values({ telegramId: BigInt(id), firstName: first_name, username })
        .onConflictDoUpdate({
            target: users.telegramId,
            set: { firstName: first_name, username },
        })
        .returning()
    
    return { user }
})

// Endpoint: купить предмет (транзакция)
app.post('/api/shop/buy', async (request, reply) => {
    const { itemId } = request.body as { itemId: number }
    const userId = BigInt(request.telegramUser.id)
    
    // Атомарная транзакция
    try {
        await db.transaction(async (tx) => {
            const [user] = await tx
                .select({ balance: users.balance })
                .from(users)
                .where(eq(users.telegramId, userId))
                .for('update') // FOR UPDATE — блокировка строки
            
            const [item] = await tx
                .select()
                .from(shopItems)
                .where(eq(shopItems.id, itemId))
            
            if (user.balance < item.price) {
                throw new Error('Недостаточно баллов')
            }
            
            // Списание баллов
            await tx.update(users)
                .set({ balance: sql`balance - ${item.price}` })
                .where(eq(users.telegramId, userId))
            
            // Добавление предмета в инвентарь
            await tx.insert(inventory).values({ userId, itemId })
        })
        
        return { success: true }
    } catch (e: any) {
        return reply.status(400).send({ error: e.message })
    }
})

Redis: сессии, rate limiting и кэш

Redis решает задачи, которые PostgreSQL делает медленно или неудобно:

# pip install redis[asyncio]

import redis.asyncio as redis

redis_client = redis.from_url("redis://localhost:6379", decode_responses=True)

# 1. Состояние FSM (Finite State Machine) для aiogram
async def set_user_state(user_id: int, state: str, ttl: int = 3600):
    await redis_client.setex(f"state:{user_id}", ttl, state)

async def get_user_state(user_id: int) -> str | None:
    return await redis_client.get(f"state:{user_id}")

# 2. Rate limiting (защита от спама)
async def check_rate_limit(user_id: int, action: str, limit: int, window: int) -> bool:
    key = f"ratelimit:{user_id}:{action}"
    pipe = redis_client.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    results = await pipe.execute()
    count = results[0]
    return count <= limit  # True = разрешено

# Использование
@dp.message()
async def handle_message(message: types.Message):
    allowed = await check_rate_limit(
        message.from_user.id, 
        "message", 
        limit=5,    # 5 сообщений
        window=60   # в минуту
    )
    if not allowed:
        await message.answer("⏱ Подождите немного...")
        return
    # обработка...

# 3. Кэш лидерборда (обновляется каждые 5 минут)
async def get_cached_leaderboard():
    cached = await redis_client.get("leaderboard")
    if cached:
        return json.loads(cached)
    
    # Запрос к PostgreSQL — дорогой
    leaderboard = await get_leaderboard()
    await redis_client.setex("leaderboard", 300, json.dumps(leaderboard))
    return leaderboard

Архитектура по размеру проекта

| Проект | БД | Redis | Хостинг | |---|---|---|---| | Простой бот (<1,000 users) | SQLite | Нет | VPS 512MB | | Средний бот (<10,000 users) | PostgreSQL | Нет (опционально) | VPS 1-2GB | | Большой бот / MiniApp | PostgreSQL | Да | VPS 4GB+ | | Highload MiniApp | PostgreSQL + реплика | Redis Cluster | Облако |

Бэкапы для Telegram-бота

Потеря базы бота = потеря всех пользователей и их данных. Критично настроить до запуска:

# SQLite бот: бэкап каждые 30 минут
dbsend source add \
  --name="telegram-bot" \
  --db=sqlite:///opt/bot/bot.db \
  --schedule="*/30 * * * *" \
  --keep=200

# PostgreSQL бот: бэкап каждый час
dbsend source add \
  --name="telegram-bot-pg" \
  --db="${DATABASE_URL}" \
  --schedule="0 * * * *" \
  --keep=72

# Проверка
dbsend status

FAQ

SQLite или PostgreSQL для начинающего разработчика Telegram-бота?

Начните с SQLite — нулевая настройка, не нужен отдельный сервер, проще локальная разработка. SQLite справится с нагрузкой до 5,000-10,000 пользователей при правильном WAL-режиме. Когда почувствуете ограничения (конкурентная запись, сложные запросы) — мигрируйте на PostgreSQL. Aiogram поддерживает оба варианта.

Что такое Telegram InitData и зачем его проверять?

InitData — зашифрованные данные о пользователе, которые Telegram передаёт в MiniApp при запуске. Содержит user_id, имя, username. Верификация через HMAC-SHA256 подтверждает, что данные действительно от Telegram, а не подделаны злоумышленником. Без проверки InitData — уязвимость: любой может притвориться любым пользователем.

Можно ли хранить историю чата с AI в SQLite?

Да, для небольших объёмов. Структура: таблица messages с полями user_id, role (user/assistant), content (TEXT), created_at. При запросе к AI извлекайте последние N сообщений. При больших объёмах (>1M сообщений) лучше PostgreSQL с партиционированием таблицы по дате.

Нужен ли Redis для aiogram FSM?

С версии aiogram 3.x FSM поддерживает несколько storage backends: MemoryStorage (по умолчанию, теряется при перезапуске), RedisStorage (persistent, рекомендуется для продакшна), MongoStorage. Для продакшн-бота — RedisStorage обязателен, иначе пользователи теряют контекст диалога при каждом деплое.

Какую ORM использовать для Telegram-бота на Python?

SQLAlchemy (async) — мощная, поддерживает SQLite и PostgreSQL, хорошая типизация. Tortoise ORM — специально для async-кода, проще в настройке. peewee — синхронная, простая, подходит для простых ботов без async. Для Node.js — Drizzle ORM (поддерживает SQLite через better-sqlite3 и PostgreSQL через postgres.js).