Learn with DataTug · English

How to join PostgreSQL and MySQL

To join PostgreSQL and MySQL is to combine rows from those two engines in one result. PostgreSQL is a relational database engine. MySQL is a different relational database engine. SQL (Structured Query Language) is the language each engine runs. A join combines rows from two tables that share a key. A cross-database join is a join whose tables are not stored by the same engine. A foreign data wrapper (FDW) is an extension that lets one engine read tables on another server. ETL (extract, transform, and load) copies data into one place before you query it. The command-line interface (CLI) is the datatug program in a terminal. TugQL is DataTug's query language for sources, joins, filters and columns. JSON (JavaScript Object Notation) is one text format the query command can print.

A plain SQL JOIN cannot see two servers

A SQL statement is sent to one engine. That engine plans the join from tables in its own catalog. PostgreSQL and MySQL are separate servers, each with its own catalog, even when they sit on the same machine. Neither server is given the other server's tables by the word JOIN. Two open clients do not merge into one statement either. Putting both host names in one connection address still selects one engine.

The same boundary is in the CLI reference for datatug query run. The --db flag is one database address, or env:NAME when that name holds a PostgreSQL, MySQL, Oracle or SQL Server address in an environment variable. The -f flag expects a TugQL document. Raw SQL text fails with invalid TugQL. One --db address cannot join tables across PostgreSQL and MySQL.

Foreign data wrappers, a copy, or a tool

People usually bridge the gap in one of three ways.

postgres_fdw is PostgreSQL's own extension for reading another PostgreSQL server. mysql_fdw is a separate PostgreSQL extension, not a feature of MySQL, that lets PostgreSQL read a MySQL table. You declare a foreign server, a user mapping and foreign tables. The JOIN then runs in PostgreSQL. The MySQL server still does not plan that JOIN. Privileges and network access stay yours to set. This page does not repeat those extension commands.

ETL is the copy. Export each side, or load both into one database, then run an ordinary JOIN on the copy. The copy is not the live rows, and someone has to refresh it when either source changes.

The third way is a tool that reads each side itself and combines the rows. That is a cross-database join done outside either engine. It is the path the rest of this page describes for DataTug.

Query PostgreSQL and MySQL together from the CLI

To query PostgreSQL and MySQL together, both databases have to be sources in one DataTug project. The supported databases catalogue lists PostgreSQL and MySQL as command-line sources for read-only SQL and TugQL, with schema scan implemented. The catalogue's column for the browser and hosted app says "Not verified by this catalogue" on each row. A server read on those engines requires unrestricted access, which the CLI calls --no-policies. Do not assume row or column policy enforcement. --no-policies skips DataTug's access policy. It does not take write permission away from the database login. Use a login that can only read.

Cross-source TugQL joins are available in the CLI. One TugQL query can name more than one source. The browser join is a separate status on that page, and it is in development. A saved query is a file in the project. The CLI reference runs a saved query with --project and --query. Add --env when the project has more than one environment. The reference says the default is the project's only environment.

PostgREST and libSQL are different catalogue rows. The PostgREST note says joins are unavailable, and that the source is not the PostgreSQL wire protocol. The libSQL note also says joins are unavailable. Neither row is the PostgreSQL or MySQL server read this page is about.

Example: customers in PostgreSQL, orders in MySQL

This is an example, not a recorded result. Suppose customers live in PostgreSQL and orders live in MySQL. In the project the PostgreSQL database is named shop, with a customers table and an id column. The MySQL database is named warehouse, with an orders table and a customer_id column. The shared key is that customer identifier. The names are made up. DataTug has not published a row count for this pair.

The join below uses the same keys as the recorded query in the next section: database, name, alias, joins and on. database is the name in the project, not the word PostgreSQL or MySQL. This fragment is the join only. It is not a complete saved query, and it is not a command to paste.

# Example only. Not a recorded PostgreSQL-to-MySQL query.
from:
  database: shop
  name: customers
  alias: c
  joins:
    - from:
        database: warehouse
        name: orders
        alias: o
      on:
        - left: {field: id, source: c}
          op: '=='
          right: {field: customer_id, source: o}

For a real project you save a full TugQL query, then run it. PROJECT and QUERY_ID below stand for that project and the saved query's identifier. --no-policies is there because a PostgreSQL or MySQL server read requires it, as the catalogue states.

datatug query run --project PROJECT --query QUERY_ID --no-policies

The example assumes customers.id and orders.customer_id are the same kind of key. It does not show the engines converting types for you.

What has actually been run

The public recorded CLI run is a different pair of sources. On 2 October 2026, DataTug CLI 0.51.0 ran one saved TugQL query. The command, recorded with the timing note in the site kit, was:

datatug query run --project demo-project-1 --query sales/chinook-sales-per-capita --env local --format json

The document is sales/chinook-sales-per-capita.query.dtql at that commit. It joins Chinook invoices to a country-alias table to a population snapshot. The log printed query download for each source, then query process. That run does not pass --no-policies, and it is not a PostgreSQL or MySQL server read. DataTug.ai describes the same run. The feature page also points at the live demo, which joins Chinook customers to population figures. That demo is not a recorded PostgreSQL-to-MySQL result either.

What this page does not claim

There is no published PostgreSQL-to-MySQL row count, and no timing. The catalogue does not verify a browser or hosted connection to a customer database. Firestore cannot be one side of this join: the Firestore how-to says joins are unsupported on that source. A login that can write will still be able to write if something other than this read uses it. --no-policies is the catalogue's requirement for the server read, not a read-only switch on the database.

Where to go next

SQL JOINs vs subqueries teaches a join inside one SQLite database, with checked results. It does not span two servers. Query multiple databases at once is the feature page, including the browser status. The SQL CLI tool page is the terminal database client those commands belong to. The catalogue is the operation-by-operation list. Install the CLI from the installer.