Асинхронная работа с PostgreSQL в Python через asyncpg

10 сентября 2026
Автор

Антон Ларичев

Что такое asyncpg и зачем он нужен

asyncpg — это высокопроизводительный асинхронный драйвер для работы с PostgreSQL в Python, построенный поверх asyncio. В отличие от синхронных драйверов вроде psycopg2, asyncpg не блокирует поток выполнения во время ожидания ответа от базы данных, что критически важно для высоконагруженных веб-приложений и сервисов.

Ключевые преимущества asyncpg:

  • Работает напрямую с бинарным протоколом PostgreSQL, минуя лишние слои абстракции
  • Скорость выполнения запросов в 3–5 раз выше, чем у psycopg2 в асинхронном режиме
  • Нативная поддержка Python-типов: автоматическое преобразование UUID, JSON, массивов, перечислений
  • Встроенный пул соединений без дополнительных зависимостей
  • Полная поддержка prepared statements

asyncpg отлично подходит для работы с FastAPI, aiohttp и любым другим async-фреймворком.

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка

pip install asyncpg

Для разработки удобно использовать виртуальное окружение:

python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate     # Windows
pip install asyncpg

Базовое подключение к базе данных

Самый простой способ подключиться — создать единственное соединение через asyncpg.connect(). Этот подход подходит для скриптов и утилит, но не для серверных приложений.

import asyncio
import asyncpg

async def main():
    conn = await asyncpg.connect(
        host="localhost",
        port=5432,
        user="postgres",
        password="secret",
        database="mydb",
    )

    try:
        version = await conn.fetchval("SELECT version()")
        print(f"PostgreSQL: {version}")
    finally:
        await conn.close()

asyncio.run(main())

Alternative DSN-формат:

conn = await asyncpg.connect("postgresql://postgres:secret@localhost:5432/mydb")

Пул соединений

Для production-приложений всегда используйте пул соединений. Создание нового TCP-соединения к PostgreSQL занимает десятки миллисекунд — пул позволяет переиспользовать уже установленные соединения.

import asyncio
import asyncpg

async def main():
    pool = await asyncpg.create_pool(
        dsn="postgresql://postgres:secret@localhost:5432/mydb",
        min_size=5,   # минимальное число соединений в пуле
        max_size=20,  # максимальное число соединений
    )

    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT id, name FROM users LIMIT 10")
        for row in rows:
            print(row["id"], row["name"])

    await pool.close()

asyncio.run(main())

pool.acquire() возвращает соединение из пула; по выходу из контекстного менеджера соединение автоматически возвращается обратно.

Пул в FastAPI-приложении

Пул удобно хранить как атрибут приложения и инициализировать при старте:

from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
import asyncpg

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.pool = await asyncpg.create_pool(
        dsn="postgresql://postgres:secret@localhost:5432/mydb",
        min_size=5,
        max_size=20,
    )
    yield
    await app.state.pool.close()

app = FastAPI(lifespan=lifespan)

@app.get("/users")
async def get_users(request: Request):
    async with request.app.state.pool.acquire() as conn:
        rows = await conn.fetch("SELECT id, name, email FROM users")
        return [dict(row) for row in rows]

Основные методы выполнения запросов

asyncpg предоставляет несколько методов в зависимости от того, что нужно вернуть.

fetch — список строк

rows = await conn.fetch(
    "SELECT id, name, email FROM users WHERE active = $1",
    True,
)
# rows — список объектов Record
for row in rows:
    print(row["id"], row["name"])

fetchrow — одна строка

user = await conn.fetchrow(
    "SELECT id, name, email FROM users WHERE id = $1",
    42,
)
if user:
    print(user["name"])  # доступ по имени колонки
    print(user[0])       # доступ по индексу

fetchval — скалярное значение

count = await conn.fetchval("SELECT COUNT(*) FROM users WHERE active = $1", True)
print(f"Активных пользователей: {count}")

execute — INSERT, UPDATE, DELETE

status = await conn.execute(
    "UPDATE users SET last_login = NOW() WHERE id = $1",
    42,
)
# status содержит строку вроде "UPDATE 1"
print(status)

executemany — пакетная вставка

users_data = [
    ("Alice", "alice@example.com"),
    ("Bob", "bob@example.com"),
    ("Charlie", "charlie@example.com"),
]

await conn.executemany(
    "INSERT INTO users (name, email) VALUES ($1, $2)",
    users_data,
)

Параметры всегда передаются позиционно через $1, $2, ... — это защищает от SQL-инъекций.

Работа с транзакциями

Базовая транзакция

async with conn.transaction():
    await conn.execute(
        "INSERT INTO accounts (user_id, balance) VALUES ($1, $2)",
        1, 1000.0
    )
    await conn.execute(
        "INSERT INTO accounts (user_id, balance) VALUES ($1, $2)",
        2, 500.0
    )
