НОВОЕ 📢 Ограниченное стартовое предложение: скидка до 70% на первый месяц для любой игры!Посмотреть тарифы

Как использовать базу данных MySQL с ботом aiogram (aiomysql)

Хостинг Telegram-ботовUpdated Nov 6, 202611 min read

Коротко. Добавьте aiomysql в бота на aiogram 3, откройте один пул с autocommit=True до начала polling и передайте его обработчикам через dp.start_polling(bot, pool=pool): aiogram передаст его каждому обработчику, у которого есть параметр pool. Используйте плейсхолдеры %s и храните ID Telegram как BIGINT, а не как INT. В PlayNHost каждый тариф для ботов включает одну базу MySQL: создайте её на странице Базы данных, вставьте её данные в раздел Окружение и код ниже заработает как есть. База принимает подключения с ваших серверов PlayNHost и из phpMyAdmin, но не с вашего компьютера. Мы проверили этот код 6 ноября 2026 года с aiogram 3.31.0, aiomysql 0.3.2 и Python 3.12, на MariaDB 10.11, с аккаунтом базы данных, настроенным как наш (20 подключений, лимит запроса 120 секунд), и заглушкой вместо сервера Bot API Telegram: 8 команд на входе, 8 ответов на выходе, заметки сохранены в базе. Настоящий вход в Telegram в тест не входил.

Что понадобится

1. Создайте базу данных

  1. Откройте сервер бота в панели и нажмите Базы данных.
  2. Нажмите Новая база данных. Поле Метка необязательно; поле Будет создана как показывает полное имя. Нажмите Создать базу данных.
  3. Откроется страница новой базы (позже нажмите на неё в разделе Ваши базы данных). Её карточка Подключение показывает Хост, Порт, База данных, Имя пользователя и Пароль. В блоке Готовый конфиг в поле Конфиг для есть фрагмент Python (aiomysql) с теми же уже подставленными значениями.

2. Внесите данные в Окружение

  1. Откройте страницу Бот, раздел Окружение, и нажмите Вставить файл .env.
  2. Вставьте эти пять строк со значениями с карточки Подключение:
    DB_HOST=your-host
    DB_PORT=3306
    DB_NAME=your-database
    DB_USER=your-username
    DB_PASSWORD=your-password
  3. DB_PASSWORD получает метку Секрет: значение сохраняется только для записи и больше не показывается. Нажмите Импортировать 5 переменных.

Используйте реальный порт с карточки Подключение. Токена бота среди этих строк нет: токен, который вы сохранили на странице Бот, раздел Telegram, уже доступен в вашем коде как BOT_TOKEN, так называется он в примерах самого aiogram (а также как TELEGRAM_BOT_TOKEN, TOKEN, TELEGRAM_TOKEN и TELOXIDE_TOKEN). Если в вашем файле .env есть строка с токеном, диалог её пропустит. Подробнее об этом разделе: Как задать переменные окружения и секреты для Discord-бота.

3. Код

Небольшой бот для заметок: /note buy milk сохраняет заметку, /notes показывает ваши последние пять.

requirements.txt

aiogram>=3,<4
aiomysql

bot.py

import asyncio
import logging
import os
import sys

import aiomysql
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandObject
from aiogram.types import Message

dp = Dispatcher()


@dp.message(Command("note"))
async def note(message: Message, command: CommandObject, pool: aiomysql.Pool) -> None:
    text = (command.args or "").strip()
    if not text:
        await message.answer("Write your note after the command, for example: /note buy milk")
        return
    if len(text) > 500:
        await message.answer("A note can be up to 500 characters.")
        return
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "INSERT INTO notes (user_id, text) VALUES (%s, %s)",
                (message.from_user.id, text),
            )
    await message.answer("Saved. Send /notes to see your last five.")


@dp.message(Command("notes"))
async def notes(message: Message, pool: aiomysql.Pool) -> None:
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "SELECT text FROM notes WHERE user_id = %s ORDER BY id DESC LIMIT 5",
                (message.from_user.id,),
            )
            rows = await cur.fetchall()
    if not rows:
        await message.answer("You have no notes yet.")
        return
    await message.answer("\n".join(f"- {text}" for (text,) in rows))


