> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oleander.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# MySQL

> Query a live MySQL database from the lake, and import tables into Iceberg with a parallel read once or on a schedule.

Register a MySQL 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](https://oleander.dev/app/settings/lake).

<Tip>
  For production databases, point oleander at a read replica with a dedicated read-only user, and make sure the database firewall allows oleander's egress.
</Tip>

## Setup

1. Open [Settings → Lake](https://oleander.dev/app/settings/lake) and choose **Add MySQL connection**.
2. Paste a `mysql://` URL to fill the fields automatically, or enter them by hand:

| Field | Description |
| - | - |
| **Connection name** | Lowercase letters, numbers, and underscores, e.g. `my_mysql`. This becomes the catalog prefix in SQL. Names are shared across all connection types, so it cannot match an existing Postgres, MongoDB, or other connection. |
| **Host** | Database host, e.g. `db.example.com` |
| **Port** | Defaults to `3306` |
| **Database** | The database to expose |
| **SSL mode** | `DISABLED`, `PREFERRED` (default), `REQUIRED`, `VERIFY_CA`, or `VERIFY_IDENTITY` |
| **Username** | The user oleander connects as |
| **Password** | Stored encrypted and never returned to the browser |

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:

```sql theme={null}
CREATE USER 'oleander_reader'@'%' IDENTIFIED BY '<choose a password>';
GRANT SELECT, SHOW VIEW ON `app_production`.* TO 'oleander_reader'@'%';
```

## Querying live

A connection is scoped to one database, so tables are reachable as `connection_name.database.table`:

```sql theme={null}
-- Read straight from MySQL
SELECT id, email, created_at
FROM my_mysql.app_production.users
WHERE created_at >= '2026-01-01'
LIMIT 100;

-- Join MySQL against your Iceberg lake
SELECT u.email, count(*) AS events
FROM my_mysql.app_production.users u
JOIN oleander.default.events e ON e.user_id = u.id
GROUP BY 1;
```

In the lake tree, column lists come from `information_schema` with primary key columns marked.

<Note>
  **A MySQL table selects DuckDB.** DuckDB is the only engine that attaches external connections, so the [query router](/platform/query-routing/overview) routes any query naming a connection table there 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.
</Note>

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](#importing-into-iceberg) 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 read is split into parallel ranges over the table's integer primary key, with the partition count following table size. A table without an integer primary key is read on a single connection.
* `DATETIME` values keep their wall-clock time, and zero dates (`0000-00-00`) are read as `NULL`.

<Note>
  Unlike [Postgres imports](/connections/postgres#importing-into-iceberg), MySQL cannot share one snapshot across connections, so a parallel import of a live, changing table is not guaranteed to be consistent across partitions. Import from a replica, or during a quiet period, when that matters.
</Note>

The destination defaults to `oleander.default.<table>` - the sanitized source table name, with no connection prefix. `OVERWRITE` replaces it, `APPEND` adds to it.

<Tip>
  For large tables, prefer more executors (4-8) over bigger machines. Each executor core opens one connection to the source database.
</Tip>

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. Schedules run **hourly** or **daily**, and are managed the same way as [Postgres syncs](/connections/postgres#table-syncing): under **Scheduled syncs** in [Settings → Lake](https://oleander.dev/app/settings/lake), and under **Transfers** in the lake's left panel.

<Note>
  **Scheduled syncs are incremental** where the table allows it. After the first full import, each run picks up new and changed rows rather than recopying the table.
</Note>

What a run does depends on the table's keys, and the reason is written to the run's plan log:

| Table has | Each run |
| - | - |
| A timestamp cursor column and a primary key | Merges rows changed since the last run |
| An integer primary key only | Appends rows with a key above the last one seen |
| Neither | Falls back to a full refresh |

Every sync run is a normal oleander run: it appears in run history, emits lineage with the MySQL table as the input and the Iceberg table as the output, and carries cost like any other job.

## From an agent

Two tools cover MySQL over [MCP](/mcp/introduction):

| Tool | What it does |
| - | - |
| `mysql_connections_list` | List registered connections - name, host, port, database, username, SSL mode. Credentials are never returned. |
| `mysql_tables_import` | Import a table into Iceberg with the partitioned, parallel read described above |

For ad hoc reads of live data an agent uses `query_run` against `connection.database.table` instead.

## Permissions

MySQL connections are an IAM resource type. Registering one requires **Create** on **MySQL connections → All MySQL connections**. Browsing a connection's tables in the lake tree and importing from it require **Describe** on that specific connection, and scheduled syncs check the same permission for the principal that runs them. See [Resources and actions](/iam/resources-and-actions#connections).
