spatial-etl-framework

Batch Processing for SQL Operations

Overview

The pipeline now supports batch processing for large SQL operations to prevent long-running transactions that can block PostgreSQL and cause performance issues.

Problem

Previously, mapping operations executed queries like:

INSERT INTO mapping_table (way_id, data, ...)
SELECT ...
FROM ways_base w  -- millions of rows
JOIN LATERAL (...)

This could:

Solution

The new batching system automatically:

  1. Detects large INSERT INTO … SELECT queries with JOINs
  2. Splits them into smaller batches (default: 10,000 rows per batch)
  3. Commits each batch separately to release locks
  4. Provides progress logging for visibility
  5. Reduces PostgreSQL stress by limiting transaction size

Configuration

Global Configuration

Add to your config.yaml under database:

database:
  # ... existing config ...
  performance:
    enable_batching: true           # Enable batch processing
    default_batch_size: 10000       # Default rows per batch
    batch_commit_interval: 5000     # Commit every N rows (0 = per batch only)

Per-Datasource Configuration

Override batch size for specific datasources:

datasources:
  - name: "my_heavy_datasource"
    # ... other config ...
    mapping:
      enable: true
      batch_size: 5000  # Override global default for this datasource
      strategy:
        type: sql_template
      config:
        sql: |
          INSERT INTO ...
          # Your complex query here

How It Works

Automatic Detection

The system detects queries that are suitable for batching:

Batching Process

  1. Extract base table from the query (e.g., ways_base)
  2. Count total rows to process
  3. Calculate number of batches needed
  4. Execute query in batches using LIMIT/OFFSET
  5. Commit after each batch
  6. Log progress throughout

Example Execution

Before (single query):

INFO: Starting SQL execution...
INFO: SQL execution completed.  [10 minutes later]

After (batched):

INFO: Using batched execution for Mapping with batch size: 10000
INFO: Total rows to process: 125000, batch size: 10000
INFO: Processing in 13 batches...
INFO: Processing batch 1/13 (rows 1-10000)
INFO: Batch 1 completed in 45.23s (10000 rows affected)
INFO: Processing batch 2/13 (rows 10001-20000)
INFO: Batch 2 completed in 43.12s (10000 rows affected)
...
INFO: All 13 batches completed successfully

Benefits

Performance

Visibility

Reliability

Query Transformation

The system automatically transforms queries:

Original:

INSERT INTO trial.mapping_table (way_id, data)
SELECT w.id, e.data
FROM trial.ways_base w
JOIN enrichment e ON ...

Batched (Batch 1):

INSERT INTO trial.mapping_table (way_id, data)
SELECT w.id, e.data
FROM (SELECT * FROM trial.ways_base LIMIT 10000 OFFSET 0) w
JOIN enrichment e ON ...

Batched (Batch 2):

INSERT INTO trial.mapping_table (way_id, data)
SELECT w.id, e.data
FROM (SELECT * FROM trial.ways_base LIMIT 10000 OFFSET 10000) w
JOIN enrichment e ON ...

When to Adjust Batch Size

Increase Batch Size (20000+)

Decrease Batch Size (5000 or less)

Monitoring

Check Batch Performance

Look for log messages:

INFO: Batch 1 completed in 45.23s (10000 rows affected)
INFO: Batch 2 completed in 43.12s (10000 rows affected)

If batches slow down over time:

Database Monitoring

While batched queries run:

-- Check active queries
SELECT pid, state, query_start, query
FROM pg_stat_activity
WHERE state = 'active';

-- Check lock waits
SELECT * FROM pg_locks WHERE NOT granted;

Disabling Batching

To disable batching globally:

database:
  performance:
    enable_batching: false

To disable for specific datasource:

mapping:
  batch_size: 0  # 0 or null disables batching

Or remove the performance config entirely to use original behavior.

Advanced Configuration

Custom Batch Logic

For queries that need custom batching logic, you can call call_sql_batched() directly:

# In your mapper class
def mapping_db_query(self):
    # Build your query
    query = "INSERT INTO ... SELECT ..."

    # Don't return it - execute with custom batch size
    self.db.call_sql_batched(
        query,
        batch_size=5000,
        base_id_column="id",  # Column to use for ordering
        raise_on_error=True
    )
    return None  # Already executed

Troubleshooting

“Query not suitable for batching”

“Could not extract base table from query”

Batches are too slow

Memory issues during batching

Future Enhancements

Potential improvements: