Массовая вставка 100K записей в production — частый сценарий ETL-пайплайнов и миграций. Многие выбирают ORM ради удобства, но забывают про overhead сериализации. Я сравнил три async ORM в условиях zero-overhead — минимум преобразований типов, никаких моделей, только сырые dict'ы.
Методология
Стек: PostgreSQL 15, Python 3.11, asyncio. Каждая ORM получает готовые dict'ы. Вставка через bulk_insert, обновление — bulk_update или аналог. Тесты на 100K записей, замеры времени и памяти.
Результаты: время (сек) и память (MB)
* SQLAlchemy async: вставка 2.3с, обновление 3.1с, память ~45 MB
* Tortoise-ORM: вставка 4.1с, обновление 6.7с, память ~82 MB
* GINO: вставка 5.8с, обновление 7.2с, память ~95 MB
SQLAlchemy async лидирует. При использовании
insert().returning() с bulk-операциями и отключённым автокоммитом overhead минимален. Core-level доступ к данным позволяет обойти лишние сериализации.Почему Tortoise отстаёт
Обязательная валидация полей модели при каждом bulk-вызове. Даже с
bulk_create(batch_size=500) каждый объект проходит через __init__ модели. Нет прямого доступа к сырым dict'ам без конвертации. Совет: если нужна простота, используйте Tortoise, но для high-throughput лучше перейти на raw SQL.GINO — самый медленный
Архитектура на SQLAlchemy 1.x. Отсутствие нормального bulk-update вынуждает писать raw SQL. Overhead от asyncpg-шных prepared statements. Типичная ошибка: считать GINO "легковесным" — он legacy, я не рекомендую.
Production-oriented пример: zero-overhead вставка
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
async def bulk_insert_fast(data: list[dict]):
engine = create_async_engine("postgresql+asyncpg://...")
async with engine.begin() as conn:
await conn.execute(
text("""
INSERT INTO users (name, email, created_at)
SELECT unnest(:names::text[]),
unnest(:emails::text[]),
unnest(:created_ats::timestamptz[])
"""),
{
"names": [d["name"] for d in data],
"emails": [d["email"] for d in data],
"created_ats": [d["created_at"] for d in data]
}
)
unnest + массивы — реальный zero-overhead. Модели не загружаются, сериализация не происходит, всё на уровне raw SQL. Для обновления используйте
UPDATE ... FROM с массивами.Вывод: Для высоконагруженных пайплайнов на PostgreSQL SQLAlchemy async — лучший выбор из-за минимального overhead и гибкости core-уровня, тогда как Tortoire удобна в простых проектах, а GINO стоит избегать.