Skip to content
Back to skills

Psycopg2 Batch Insert Optimization

ASecurity

Optimize slow PostgreSQL inserts in Python using psycopg2. Use when: (1) Row-by-row inserts are taking too long over network, (2) executemany() isn't providing speedup, (3) Migrating large datasets to PostgreSQL, (4) Network latency making individual INSERT statements impractical. The key is using execute_values() from psycopg2.extras instead of executemany() or individual execute() calls.

  • 265 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
databasespythonsqldatabasedocumentation

Works with

  • claude code
  • cursor

Security analysis

A100/100

Scanned September 3, 2026

npx -y skills add divinevideo/divine-mobile --skill psycopg2-batch-insert-optimization --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Psycopg2 Batch Insert Optimization?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Psycopg2 Batch Insert Optimization
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-psycopg2-batch-insert-optimization-divine-mobile/badge)](https://www.skillsdirectory.com/skills/divinevideo-psycopg2-batch-insert-optimization-divine-mobile)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: psycopg2-batch-insert-optimization
description: |
  Optimize slow PostgreSQL inserts in Python using psycopg2. Use when: (1) Row-by-row
  inserts are taking too long over network, (2) executemany() isn't providing speedup,
  (3) Migrating large datasets to PostgreSQL, (4) Network latency making individual
  INSERT statements impractical. The key is using execute_values() from psycopg2.extras
  instead of executemany() or individual execute() calls.
author: Claude Code
version: 1.0.0
date: 2025-01-20
---

# psycopg2 Batch Insert Optimization

## Problem
When inserting thousands of rows into PostgreSQL over a network connection, row-by-row
inserts are extremely slow. Each INSERT requires a round-trip, and with network latency
of ~50-100ms, inserting 10,000 rows takes 10+ minutes.

The naive approach of using `cursor.executemany()` doesn't help much—it still sends
individual statements.

## Context / Trigger Conditions
- Inserting >100 rows into PostgreSQL via psycopg2
- Each insert taking ~1 second or more
- Network latency to database (especially Cloud SQL, RDS, remote databases)
- Migration scripts running for hours
- `executemany()` not providing expected speedup

## Solution

Use `execute_values()` from `psycopg2.extras`:

```python
from psycopg2.extras import execute_values

# Instead of this (SLOW):
for row in data:
    cursor.execute("INSERT INTO table (a, b, c) VALUES (%s, %s, %s)", row)

# Or this (STILL SLOW):
cursor.executemany("INSERT INTO table (a, b, c) VALUES (%s, %s, %s)", data)

# Use this (FAST):
execute_values(cursor, """
    INSERT INTO table (a, b, c)
    VALUES %s
    ON CONFLICT (id) DO NOTHING
""", data, page_size=500)
conn.commit()
```

### Key Parameters:
- `page_size`: Number of rows per batch (default 100, try 500-1000)
- The `VALUES %s` placeholder is replaced with multiple value tuples

### For UPSERT operations:
```python
execute_values(cursor, """
    INSERT INTO users (user_id, username, email)
    VALUES %s
    ON CONFLICT (user_id) DO UPDATE SET
        username = EXCLUDED.username,
        email = COALESCE(EXCLUDED.email, users.email)
""", user_data, page_size=500)
```

### Progress Monitoring for Long Migrations:
```python
import sys
sys.stdout.reconfigure(line_buffering=True)  # Force unbuffered output

BATCH_SIZE = 500
for i in range(0, len(data), BATCH_SIZE):
    batch = data[i:i+BATCH_SIZE]
    execute_values(cursor, query, batch)
    conn.commit()
    print(f"Processed {min(i+BATCH_SIZE, len(data))}/{len(data)} rows...")
```

## Verification
- Migration that previously took hours completes in minutes
- You can see batches being processed in real-time with progress output
- Check row counts after: `SELECT COUNT(*) FROM table`

## Example

Real-world migration of 9,563 users from SQLite to PostgreSQL:

```python
from psycopg2.extras import execute_values
import sys

sys.stdout.reconfigure(line_buffering=True)
BATCH_SIZE = 500

# Fetch from SQLite
sqlite_cur.execute('SELECT user_id, username, avatar_url, verified FROM users')
rows = sqlite_cur.fetchall()
data = [(r['user_id'], r['username'], r['avatar_url'], bool(r['verified']))
        for r in rows]

# Batch insert to PostgreSQL
for i in range(0, len(data), BATCH_SIZE):
    batch = data[i:i+BATCH_SIZE]
    execute_values(pg_cur, '''
        INSERT INTO users (user_id, username, avatar_url, verified)
        VALUES %s
        ON CONFLICT (user_id) DO UPDATE SET
            username = COALESCE(EXCLUDED.username, users.username),
            avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url)
    ''', batch)
    pg_conn.commit()
    print(f"Processed {min(i+BATCH_SIZE, len(data))}/{len(data)} users...")
```

**Result**: 9,563 users migrated in ~20 seconds instead of ~2.5 hours.

## Notes

- `execute_values()` constructs a single INSERT with multiple VALUES, drastically
  reducing round-trips
- `executemany()` is deceptively slow—it still sends individual statements
- For very large datasets (>100k rows), consider `COPY` command or `copy_expert()`
- The `page_size` parameter controls memory usage vs. batch efficiency
- Always commit after each batch for long migrations (allows progress tracking and
  partial recovery)

### SQLite to PostgreSQL Syntax Differences:
When migrating, also watch for these SQL differences:
- `INSERT OR IGNORE` → `ON CONFLICT DO NOTHING`
- `INSERT OR REPLACE` → `ON CONFLICT DO UPDATE SET ...`
- `MAX(a, b)` (SQLite) → `GREATEST(a, b)` (PostgreSQL)
- `MIN(a, b)` (SQLite) → `LEAST(a, b)` (PostgreSQL)
- `?` placeholders → `%s` placeholders
- `AUTOINCREMENT` → `SERIAL` or `GENERATED ALWAYS AS IDENTITY`

## References
- [psycopg2 execute_values documentation](https://www.psycopg.org/docs/extras.html#psycopg2.extras.execute_values)
- [PostgreSQL COPY for bulk loading](https://www.postgresql.org/docs/current/sql-copy.html)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…