MySQL
MySQL is one of the world's most popular open-source relational database management systems.
The MySQL Wrapper allows you to read and write data from MySQL within your Postgres database.
Preparation#
Before you can query MySQL, you need to enable the Wrappers extension and store your credentials in Postgres.
Enable Wrappers#
Make sure the wrappers extension is installed on your database:
create extension if not exists wrappers with schema extensions;Enable the MySQL Wrapper#
Enable the mysql_wrapper FDW:
create foreign data wrapper mysql_wrapper handler mysql_fdw_handler validator mysql_fdw_validator;Store your credentials (optional)#
By default, Postgres stores FDW credentials inside pg_catalog.pg_foreign_server in plain text. Anyone with access to this table will be able to view these credentials. Wrappers is designed to work with Vault, which provides an additional level of security for storing credentials. We recommend using Vault to store your credentials.
-- Save your MySQL connection string in Vault and retrieve the created `key_id`select vault.create_secret( 'mysql://user:password@host:3306/mydb', 'mysql', 'MySQL connection string for Wrappers');Connecting to MySQL#
We need to provide Postgres with the credentials to connect to MySQL, and any additional options. We can do this using the create server command:
create server mysql_server foreign data wrapper mysql_wrapper options ( conn_string_id '<key_ID>' -- The Key ID from above. );The connection string follows the standard MySQL URL format:
mysql://[user[:password]@][host][:port]/databaseSome connection string examples:
mysql://root:secret@localhost:3306/mydbmysql://app_user:password@db.example.com:3306/productionmysql://user:password@127.0.0.1:3306/testdb?ssl-mode=required
Create a schema#
We recommend creating a schema to hold all the foreign tables:
create schema if not exists mysql;Options#
The following options are available when creating MySQL foreign tables:
-
table- Source table name in MySQL, requiredThis can also be a subquery enclosed in parentheses, for example,
table '(select id, name from my_table where active = 1)' -
rowid_column- Primary key column name, optional for data scan, required for data modify
Entities#
Tables#
The MySQL Wrapper supports data reads and writes from MySQL tables.
Operations#
| Object | Select | Insert | Update | Delete | Truncate |
|---|---|---|---|---|---|
| Tables | ✅ | ✅ | ✅ | ✅ | ❌ |
Usage#
create foreign table mysql.my_table ( id bigint, name text) server mysql_server options ( table 'people' );Notes#
- Supports
where,order by,limitand aggregate clause pushdown - Data is streamed row-by-row from MySQL, making it suitable for large result sets
- When using
rowid_column, it must be specified for data modification operations
Query Pushdown Support#
This FDW supports where, order by and limit clause pushdown.
Aggregate Pushdown#
The FDW pushes common aggregate queries down to MySQL so the aggregation runs remotely and only the final result rows are transferred to Postgres. This is much faster than fetching every row and aggregating locally, especially over large tables.
Supported aggregates — count(*), count(col), count(distinct col),
sum(col), avg(col), min(col), max(col).
Supported shapes — scalar aggregates, group by over plain columns, with
or without a where clause. Pushdown also works when the foreign table
option is a sub-query.
-- All of these run as a single aggregate query on MySQL:select count(*) from mysql.my_table;select status, sum(amount) from mysql.my_table group by status;select count(distinct name) from mysql.my_table where id = 1;Cases that are not pushed down — the query still returns the correct result, but the aggregation happens in Postgres after fetching the rows:
- The query has a
havingclause - The aggregate has a
filter (where …)clause - A
distinctmodifier is used on anything other thancount - The aggregate's argument is not a plain column (for example
sum(a + 1)) - A
group byitem is not a plain column (for examplegroup by id + 1) - The aggregate function is not in the list above (for example
stddev,group_concat)
Import Foreign Schema#
This FDW supports import foreign schema to automatically create foreign table definitions by reading the MySQL table structure from information_schema.
The remote_schema maps to a MySQL database name. For example, to import all tables from the MySQL database mydb:
import foreign schema mydb from server mysql_server into mysql;You can limit which tables are imported using limit to or except:
-- Import only specific tablesimport foreign schema mydb limit to (users, orders) from server mysql_server into mysql;-- Import all tables except specific onesimport foreign schema mydb except (tmp_data, archive) from server mysql_server into mysql;An additional option is available for import foreign schema:
strict- If set totrue, the import will fail if any MySQL column type cannot be mapped to a Postgres type. Defaults tofalse(unsupported column types are silently skipped).
import foreign schema mydb from server mysql_server into mysql options (strict 'true');Primary key columns are automatically detected and set as the rowid_column option on the generated foreign table, enabling INSERT, UPDATE, and DELETE operations without any manual configuration.
Supported Data Types#
| Postgres Type | MySQL Type |
|---|---|
| boolean | boolean, bool, tinyint(1) |
| smallint | tinyint (non-boolean), smallint, year |
| integer | mediumint, int, integer |
| bigint | bigint |
| real | float |
| double precision | double, double precision |
| numeric | decimal, numeric |
| text | char, varchar, tinytext, text, mediumtext, longtext, enum, set |
| date | date |
| timestamp | datetime, timestamp |
| time | time |
| jsonb | json |
tinyint(1) is mapped to boolean as it is the conventional MySQL representation for boolean values. All other tinyint variants are mapped to smallint.
decimal and numeric columns with explicit precision and scale (e.g. decimal(12,2)) are imported as numeric(p,s) in Postgres.
import foreign schema always generates text for MySQL string types, but when defining a foreign table manually you may also declare these columns as varchar(n) or char(n); all three Postgres types are read correctly.
Limitations#
This section describes important limitations and considerations when using this FDW:
- Only a subset of MySQL data types are supported; columns with unmapped types are skipped during
import foreign schemaunlessstrictmode is enabled timestampanddatetimevalues are treated as naive timestamps without timezone conversion- MySQL
jsoncolumns are mapped to Postgresjsonb; invalid JSON will cause an error during reads - Materialized views using foreign tables may fail during logical backups
Examples#
Basic example#
This example shows how to query a MySQL table from Postgres.
First, create the source table in MySQL:
-- Run on MySQLcreate table people ( id bigint primary key auto_increment, name varchar(100), email varchar(200));insert into people (name, email) values ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com'), ('Carol', 'carol@example.com');Then create the foreign table in Postgres and query it:
create foreign table mysql.people ( id bigint, name text, email text) server mysql_server options ( table 'people' );select * from mysql.people;Data modification example#
This example demonstrates INSERT, UPDATE, and DELETE on a foreign table. The rowid_column option is required for data modification:
create foreign table mysql.people ( id bigint, name text, email text) server mysql_server options ( table 'people', rowid_column 'id' );-- Insert a new rowinsert into mysql.people (name, email)values ('Dave', 'dave@example.com');-- Update an existing rowupdate mysql.peopleset email = 'alice_new@example.com'where id = 1;-- Delete a rowdelete from mysql.peoplewhere id = 2;Aggregate Query Examples#
These examples assume an orders table in MySQL and a matching foreign
table on Postgres:
-- Run on MySQLcreate table orders ( id bigint primary key auto_increment, user_id bigint not null, amount decimal(12,2) not null, status varchar(50) not null);insert into orders (user_id, amount, status) values (1, 100.00, 'paid'), (1, 50.00, 'paid'), (2, 200.00, 'pending'), (2, 75.00, 'paid'), (3, 300.00, 'paid');-- Foreign table on Postgrescreate foreign table mysql.orders ( id bigint, user_id bigint, amount numeric, status text) server mysql_server options ( table 'orders' );Each query below runs a single aggregate query against MySQL and returns just the result rows:
-- Total order countselect count(*) from mysql.orders;-- Total revenue from paid ordersselect sum(amount) from mysql.orders where status = 'paid';-- Per-user order count and revenueselect user_id, count(*) as orders, sum(amount) as revenuefrom mysql.ordersgroup by user_idorder by user_id;-- Smallest and largest orderselect min(amount), max(amount) from mysql.orders;-- Number of distinct users who placed an orderselect count(distinct user_id) from mysql.orders;-- Average order value per statusselect status, avg(amount) as avg_amountfrom mysql.ordersgroup by status;Import foreign schema example#
This example imports all tables from a MySQL database automatically:
-- Import all tables from the MySQL 'shop' databaseimport foreign schema shop from server mysql_server into mysql;-- The foreign tables are now availableselect * from mysql.products limit 10;select * from mysql.orders where status = 'pending';