# При выходе из блока без исключения — COMMIT
# При исключении — автоматический ROLLBACK

Перевод средств между счетами

Практический пример с атомарной операцией:

async def transfer_funds(
    conn: asyncpg.Connection,
    from_account: int,
    to_account: int,
    amount: float,
) -> None:
    async with conn.transaction():
        sender_balance = await conn.fetchval(
            "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
            from_account,
        )

        if sender_balance is None:
            raise ValueError(f"Счёт {from_account} не найден")
        if sender_balance < amount:
            raise ValueError("Недостаточно средств")

        await conn.execute(
            "UPDATE accounts SET balance = balance - $1 WHERE id = $2",
            amount, from_account,
        )
        await conn.execute(
            "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
            amount, to_account,
        )

Точки сохранения (Savepoints)

async with conn.transaction():
    await conn.execute("INSERT INTO logs (event) VALUES ($1)", "start")

    async with conn.transaction():  # создаёт savepoint
        try:
            await conn.execute(
                "INSERT INTO orders (user_id, total) VALUES ($1, $2)",
                user_id, total
            )
        except asyncpg.UniqueViolationError:
            pass  # откатывается только до savepoint, внешняя транзакция продолжается

    await conn.execute("INSERT INTO logs (event) VALUES ($1)", "end")

Prepared Statements

Prepared statements позволяют PostgreSQL разобрать и спланировать запрос один раз, а затем выполнять его многократно без повторного парсинга. Это даёт прирост производительности при частых одинаковых запросах.

async def batch_insert_users(conn: asyncpg.Connection, users: list[dict]) -> None:
    stmt = await conn.prepare(
        "INSERT INTO users (name, email, created_at) VALUES ($1, $2, NOW())"
    )

    for user in users:
        await stmt.execute(user["name"], user["email"])

Для массовых выборок:

async def get_user_by_email(conn: asyncpg.Connection, email: str):
    stmt = await conn.prepare(
        "SELECT id, name, email, created_at FROM users WHERE email = $1"
    )
    return await stmt.fetchrow(email)

Работа со специальными типами PostgreSQL

asyncpg автоматически конвертирует многие PostgreSQL-типы в Python-объекты.

UUID

import uuid

user_id = uuid.uuid4()
await conn.execute(
    "INSERT INTO users (id, name) VALUES ($1, $2)",
    user_id, "Alice"  # uuid.UUID передаётся напрямую
)

row = await conn.fetchrow("SELECT id FROM users WHERE name = $1", "Alice")
print(type(row["id"]))  # <class 'uuid.UUID'>

JSONB

import json

metadata = {"role": "admin", "permissions": ["read", "write"]}

await conn.execute(
    "INSERT INTO users (name, metadata) VALUES ($1, $2)",
    "Alice", json.dumps(metadata)  # передаём как строку для JSONB
)

row = await conn.fetchrow("SELECT metadata FROM users WHERE name = $1", "Alice")
data = json.loads(row["metadata"])
print(data["role"])  # admin

Массивы

tags = ["python", "asyncio", "postgresql"]
await conn.execute(
    "INSERT INTO articles (title, tags) VALUES ($1, $2)",
    "asyncpg guide", tags  # список Python — в массив PostgreSQL
)

row = await conn.fetchrow(
    "SELECT tags FROM articles WHERE title = $1",
    "asyncpg guide"
)
print(row["tags"])  # ['python', 'asyncio', 'postgresql']

Обработка ошибок

asyncpg предоставляет иерархию исключений, соответствующую кодам ошибок PostgreSQL.

import asyncpg

async def create_user(conn: asyncpg.Connection, name: str, email: str) -> int | None:
    try:
        user_id = await conn.fetchval(
            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
            name, email,
        )
        return user_id
    except asyncpg.UniqueViolationError:
        # нарушение уникального ограничения (например, email уже существует)
        print(f"Пользователь с email {email} уже существует")
        return None
    except asyncpg.ForeignKeyViolationError as e:
        print(f"Нарушение внешнего ключа: {e}")
        return None
    except asyncpg.NotNullViolationError as e:
        print(f"Нарушение NOT NULL: {e}")
        return None
    except asyncpg.PostgresError as e:
        # базовый класс для всех PostgreSQL-ошибок
        print(f"Ошибка PostgreSQL [{e.sqlstate}]: {e}")
        return None

Наиболее часто используемые исключения:

Исключение Описание
UniqueViolationError Нарушение уникального индекса
ForeignKeyViolationError Нарушение внешнего ключа
NotNullViolationError NULL в NOT NULL поле
CheckViolationError Нарушение CHECK-ограничения
UndefinedTableError Таблица не существует
TooManyConnectionsError Превышен лимит соединений
ConnectionDoesNotExistError Соединение разорвано

Полный практический пример

