How to Implement Incremental Load with Watermark by updated_at in Python

Load only new or changed rows into SQLite by comparing an updated_at timestamp against a stored watermark, returning counts and the new watermark.

Easy Python 3.9+ Aug 9, 2026 Data pipelines & processing 12 views 0 copies

Python code

56 lines
Python 3.9+
import sqlite3
from datetime import datetime, timedelta


def watermark_incremental_load(db_path, table_name, last_watermark, source_data):
    """Load only rows with updated_at greater than the last watermark."""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # Create table if it doesn't exist
    cursor.execute(f"""
        CREATE TABLE IF NOT EXISTS {table_name} (
            id INTEGER PRIMARY KEY,
            name TEXT,
            updated_at TEXT
        )
    """)

    new_watermark = last_watermark
    loaded_count = 0

    for row in source_data:
        if row["updated_at"] > last_watermark:
            cursor.execute(
                f"INSERT INTO {table_name} (id, name, updated_at) VALUES (?, ?, ?)",
                (row["id"], row["name"], row["updated_at"])
            )
            loaded_count += 1
            # Update watermark to the max timestamp seen
            if row["updated_at"] > new_watermark:
                new_watermark = row["updated_at"]

    conn.commit()
    conn.close()

    return {
        "loaded_count": loaded_count,
        "new_watermark": new_watermark,
        "skipped_count": len(source_data) - loaded_count
    }


if __name__ == "__main__":
    # Test data: some rows are new, some are already loaded
    last_run = "2024-01-15 10:00:00"
    source = [
        {"id": 1, "name": "Alice", "updated_at": "2024-01-15 08:00:00"},
        {"id": 2, "name": "Bob", "updated_at": "2024-01-15 09:30:00"},
        {"id": 3, "name": "Charlie", "updated_at": "2024-01-15 10:30:00"},
        {"id": 4, "name": "Diana", "updated_at": "2024-01-15 11:00:00"}
    ]

    result = watermark_incremental_load(
        "example.db", "users", last_run, source
    )
    print(result)

Output

stdout
{'loaded_count': 2, 'new_watermark': '2024-01-15 11:00:00', 'skipped_count': 2}

How it works

The function compares each row's updated_at string to the persisted last_watermark and inserts only rows newer than it. String comparison works reliably here because ISO-8601 timestamps sort lexicographically in the same order as chronologically. As rows load, the watermark advances to the maximum updated_at seen, so the next run starts from the most recent timestamp and avoids reprocessing. Committing at the end ensures all inserts are durable before the connection closes. Skipped rows are counted simply as the difference between source size and loaded count, which is correct because no upsert logic is applied on conflict.

Common mistakes

  • Comparing timestamps as strings when format is inconsistent (e.g., mixed timezone offsets) — always use ISO-8601 or convert to datetime objects.
  • Using `INSERT` without handling primary key conflicts for rows that were partially loaded in a previous failed run — consider `INSERT OR REPLACE` or upsert logic.
  • Assuming the database table already exists and skipping `CREATE TABLE IF NOT EXISTS`, which breaks first-run scenarios.
  • Not persisting the returned new watermark back to a store, so the next run reverts to the old value and reloads the same rows.

Variations

  1. Store the watermark in a separate metadata table instead of returning it, so state survives across processes.
  2. Use `INSERT OR REPLACE` to overwrite rows that were updated after being previously loaded, turning the load into an incremental upsert.

Real-world use cases

  • Syncing a production OLTP database to a staging warehouse nightly by pulling only records modified since the last sync timestamp.
  • Refreshing a dashboard's materialized view from an event stream, fetching only events newer than the last processed offset.
  • Keeping a search index current by ingesting only documents whose `last_modified` field exceeds the previous crawl's watermark.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Data pipelines & processing

Related tutorials and quizzes for this topic.