async def main() -> None:
    # One pool for the whole bot. The values come from the Environment pane.
    pool = await aiomysql.create_pool(
        host=os.environ["DB_HOST"],
        port=int(os.environ["DB_PORT"]),
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASSWORD"],
        db=os.environ["DB_NAME"],
        autocommit=True,
        maxsize=5,
    )
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "CREATE TABLE IF NOT EXISTS notes ("
                "id INT AUTO_INCREMENT PRIMARY KEY, "
                "user_id BIGINT NOT NULL, "
                "text VARCHAR(500) NOT NULL, "
                "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
                "KEY user_id (user_id))"
            )
    print("Database ready")

    bot = Bot(os.environ["BOT_TOKEN"])
    try:
        # Every keyword passed here reaches the handlers that ask for it by name.
        await dp.start_polling(bot, pool=pool)
    finally:
        pool.close()
        await pool.wait_closed()


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())

Что он делает: main() открывает пул, создаёт таблицу notes если её нет, и выводит Database ready до того, как бот начнёт запрашивать сообщения у Telegram. dp.start_polling(bot, pool=pool) служит собственным способом aiogram передавать объект: каждый дополнительный именованный аргумент, который вы передаёте, становится данными, которые aiogram отдаёт любому обработчику с параметром с таким именем, поэтому note() и notes() получают один и тот же пул. Когда бот останавливается, блок finally закрывает пул.

Плейсхолдеры %s подставляет драйвер, поэтому заметка вроде '); DROP TABLE notes; -- сохранится как обычный текст. В нашем тесте она сохранилась как строка 2, а таблица осталась на месте. Никогда не собирайте SQL через f-строку.

Храните ID Telegram как BIGINT

В документации Bot API Telegram сказано, что ID пользователя или чата «может содержать более 32 значащих бит», но не более 52, поэтому 64-битное целое безопасно. На странице об ID даны диапазоны: ID пользователей доходят до 1.099.511.627.775, а ID групп, супергрупп и каналов отрицательные. Супергруппы и каналы идут от -1.000.000.000.001 до -1.997.852.516.352 (например, -1001234567890). Тип INT в MySQL заканчивается на 2.147.483.647. В нашем тесте, при том же строгом режиме SQL, который использует наш сервер базы данных, сохранить ID пользователя 7.000.000.001 в столбце INT не удалось. aiomysql выбросил:

pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")

Столбец BIGINT сохранил его, как и -1001234567890. Берите обычный BIGINT, а не BIGINT UNSIGNED, чтобы поместились и ID чатов.

4. Запустите

  1. Загрузите bot.py и requirements.txt в разделе Файлы.
  2. Нажмите Запустить и откройте Консоль. При запуске устанавливаются пакеты из requirements.txt, затем появляются [PlayNHost] Starting your bot, Database ready и Run polling for bot @your_bot. Последняя строка переключает статус на вкладке Обзор на Онлайн в Telegram.
  3. В Telegram отправьте боту /note buy milk, затем /notes. Чтобы посмотреть строки, откройте страницу базы и нажмите Открыть в phpMyAdmin.

Начиная со второго запуска консоль также показывает Warning: Table 'notes' already exists, перед которым стоит путь к файлу cursors.py из aiomysql. Это замечание MariaDB для CREATE TABLE IF NOT EXISTS, переданное драйвером. Всё в порядке.

В нашем тесте заглушка Telegram отправила эти команды и получила такие ответы:

ОтправленоОтвет
/note buy milkSaved. Send /notes to see your last five.
/noteWrite your note after the command, for example: /note buy milk
/note и 501 символA note can be up to 500 characters.
/notesСохранённые заметки, сначала новые
/notes от второго пользователяYou have no notes yet.

Много сообщений сразу

aiogram обрабатывает каждое входящее обновление как отдельную задачу, поэтому несколько команд могут выполнять свои запросы одновременно. В нашем тесте 4 команды, пришедшие вместе, обрабатывались примерно по 50 мс каждая. maxsize=5 ограничивает пул 5 подключениями; шестой запрос ждёт свободного, а не открывает новое. После теста у бота было открыто 2 подключения.

Ловушка autocommit

aiomysql открывает подключения с выключенным autocommit. Тогда INSERT остаётся внутри открытой транзакции. Когда подключение возвращается в пул с ещё открытой транзакцией, aiomysql закрывает его, а MariaDB выбрасывает неподтверждённую строку. В нашем тесте строку, вставленную в пул без autocommit=True увидели 0 других подключений. Либо передайте autocommit=True в create_pool, как выше, либо вызывайте await conn.commit() после записей.

Неудачный запрос не останавливает бота

В нашем тесте мы удалили таблицу, пока бот работал, и отправили /notes. aiogram записал ошибку в лог с трассировкой, заканчивающейся на pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), не отправил ответ на эту команду и продолжил работать: когда таблица вернулась, следующий /note был сохранён. Тот, кто отправил команду, ничего не видит, поэтому, если запрос может завершиться ошибкой, перехватывайте её в обработчике и отвечайте коротким сообщением.

Тихая ночь не ломает пул

Наш сервер базы данных закрывает подключение, которое простаивало 8 часов. Бот, который так долго не получает сообщений, например ночью, обнаруживает свои подключения в пуле закрытыми. aiomysql проверяет это, когда обработчик запрашивает подключение, и открывает новое вместо закрытого. Мы проверили это с лимитом простоя, сниженным до 5 секунд: после 7 секунд тишины следующий /note был сохранён без ошибок.

Лимиты, важные для вашего пула

  • 20 одновременных подключений на пользователя базы данных. Считается каждый процесс, который использует тот же логин. Подключение 21 отклоняется с (1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 держит бота намного ниже этого предела.
  • Запрос, который выполняется дольше 120 секунд, останавливается. MariaDB завершает его с ошибкой 1969, Query execution was interrupted (max_statement_time exceeded). В одном из наших прошлых тестов, на аккаунте, настроенном так же, это произошло через 120,03 секунды. Запросы бота обычно занимают миллисекунды, так что это касается только запроса, ушедшего вразнос.
  • 500 МБ на базу. На странице Базы данных видно, сколько занято. При 500 МБ запись останавливается, пока вы не удалите строки.
  • Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.

Ошибки и что они значат

Вот ошибки, которые aiomysql выдал в нашем тесте, записанные так, как они завершают трассировку в консоли (они приходят из PyMySQL, который aiomysql использует под капотом):

  • pymysql.err.OperationalError: (1045, "Access denied for user ..."): неверный DB_USER или неверный DB_PASSWORD. После Сбросить пароль на карточке Подключение, старый пароль сразу перестаёт работать: замените DB_PASSWORD и перезапустите бота.
  • pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): неверный хост или порт, либо подключение извне PlayNHost. База принимает подключения только с серверов PlayNHost и из phpMyAdmin, поэтому то же самое вы получите и на своём компьютере.
  • pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): открыто слишком много подключений, см. лимиты выше. Например, вторая копия бота или пул, открываемый на каждое сообщение.
  • pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): столбец с ID имеет тип INT. Смените его на BIGINT.
  • pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): таблицы нет. Код выше создаёт её при запуске; если вы создали её вручную, проверьте имя.

Если переменной нет в разделе Окружение бот останавливается ещё до подключения, с KeyError: 'DB_HOST' или именем недостающей переменной, потому что код читает каждое значение через os.environ[...]. Добавьте её и перезапустите бота: переменные применяются при следующем запуске.

Неверный токен проявляется после части с базой. С нашим поддельным токеном против настоящего Telegram бот вывел Database ready, затем остановился с aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Скопируйте токен заново у @BotFather и вставьте его на странице Бот, раздел Telegram. Другие ошибки Telegram: Как исправить ошибку 409 Conflict у Telegram-бота.

Сколько памяти он занимает

В нашем тесте работающий бот занял 174 МБ памяти (resident) после ответа на 8 команд, с открытым пулом, в контейнере, ограниченном 512 МБ, как наш самый маленький тариф для ботов. Большая часть приходится на сам aiogram: в нашем прошлом тесте хостинга один aiogram показал 173 МБ (тот бот остановился на отклонённом токене, не успев обработать ни одного сообщения, так что цифры близки, но это не точное сравнение). Установка aiogram и aiomysql через pip с нуля достигла пика в 163 МБ, включая файловый кэш. Это оставляет место для вашего кода на самом маленьком тарифе. Руководство по хостингу сравнивает библиотеки: python-telegram-bot или aiogram на PlayNHost.

То же самое с другой библиотекой

Нужен сервер для вашего бота Telegram с включённой базой данных? Смотреть тарифы для ботов Telegram.

Ваш бот уже у другого хостинга? Перенесите его к нам

ПОСЛЕДНИЕ БЛОГИ