Реализуем простой репозиторий пользователей с полным набором CRUD-операций.

import asyncio
import asyncpg
from dataclasses import dataclass
from datetime import datetime


@dataclass
class User:
    id: int
    name: str
    email: str
    active: bool
    created_at: datetime


class UserRepository:
    def __init__(self, pool: asyncpg.Pool) -> None:
        self._pool = pool

    async def create_table(self) -> None:
        async with self._pool.acquire() as conn:
            await conn.execute("""
                CREATE TABLE IF NOT EXISTS users (
                    id        SERIAL PRIMARY KEY,
                    name      TEXT NOT NULL,
                    email     TEXT NOT NULL UNIQUE,
                    active    BOOLEAN NOT NULL DEFAULT TRUE,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
                )
            """)

    async def create(self, name: str, email: str) -> User:
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(
                """
                INSERT INTO users (name, email)
                VALUES ($1, $2)
                RETURNING id, name, email, active, created_at
                """,
                name, email,
            )
        return User(**row)

    async def get_by_id(self, user_id: int) -> User | None:
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(
                "SELECT id, name, email, active, created_at FROM users WHERE id = $1",
                user_id,
            )
        return User(**row) if row else None

    async def get_active_users(self, limit: int = 100) -> list[User]:
        async with self._pool.acquire() as conn:
            rows = await conn.fetch(
                """
                SELECT id, name, email, active, created_at
                FROM users
                WHERE active = TRUE
                ORDER BY created_at DESC
                LIMIT $1
                """,
                limit,
            )
        return [User(**row) for row in rows]

    async def deactivate(self, user_id: int) -> bool:
        async with self._pool.acquire() as conn:
            status = await conn.execute(
                "UPDATE users SET active = FALSE WHERE id = $1 AND active = TRUE",
                user_id,
            )
        return status == "UPDATE 1"

    async def delete(self, user_id: int) -> bool:
        async with self._pool.acquire() as conn:
            status = await conn.execute(
                "DELETE FROM users WHERE id = $1",
                user_id,
            )
        return status == "DELETE 1"


async def main():
    pool = await asyncpg.create_pool(
        dsn="postgresql://postgres:secret@localhost:5432/mydb",
        min_size=2,
        max_size=10,
    )

    repo = UserRepository(pool)
    await repo.create_table()

    # Создаём пользователей параллельно
    alice, bob = await asyncio.gather(
        repo.create("Alice", "alice@example.com"),
        repo.create("Bob", "bob@example.com"),
    )
    print(f"Созданы: {alice.name} (id={alice.id}), {bob.name} (id={bob.id})")

    # Получаем список активных
    users = await repo.get_active_users()
    print(f"Активных пользователей: {len(users)}")

    # Деактивируем Bob
    deactivated = await repo.deactivate(bob.id)
    print(f"Bob деактивирован: {deactivated}")

    await pool.close()


asyncio.run(main())

Настройка соединения и таймауты

Для production-окружения важно настроить таймауты и параметры соединения:

pool = await asyncpg.create_pool(
    dsn="postgresql://postgres:secret@localhost:5432/mydb",
    min_size=5,
    max_size=20,
    max_inactive_connection_lifetime=300.0,  # закрывать idle-соединения через 5 минут
    command_timeout=30.0,  # таймаут на выполнение запроса — 30 секунд
    server_settings={
        "application_name": "myapp",
        "statement_timeout": "30000",  # ms, через SET
    },
)

Таймаут на уровне отдельного запроса:

try:
    row = await conn.fetchrow(
        "SELECT * FROM heavy_query()",
        timeout=5.0,  # максимум 5 секунд на этот запрос
    )
except asyncio.TimeoutError:
    print("Запрос выполнялся слишком долго")

Итоги

asyncpg — зрелый и производительный инструмент для работы с PostgreSQL в async Python-приложениях. Ключевые принципы при работе с ним:

  • Используйте пул соединений (create_pool) в серверных приложениях — не создавайте отдельное соединение на каждый запрос
  • Всегда передавайте параметры запроса через $1, $2 — никогда не форматируйте SQL вручную
  • Оборачивайте связанные изменения в conn.transaction() для атомарности
  • Обрабатывайте специфические исключения asyncpg, а не общий Exception
  • Используйте asyncio.gather() для параллельного выполнения независимых запросов

Для работы с более высокоуровневым ORM поверх asyncpg рассмотрите SQLAlchemy async (использует asyncpg как backend) или SQLModel.


Хотите освоить Python для backend-разработки с нуля до профессионального уровня? Изучите полный курс на PurpleSchool: https://purpleschool.ru/course/python?utm_source=knowledgebase&utm_medium=text&utm_campaign=asyncpg-postgresql

Стрелочка влевоФоновые задачи с Celery в PythonPython asyncio.TaskGroupСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со списком значений в PythonРабота со столбцами в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий