DEV Community

qing
qing

Posted on

3 Faster PostgreSQL with asyncpg

Python PostgreSQL with asyncpg: Async Database Operations

asyncpg is the fastest PostgreSQL driver for Python — pure asyncio, no thread overhead, and up to 3× faster than psycopg2 on typical workloads. It is the go-to choice for any async Python backend.

Installation

pip install asyncpg
# PostgreSQL server must already be running
Enter fullscreen mode Exit fullscreen mode

Connect and Create a Pool

import asyncio
import asyncpg
from datetime import datetime

DATABASE_URL = "postgresql://user:password@localhost:5432/mydb"

async def create_pool() -> asyncpg.Pool:
    pool = await asyncpg.create_pool(
        DATABASE_URL,
        min_size=2,
        max_size=10,
        command_timeout=30,
        server_settings={"application_name": "myapp"},
    )
    print("Pool created.")
    return pool
Enter fullscreen mode Exit fullscreen mode

Schema Setup

CREATE_TABLES = """
CREATE TABLE IF NOT EXISTS users (
    id         BIGSERIAL PRIMARY KEY,
    username   TEXT      NOT NULL UNIQUE,
    email      TEXT      NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS posts (
    id         BIGSERIAL PRIMARY KEY,
    user_id    BIGINT    NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title      TEXT      NOT NULL,
    body       TEXT      NOT NULL DEFAULT '',
    published  BOOLEAN   NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_posts_user    ON posts(user_id);
CREATE INDEX IF NOT EXISTS idx_posts_created ON posts(created_at DESC);
"""

async def setup_schema(pool: asyncpg.Pool) -> None:
    async with pool.acquire() as conn:
        await conn.execute(CREATE_TABLES)
    print("Schema ready.")
Enter fullscreen mode Exit fullscreen mode

INSERT — Adding Records

async def create_user(pool: asyncpg.Pool, username: str, email: str) -> int:
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            """
            INSERT INTO users (username, email)
            VALUES ($1, $2)
            ON CONFLICT (username) DO UPDATE SET email = EXCLUDED.email
            RETURNING id, created_at
            """,
            username, email,
        )
        return row["id"]

async def create_post(
    pool: asyncpg.Pool,
    user_id: int,
    title: str,
    body: str,
    published: bool = False,
) -> int:
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            """
            INSERT INTO posts (user_id, title, body, published)
            VALUES ($1, $2, $3, $4)
            RETURNING id
            """,
            user_id, title, body, published,
        )
        return row["id"]

# Bulk insert with copy_records_to_table — extremely fast
async def bulk_insert_posts(pool: asyncpg.Pool, rows: list[tuple]) -> None:
    async with pool.acquire() as conn:
        await conn.copy_records_to_table(
            "posts",
            records=rows,
            columns=["user_id", "title", "body", "published"],
        )
    print(f"Bulk inserted {len(rows)} posts.")
Enter fullscreen mode Exit fullscreen mode

SELECT — Querying Records

async def get_user_posts(
    pool: asyncpg.Pool,
    user_id: int,
    limit: int = 20,
    offset: int = 0,
) -> list[asyncpg.Record]:
    async with pool.acquire() as conn:
        return await conn.fetch(
            """
            SELECT p.id, p.title, p.published, p.created_at, u.username
            FROM   posts p
            JOIN   users u ON u.id = p.user_id
            WHERE  p.user_id = $1
            ORDER  BY p.created_at DESC
            LIMIT  $2 OFFSET $3
            """,
            user_id, limit, offset,
        )

async def search_posts(pool: asyncpg.Pool, keyword: str) -> list[asyncpg.Record]:
    async with pool.acquire() as conn:
        return await conn.fetch(
            """
            SELECT p.title, u.username, p.created_at
            FROM   posts p
            JOIN   users u ON u.id = p.user_id
            WHERE  to_tsvector('english', p.title || ' ' || p.body)
                   @@ plainto_tsquery('english', $1)
            ORDER  BY p.created_at DESC
            LIMIT  50
            """,
            keyword,
        )
Enter fullscreen mode Exit fullscreen mode

UPDATE, DELETE, and Transactions

async def publish_post(pool: asyncpg.Pool, post_id: int) -> bool:
    async with pool.acquire() as conn:
        result = await conn.execute(
            "UPDATE posts SET published = TRUE WHERE id = $1",
            post_id,
        )
        return result == "UPDATE 1"

async def transfer_posts(
    pool: asyncpg.Pool,
    from_user: int,
    to_user: int,
) -> int:
    async with pool.acquire() as conn:
        async with conn.transaction():
            # Both statements run atomically
            result = await conn.execute(
                "UPDATE posts SET user_id = $1 WHERE user_id = $2",
                to_user, from_user,
            )
            count = int(result.split()[-1])
            await conn.execute(
                "INSERT INTO audit_log (action, detail) VALUES ($1, $2)",
                "transfer_posts",
                f"from={from_user} to={to_user} count={count}",
            )
        return count
Enter fullscreen mode Exit fullscreen mode

Full Working Example

async def main():
    pool = await create_pool()
    await setup_schema(pool)

    uid = await create_user(pool, "alice", "alice@example.com")
    print(f"Created user id={uid}")

    pid = await create_post(pool, uid, "asyncpg Deep Dive", "asyncpg is blazing fast...", True)
    print(f"Created post id={pid}")

    posts = await get_user_posts(pool, uid)
    for p in posts:
        print(f"  [{p['id']}] {p['title']} published={p['published']}")

    await pool.close()

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Practical Tips

  • Always use a poolcreate_pool() is cheap once, connection acquisition is fast
  • Use $1, $2, ... placeholders to prevent SQL injection (asyncpg uses positional params)
  • conn.copy_records_to_table() is 10-50× faster than looped INSERT for bulk data
  • async with conn.transaction() nests safely — inner blocks become savepoints
  • fetchrow() returns None if no row matches — always check before accessing fields
  • Enable server_settings={"statement_timeout": "5000"} to protect against runaway queries

Follow me for more Python tips! 🐍


🛠️ Useful resource: **Content Creator Ultimate Bundle (Save 33%)* — $29.99. Check it out on Gumroad!*


📧 Get my FREE Python CheatsheetFollow me on Dev.to and drop a comment below — I'll DM you the cheatsheet directly!

🐍 50+ essential Python patterns, one-liners, and best practices for everyday development. Free for all readers.


喜欢这篇文章?关注获取更多Python自动化内容!

Top comments (0)