Когда бот на aiogram начинает хранить не пару chat_id, а реальные данные - заказы, лиды, историю переписки, статусы подписки, - встаёт вопрос хранения. Я закладываю бот с базой данных PostgreSQL в архитектуру сразу, даже если на старте нужно хранить десяток полей: переезд с SQLite или json-файла на Postgres посреди продакшена обходится дороже, чем правильная схема с нуля. Ниже - рабочая архитектура, которую использую в проектах на aiogram 3.x: слои, подключение через SQLAlchemy 2.0 и asyncpg, миграции Alembic, хранение FSM и деплой в Docker.
Зачем telegram-боту на aiogram база PostgreSQL, а не SQLite
SQLite подкупает простотой - один файл, ноль настройки, годится для MVP и тестов. Но aiogram работает асинхронно, и на потоке от 150-200 сообщений в минуту SQLite начинает отдавать database is locked при параллельной записи: движок блокирует файл целиком на время транзакции. Упирался в это на боте для приёма заявок, где вебхуки от Т‑Банка про оплату и сообщения пользователей прилетали почти одновременно - часть записей терялась без внятной ошибки в логах.
Postgres держит десятки параллельных подключений через пул, поддерживает транзакции с изоляцией на уровне строк и не разваливается при конкурентной записи. Для бота это критично, если он:
- принимает оплату (эквайринг Т‑Банка, ЮKassa) и должен зафиксировать статус заказа без потерь;
- работает как приёмник заявок с интеграцией в СДЭК или CRM;
- хранит историю диалога для чат-бота с базой знаний на RAG;
- растёт за 500+ активных пользователей в день.
Для бота-опросника на 20 вопросов без интеграций хватит и SQLite. Но если в планах хоть одна внешняя интеграция - закладываю Postgres сразу, чтобы не переписывать слой хранения на 300 активных пользователях.
Архитектура проекта: слои и модули
Плоский bot.py с десятком хендлеров и прямыми SQL-запросами внутри живёт до первой сложной фичи. Дальше правки в схему занимают часы вместо минут, потому что SQL-запрос к таблице users раскидан по семи файлам. Использую слоистую структуру:
- handlers/ - только aiogram-логика: разбор апдейтов, клавиатуры, состояния FSM;
- services/ - бизнес-логика: расчёт цены заказа, проверка лимитов, вызов внешних API;
- repositories/ - весь SQL и работа с сессией SQLAlchemy, ничего про aiogram не знает;
- models/ - декларативные модели SQLAlchemy;
- db/ - engine, sessionmaker, middleware, конфиг подключения.
Хендлер не видит SQL напрямую - он вызывает OrderRepository.create(session, user_id, items), а репозиторий не знает про Message или CallbackQuery. Такое разделение экономит время на тестах: репозитории проверяются на отдельной тестовой базе без поднятия бота целиком, а бизнес-логику в services можно гонять через pytest с моками репозитория.
Подключение к PostgreSQL: asyncpg, SQLAlchemy 2.0 и пул соединений
Для aiogram 3.x беру связку asyncpg как драйвер и SQLAlchemy 2.0 в async-режиме поверх него - asyncpg сам по себе быстрее, но без ORM SQL обрастает дублированием уже на 5-6 таблицах.
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from aiogram import BaseMiddleware
engine = create_async_engine(
"postgresql+asyncpg://bot:secret@localhost:5432/botdb",
pool_size=10,
max_overflow=5,
pool_pre_ping=True,
)
async_session = async_sessionmaker(engine, expire_on_commit=False)
class DbSessionMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
async with async_session() as session:
data["session"] = session
return await handler(event, data)
pool_pre_ping=True спасает от протухших соединений после простоя Postgres в контейнере - без него бот раз в сутки падал на первом запросе после ночного затишья с OperationalError. pool_size держу на 10-15 для бота с нагрузкой до 1000 пользователей в день, для более крупных проектов поднимаю до 20-30 и слежу за max_connections в postgresql.conf, чтобы не упереться в лимит инстанса.
Сессию прокидываю через middleware - на каждый апдейт открывается новая сессия и закрывается после хендлера, без утечек соединений.
| Вариант | Скорость | Миграции | Порог входа |
|---|---|---|---|
| asyncpg напрямую | Высокая | Руками | Нужно писать SQL |
| SQLAlchemy 2.0 async | Средняя-высокая | Alembic | Средний |
| Tortoise ORM | Средняя | Aerich | Низкий |
Для бота с 3-4 таблицами разница в скорости между чистым asyncpg и SQLAlchemy почти не ощущается - на нагрузке 50-100 запросов в секунду оверхед ORM в пределах 5-10%. Беру SQLAlchemy почти всегда: миграции и типизация моделей экономят время при росте схемы.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Модели и миграции: Alembic для эволюции схемы
from datetime import datetime
from sqlalchemy import BigInteger, String, ForeignKey, func
from sqlalchemy.orm import Mapped, mapped_column, relationship, DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
username: Mapped[str | None] = mapped_column(String(64))
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
orders: Mapped[list["Order"]] = relationship(back_populates="user")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
status: Mapped[str] = mapped_column(String(32), default="new")
amount: Mapped[int] = mapped_column()
user: Mapped["User"] = relationship(back_populates="orders")
Схема бота меняется постоянно: сегодня нужно поле phone у заказа, через месяц - статус доставки от СДЭК, через два - таблица под эмбеддинги для чат-бота с базой знаний. Без миграций правки в проде - это либо ALTER TABLE руками через psql с риском забыть применить его на бэкапе, либо потеря данных при пересоздании таблиц.
Alembic генерирует миграцию по diff моделей и применяет её одной командой.
alembic init migrations
alembic revision --autogenerate -m "add orders table"
alembic upgrade head
revision - autogenerate не идеален - на переименовании колонки он предложит удалить старую и создать новую, что уронит данные. Каждую автосгенерированную миграцию читаю перед применением, особенно на боевой базе с заказами: цена ошибки - потерянные платежи.
Хранилище FSM-состояний и пользовательские данные в Postgres
FSM в aiogram по умолчанию живёт в MemoryStorage - рестарт контейнера обнуляет все состояния, и пользователь на середине заполнения заявки (например, многошаговая форма доставки с адресом и телефоном для интеграции с СДЭК) откатывается на старт. Для бота, который деплоится через CI на каждый пуш, это раздражает пользователей на третьем шаге формы.
Варианты:
- RedisStorage - быстро, штатно поддерживается aiogram, но добавляет ещё один сервис в инфраструктуру;
- собственное хранилище на Postgres через кастомный класс BaseStorage - состояние и data пишутся в ту же базу, что и остальные данные бота, без Redis в docker-compose.
Для проектов, где Redis уже есть под кэш или очереди - беру RedisStorage. Для небольших ботов без Redis пишу состояние в отдельную таблицу fsm_state (user_id, chat_id, state, data jsonb) - на 5-10 полей формы работает без проблем, а бэкап состояний идёт вместе с бэкапом основной базы.
Шаблон репозитория и middleware для такой связки выкладываю в библиотеке готовых скриптов - можно взять как основу и адаптировать под свою схему.
Docker Compose: бот, Postgres и миграции при деплое
Локально и на проде запускаю бота, Postgres и применение миграций одним docker-compose.yml - миграции идут отдельным сервисом, который стартует перед ботом и завершается после успешного upgrade head.
version: "3.9"
services:
bot:
build: .
depends_on:
migrate:
condition: service_completed_successfully
env_file: .env
restart: unless-stopped
migrate:
build: .
command: alembic upgrade head
env_file: .env
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_DB: botdb
POSTGRES_USER: bot
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
На проде добавляю pg_dump в cron контейнера раз в сутки с ротацией на 7 дней - обходится дешевле, чем восстанавливать заказы за неделю вручную из логов. Если бот встроен в цепочку с n8n - например, уведомляет менеджера в отдельный чат при новом заказе или дёргает вебхук на смену статуса - держу n8n в том же docker-compose на соседнем порту, чтобы не гонять запросы между сервисами через интернет, когда оба крутятся на одном сервере.
Автоматизация в мессенджере
Telegram-бот / Mini App
от 30 000 ₽
Подробнее →Частые вопросы
Нужна ли отдельная база для простого бота-опросника?
Если бот без интеграций и меньше 10 полей данных на пользователя, обычно хватает SQLite или даже json-файла на первых порах. Postgres закладываю, когда планируются платежи, CRM, СДЭК или рост аудитории за пару сотен активных пользователей в день - переезд на большую базу позже стоит дороже, чем сразу спроектировать схему под Postgres.
Чем плоха SQLite для aiogram-бота в проде?
Основная проблема - блокировка файла на запись: при параллельных апдейтах от нескольких пользователей часть транзакций падает с database is locked. На потоке от 150-200 сообщений в минуту это проявляется регулярно, а не как редкий случай.
Как переносить данные бота при переезде на новый сервер?
pg_dump в кастомном формате (-Fc) на старом сервере, pg_restore на новом - миграции Alembic на новой базе не трогаю, схема приезжает вместе с данными. Для бота с 10-15 таблицами и парой миллионов строк перенос занимает 10-20 минут в зависимости от канала между серверами.
Сколько стоит разработка бота с базой Postgres под ключ?
У меня разработка telegram-бота с базой Postgres начинается от 30 000 ₽ - цена растёт от количества интеграций: приём оплаты, CRM, СДЭК, многошаговые FSM-формы. На рынке у студий похожий бот может стоить в разы дороже за счёт менеджмента и допродажи часов, у фрилансеров - сопоставимо по цене, но без гарантии архитектуры, которая переживёт рост нагрузки.