Skip to main content
Register a Postgres database and it becomes a live, read-only catalog in the lake. Query it directly for ad hoc work, or import tables into Iceberg when you want a snapshot you can run large jobs against. Connections are managed in Settings → Lake.
For production databases, point oleander at a read replica with a dedicated read-only user.

Setup

  1. Open Settings → Lake and choose Add Postgres connection.
  2. Paste a connection string to fill the fields automatically, or enter them by hand:
The dialog generates the SQL for a read-only user if you want to create one - copy it and run it against your database before saving.

Querying live

Tables are reachable as connection_name.schema.table:
A Postgres table selects DuckDB. DuckDB is the only engine that attaches external connections, so the query router routes any query naming a connection table here before it considers input size - leave engine on auto. Asking for Bloom, Polars, or Spark on one of these queries returns an engine capability error rather than rerouting silently.
Live reads run against your database, so treat them the way you would treat any query against production: filter, limit, and prefer a replica. Once a table is imported it lives in Iceberg and the router is free to send it to any engine.

Importing into Iceberg

For anything larger than an ad hoc read, import the table. The import runs as a Spark JDBC read and lands the data as an Iceberg table in your lake, where every engine can reach it. The import plans itself from Postgres catalog metadata:
  • Partition count follows table size.
  • Partition ranges come from the integer primary key, balanced by histogram, or from physical ctid chunks when there is no suitable key.
  • Every partition reads from one exported MVCC snapshot, so the copy is consistent even against a live, changing database.
Reads run with a bounded number of connections against the source. It is suitable for very large tables. The destination defaults to oleander.default.<connection>_<table>. OVERWRITE replaces it, APPEND adds to it.
For large tables, prefer more executors (4-8) over bigger machines. Each executor core opens one connection to the source database.
An import does not return rows. It submits a job and returns a run_id you can monitor like any other run, and once it completes you can sample the destination with a normal query.

Table syncing

A one-off import gives you a snapshot. Put the same import on a schedule and the Iceberg copy tracks the source instead.
Scheduled syncs are incremental. After the first full import, each run picks up new and changed rows only rather than recopying the table, so a sync over a large table costs a fraction of the initial import.
Schedules run hourly or daily. Each one is a connection.source_table → destination_table pair, listed under Scheduled syncs in Settings → Lake with its cadence and next run time, where it can be paused or removed. Every sync run is a normal oleander run: it appears in run history, emits lineage with the Postgres table as the input and the Iceberg table as the output, and carries cost like any other job. A sync that starts failing or silently stops moving rows shows up in observability the same way a pipeline regression does. Pick a cadence against how fresh the lake copy needs to be, not how often the source changes - the incremental read is cheap, but each run is still a Spark job against your database.

From an agent

Two tools cover Postgres over MCP: For ad hoc reads of live data an agent uses query_run against connection.schema.table instead.