Migrate from MySQL to PostgreSQL

Moving MySQL or MariaDB schemas and data to self-hosted PostgreSQL is mostly a question of type mapping, application query changes and cutover planning. This page explains what maps cleanly, which documented tools move the data, and what drives effort and risk.

On this page

Migrate from MySQL to PostgreSQL: what the move involves and what drives the effort

For a team that runs MySQL or MariaDB in production, the data load is often the smaller part of a move to PostgreSQL. The effort sits in three places: rewriting application queries that depend on MySQL behavior, deciding how each column type maps to a PostgreSQL type, and planning the cutover so writes move from one database to the other without loss. Copying schemas and rows can be automated and tested against a copy of production. The judgment calls around types, SQL and timing are where schedules usually slip, so they deserve the most attention during planning.

Type decisions and query changes come first. MySQL is more lenient than PostgreSQL in several ways. Zero dates such as 0000-00-00 are rejected, implicit conversions between strings and numbers behave differently, and GROUP BY can return non-aggregated columns that PostgreSQL refuses. AUTO_INCREMENT columns become identity columns or sequences, and ENUM and SET columns need an explicit choice between native types, check constraints or lookup tables. TINYINT(1) columns used as booleans need a deliberate mapping. Write statements such as REPLACE INTO and ON DUPLICATE KEY UPDATE need rewriting into PostgreSQL forms. Collect the query set from slow query logs, ORM code and reports before work begins, because application changes usually cost more than the schema.

The pgloader path works like this: pgloader connects to the MySQL source, reads table, index and key definitions, creates the matching PostgreSQL objects, copies the rows and applies casting rules you define for types and values. Its on-the-fly transformations let you correct defaults, convert specific values and rename objects during the load, which keeps one-off fixes out of the application code. Treat the first run as a discovery exercise. Load a recent copy into a staging PostgreSQL instance in infrastructure you control, review the generated DDL and casting rules, and compare row counts and checksums for each table. Confirm the pgloader release and option set you test against, because behavior can differ between versions and source configurations.

Cutover planning sets the schedule. Decide how long writes can stop, whether changes made during the copy must be carried over, and who makes the go or no-go call. Confirm with the tooling how incremental change capture from MySQL would work in your setup, since a one-time load alone may not fit a short window. Build a runbook with the order of steps, validation queries, application configuration switches and a rollback path back to MySQL until the business accepts the new database. Before work starts, agree the scope in writing: which schemas and applications are included, the query inventory, the test cycles, the cutover window and the deliverables. Ask any implementation partner to show how each of those items drives its quote.

Which MySQL features map cleanly to PostgreSQL and which need rework

This table compares common MySQL and MariaDB features with their PostgreSQL equivalents and notes what each one needs during migration. Read the migration note as the work to plan, since some features carry over directly while others need schema or application changes. Confirm exact type behavior for your versions in the pgloader and PostgreSQL documentation before you finalize the type mapping.

FeatureMySQL or MariaDBPostgreSQL equivalentMigration note
Indexes, primary keys and foreign keysB-tree, FULLTEXT and spatial indexes; primary and foreign key constraintsB-tree and other index types; primary and foreign key constraintsKeys usually carry over; FULLTEXT queries need rewriting and foreign key actions need review
AUTO_INCREMENTColumn attribute backed by a per-table counterIdentity columns or serial sequencesReset each sequence to the maximum loaded value after data load
ENUM and SETENUM holds one listed value; SET holds several listed valuesENUM through CREATE TYPE; SET usually becomes a text array or lookup tableSET has no direct equivalent, so choose the design and confirm how pgloader handles it
DATETIME and TIMESTAMPTIMESTAMP converts to and from UTC and supports auto-update; DATETIME does nottimestamp and timestamptz; auto-update requires a triggerDecide time zone storage first and confirm zero-date and auto-update behavior in both documents
Unsigned integersUNSIGNED variants of integer typesNo unsigned types; use a wider signed type or a CHECK constraintWiden the column or add a CHECK constraint to keep the original value range
BLOB and TEXTBLOB for binary data; TEXT variants sized by lengthbytea for binary data; text for character dataConfirm length limits and convert character sets to UTF-8 during load
Stored procedures and triggersProcedural SQL in CREATE PROCEDURE and row-level triggersPL/pgSQL functions, procedures and trigger functionsUsually rewritten by hand, since automated conversion does not cover all syntax
ViewsNamed SELECT definitionsViews with the same general roleMost carry over; review functions, quoting and date handling that differ
SQL mode settingssql_mode controls strictness, invalid dates and zero-date handlingStrict type checking with no silent truncation by defaultData MySQL accepted can fail on load, so test loads against the real schema and data

How pgloader moves MySQL schemas and data in one pass

pgloader connects to the source MySQL or MariaDB database, reads its catalog to discover tables, columns, indexes and keys, and generates PostgreSQL tables from a default type mapping. It then streams the rows across and creates indexes and constraints in the same run. The practical consequence is that the team reviews the generated schema before the first production-like load, not after. Each table needs a decision: accept the default mapping, or override it with a casting rule. Those decisions belong in a version-controlled load file, so the migration can be rehearsed and repeated with identical results. Types without a clean match need the most attention. MySQL has types that do not map one to one onto PostgreSQL, such as ENUM and SET columns, BIT fields, YEAR, unsigned integer ranges that exceed the signed PostgreSQL equivalent, and spatial types. For each one, the team chooses among a wider PostgreSQL type, text with a check constraint, a domain, a separate lookup table, or a deliberate exclusion handled by hand. Date and time columns that contain zero dates or invalid values are a common source of rejected rows and should be listed explicitly during discovery.

On-the-fly transformations let the team change data and names as rows pass through the load, without a second cleanup pass in PostgreSQL. Typical uses include renaming columns to match an application naming standard, changing column defaults, trimming whitespace, normalizing character sets, and replacing values that PostgreSQL would reject. These changes are powerful only when they are documented. For every transformation, record the table, the rule, the expected number of affected rows and the owner who signed off on it. Keep the rejected-row output and review it with the data owners before deciding whether a row is repaired, excluded or corrected at the source. Confirm the exact casting and transformation options against the pgloader version you choose. Syntax for cast rules, column expressions, and table or column name matching has changed across releases, and behavior for some MySQL types has been adjusted over time. Read the release notes for the target version, pin that version in your build, and run the load file against a copy of production data. Treat the rehearsal output, including row counts, rejected rows and generated DDL, as a migration artifact for the cutover plan.

Documented import paths for MySQL and MariaDB

This section lists the documented tools for moving MySQL and MariaDB data into PostgreSQL. pgloader performs the source-side load, PostgreSQL client tools verify the result, and Oracle tooling is covered on its own page. Read the cards in order, since the version check should come before you commit to a path.

pgloader as the primary path

pgloader is the documented import tool for MySQL and MariaDB sources in this topic. It connects to the source, reads table definitions, creates the matching objects in PostgreSQL and streams rows across, applying casting rules that map MySQL column types to PostgreSQL types. Teams use it for the schema and data load, then review the generated objects by hand because automatic mapping may not match every column the way your application expects. Run it against a copy of production data before you plan any cutover window.

Confirm MariaDB and MySQL 8 coverage

Before you choose a path, read the current pgloader documentation for the MariaDB and MySQL versions in your estate. Confirm which versions the documentation covers, how it handles MySQL 8 specifics such as authentication plugin changes and newer data types, and which connection options your source requires. Record the answers in the migration design so the choice traces to the documentation as it stood on the day you checked.

psql for target-side validation

psql is the PostgreSQL client you use to inspect the loaded database. Run row counts per table, check that primary keys, foreign keys and indexes exist, and execute representative application queries that were written against MySQL behavior. Keep these queries in version-controlled scripts so the same checks run after every rehearsal load and again at cutover. Each failed check should name the table, the column and the mapping rule responsible.

pg_dump for verification exports

pg_dump exports the migrated schema and data from PostgreSQL in a form you can compare across rehearsals. Export a schema-only dump after each load and diff it against the reviewed design, which shows drift in column types, defaults and constraints. Use data exports from a restored copy to confirm that the cutover image matches the rehearsal image. Dump sizes and run times grow with the database, so measure them on your own data before you schedule the window.

Why Ora2Pg sits on the Oracle page

Ora2Pg is a documented tool for Oracle sources, so it is covered on the Migrate from Oracle to PostgreSQL page instead of here. Its assessment reports and PL/SQL conversion do not apply to a MySQL or MariaDB schema. If your estate includes both Oracle and MySQL sources, plan each source with its own tool and keep target conventions, such as naming, data types and roles, consistent across both migrations.

The MySQL to PostgreSQL migration sequence with a parallel run and cutover

This sequence moves a MySQL or MariaDB workload to PostgreSQL in stages that each produce evidence, so the cutover decision rests on tested data, tested application behavior and a rollback point you have already exercised. Each stage has named outputs and named client sign-off owners.

  1. Inventory schemas, queries and integrations

    DeployOpen works with your database and application owners to list every schema, table, view, stored routine, trigger, event and scheduled job, along with the queries and integrations that read or write them. Typical sources include application repositories, ORM mappings, reporting tools, ETL jobs and DBA scripts. You provide read access to the MySQL schema catalog, query logs or slow-query logs for a representative period, and the names of the owners who can confirm behavior. The step closes with an inventory register and a list of MySQL-specific constructs, such as storage engines, zero-date handling, character sets and non-standard SQL, that need a decision. Sign-off comes from the application owners on the register.

  2. Map types and generate the target schema

    Using the inventory, DeployOpen defines the type mapping from MySQL to PostgreSQL, covering integer widths, decimals, datetime and timezone handling, JSON columns, enumerations, auto-increment keys and character collation. Tooling such as pgloader can discover tables, indexes and keys and produce a first draft, which is then reviewed by hand for anything it cannot translate. You receive a mapping specification and a generated PostgreSQL DDL set under version control, with each deviation from the default mapping explained. Your DBA and data owners approve the mapping before any data is moved.

  3. Pilot load into a non-production cluster

    DeployOpen loads a copy of production data, or a masked subset you approve, into a non-production PostgreSQL cluster that matches the planned production configuration as closely as your environment allows. The pilot measures load duration, rejected rows, truncated values and constraint violations, and records every transformation applied during load. You provide the non-production cluster or the infrastructure to build it, masked or approved test data, and the data protection rules that apply to copies. The step closes with a load report listing row counts per table and every exception, with a count of rows that failed to load and the reason for each class of failure. Data owners sign off on the exception list.

  4. Test application compatibility

    Your application teams point test builds at the pilot PostgreSQL database and run their functional and regression suites, paying particular attention to SQL that depends on MySQL behavior such as implicit type conversion, case-insensitive comparisons, GROUP BY leniency, LIMIT clauses inside subqueries and quoting rules. DeployOpen supports this work by proposing query changes, keeping a log of each change and the test that justified it. You provide test environments, test accounts, and access to your test suites. The output is a compatibility register with every changed query, its test result and the owner who accepted the change. Application owners sign off before the parallel run starts.

  5. Run the parallel run and compare results

    Both databases receive the same business writes for a defined period while your application reads from MySQL as the system of record. DeployOpen builds a comparison job that runs agreed reports and key queries against both databases and records differences in row counts, aggregates, and sampled row contents. Differences are triaged into data defects, query defects and expected differences, and each has an owner. You provide the replication or dual-write mechanism agreed with your architects, access to both databases for comparison, and the business reports used for acceptance. The parallel run closes when the difference log shows no open data defects and the agreed reports match, as confirmed by the business owners.

  6. Execute the cutover window with a tested rollback point

    Before the production window, DeployOpen rehearses the cutover on the non-production cluster, timing each step and recording the exact order of actions. The rehearsal includes a defined rollback point: the procedure for returning writes to MySQL, the data reconciliation needed if PostgreSQL has accepted writes, and the decision criteria for invoking rollback. During the window, the team freezes writes, applies final synchronization, runs post-load checks, and switches application connection strings. You provide the change window, the on-call contacts, and the people who can make the go or no-go decision. The step closes with a signed cutover record that names the rollback point tested in rehearsal.

  7. Decommission MySQL after a monitored period

    After the cutover has run through a monitoring period agreed in the scope, DeployOpen supports the retirement of MySQL. This includes a final backup that you retain under your retention policy, confirmation that no jobs or integrations still connect to the old server, removal of credentials, and documentation of the archived data location. Retention periods and any regulatory hold requirements are set by your compliance and legal owners, not by the migration team. You receive a decommissioning record, updated runbooks for PostgreSQL backup, restore and monitoring, and a handover to the operations team. Your operations owner accepts the handover in writing.

Which application queries change when MySQL moves to PostgreSQL

The application query changes that matter most are the ones MySQL accepts without complaint and PostgreSQL either rejects or reads differently. Start by inventorying three sources of SQL: hand-written queries in the application, SQL generated by the ORM or query builder, and stored logic such as views, functions and triggers. Each source needs its own review, because a migration tool can convert the schema and data while leaving application code untouched. Identifier handling is the first place to decide. MySQL quotes identifiers with backticks, while PostgreSQL uses double quotes. PostgreSQL also folds unquoted identifiers to lowercase and treats quoted identifiers as case-sensitive, so a mixed-case table name created during migration will fail when application SQL refers to it without quotes. Choose one naming convention, usually all lowercase, and apply it to the schema and the code together.

Several query patterns break on contact with PostgreSQL. MySQL accepts the LIMIT offset, count form, which PostgreSQL does not, so queries must use LIMIT count OFFSET offset. GROUP BY is the next risk. PostgreSQL requires every selected column that is not aggregated to appear in the GROUP BY clause, or to be functionally dependent on a primary key that is included in it. MySQL deployments may already run with ONLY_FULL_GROUP_BY enabled, so check the sql_mode setting on the source before assuming the queries are clean. Implicit type conversion causes the most surprises at runtime. MySQL coerces strings to numbers in comparisons and reports a warning, while PostgreSQL raises an error when an integer is compared with text without an explicit cast. Parameters bound as strings by a driver are the usual source. Run the full query suite against PostgreSQL and capture every error by category.

Upsert logic needs a deliberate rewrite. INSERT ... ON DUPLICATE KEY UPDATE becomes INSERT ... ON CONFLICT with a named conflict target and DO UPDATE SET, where EXCLUDED refers to the row that was proposed for insertion. The conflict target must match a unique constraint or unique index that exists in the PostgreSQL schema, so confirm that every upsert path has one. INSERT IGNORE maps to ON CONFLICT DO NOTHING, and REPLACE INTO has no direct equivalent because it deletes and reinserts rows, which changes triggers and foreign key behavior. ORM dialect settings are the second half of this work. Most ORMs take the database dialect from configuration, and the setting controls quoting, generated SQL and migration output. Confirm that the ORM and the database driver versions in your stack both support PostgreSQL and the major version you intend to run, and record the versions you tested.

Authentication changes apply to every application that connects, not only to the queries. PostgreSQL decides how each client authenticates through the host-based authentication rules in pg_hba.conf, and the password storage method is controlled by the password_encryption setting. SCRAM is the preferred password method. PostgreSQL 18 deprecated MD5 password authentication in favor of SCRAM, so new roles should be created with SCRAM and existing MD5 roles should be migrated as part of the cutover plan. Check that each driver, ORM connector and connection pooler in the path supports SCRAM, because an older client library can fail to connect even when the database is configured correctly. If PgBouncer sits between the applications and PostgreSQL, confirm how it handles SCRAM credentials before cutover. Confirm these details with your driver and ORM vendors for the exact versions you run, and include the authentication test in the pre-cutover checklist.

What drives the cost and duration of a MySQL to PostgreSQL migration

This table lists the factors that change the effort of moving a MySQL or MariaDB database to PostgreSQL, who pays for each one, whether it is one-time or recurring, and what to request in a written quote. Use it to compare your own inventory against the scope a partner proposes, since most overruns come from drivers that were never counted.

DriverWhy it changes cost or effortWho pays and one-time or recurringWhat to ask for in a quote
Schemas and tablesEach schema adds discovery, type mapping review, index and key translation, and row-count reconciliation. Large table counts also multiply test cases.Client pays the engagement fee for the migration build. One-time.A per-schema inventory, the number of tables and indexes in scope, and the reconciliation method for each.
Stored procedures, functions and triggersMySQL routines and triggers often use dialect-specific syntax, implicit behavior and session variables that must be rewritten by hand in PL/pgSQL.Client pays for the rewrite and its tests. One-time, though later changes to that code are recurring maintenance.A list of every routine and trigger, with the line count and a stated approach for each: convert, rewrite or retire.
Data volume and downtime toleranceLarger datasets lengthen the initial load and final delta sync. A tighter downtime window requires more rehearsal and a more controlled cutover.Client pays for rehearsal loads and cutover support. One-time for the migration, plus any hosting cost for temporary capacity.Measured data size per table, the agreed maximum write freeze, and the number of full rehearsal loads included.
Applications and ORMs issuing queriesEach application, driver or ORM may send MySQL-specific SQL, quoting, LIMIT syntax, or type assumptions that need changes and retesting.Client pays for application code changes, usually from their own or a partner development team. One-time.A list of applications and ORMs with their versions, the queries flagged for change, and who owns the code fixes.
Reporting, BI and ETL dependenciesReports, extracts and pipelines that read the database directly break on type or naming changes and need updates on both sides.Client pays for pipeline and report changes, and may pay ongoing run costs for any new tooling. One-time for changes, recurring for run costs.A dependency map of consumers, the owner of each, and whether consumers are updated before or after cutover.
Environments to validateDevelopment, test, staging and production each need the schema, data and application checks repeated, and drift between them adds rework.Client pays for infrastructure in each environment. Validation effort is one-time per cycle, while environments may be recurring.The environments included in scope, the validation steps run in each, and how configuration drift is controlled.
Parallel runRunning MySQL and PostgreSQL side by side for comparison, with data kept in sync, adds infrastructure, sync tooling and reconciliation work for a set period.Client pays for duplicate infrastructure and reconciliation. Largely one-time for setup, with recurring infrastructure cost while the run continues.Whether a parallel run is included, its planned length as agreed in writing, the comparison criteria, and who pays for the duplicate hosting.

What usually goes wrong in a MySQL to PostgreSQL migration and how to prevent it

Most MySQL to PostgreSQL migrations fail in predictable places: data changes meaning during conversion, text is compared differently, and application paths break after cutover. The cards below cover each risk with a concrete check, so the migration plan can name who verifies what before the load starts.

Silent type conversions that change meaning

Automated mapping can convert MySQL types such as TINYINT(1), DATETIME, ENUM and unsigned integers into PostgreSQL types that store different values or ranges. Review the generated type map table by table before the first load, and confirm how zero dates and out-of-range values are handled. Prevent drift with row-count and checksum comparison on every migrated table, including sample rows with edge values.

Character set and collation mismatches

MySQL databases configured with utf8mb3 or a case-insensitive collation can hold text that PostgreSQL stores differently, which changes sort order, uniqueness checks and equality comparisons. Confirm the source character set and collation for every schema, choose the target encoding and collation deliberately, and test unique constraints against real data. Record any rows that fail to load so they can be reviewed rather than silently altered.

Identifier case and quoting problems

PostgreSQL folds unquoted identifiers to lowercase, while MySQL table names can depend on the operating system and mixed-case names are common in legacy schemas. Application code that quotes or capitalizes names may then fail or query the wrong object. Decide one naming convention before conversion, map every renamed object in a written list, and grep application code and reporting queries for affected identifiers.

Application queries that fail only on edge paths

Routine page loads often pass after migration while rarely used paths fail on MySQL-specific syntax, implicit type coercion, GROUP BY behavior, LIMIT with offsets or functions such as IFNULL and DATE_FORMAT. Build a list of queries from application logs, batch jobs, reports and admin tools, then run each one against the target. Include failure paths, empty result sets and error handling in the test set.

Sequences not reset after the data load

AUTO_INCREMENT columns become PostgreSQL sequences that do not advance when rows are inserted with explicit ids during the load. The first application insert then fails with a duplicate key error or, worse, reuses ids that other systems already reference. Set each sequence to the maximum loaded value as a named cutover step, and verify it with a test insert in a non-production copy before the cutover window.

Missing rollback criteria and rollback point

Teams often agree to cut over without writing down the conditions that would trigger a return to MySQL or the point after which rollback means losing writes. Define the go or no-go checks, the last safe point for reverting, and who decides, before the rehearsal. Decide how writes made on PostgreSQL would be reconciled if a rollback happens late, and rehearse that path at least once.

Underestimated stored code rewrites

Stored procedures, triggers, events and functions written for MySQL do not convert to PL/pgSQL automatically, and the gap is often larger than the table migration itself. Inventory all routines, triggers and scheduled events early, classify each as rewrite, replace in the application or retire, and estimate that work separately from the data load. Test each rewritten routine against the same inputs used on MySQL.

Running PostgreSQL after cutover: backups, failover and connection pooling

The team must design backup, standby and failover behavior before the first production write lands on PostgreSQL. Cutover is not the end of database work. Once MySQL traffic is switched, the team owns recovery objectives, replication topology and connection handling, and each of these needs a decision and a test. Start by writing down the recovery point you need for each database, because that choice drives every backup setting that follows.

Point-in-time recovery in PostgreSQL is built from two pieces: a base backup and the archived write-ahead log (WAL) files produced after it. Restoring to an arbitrary moment means restoring the base backup and replaying archived WAL up to the target time. Confirm where WAL archives are stored, how long they are retained, and who can restore them. For larger clusters, the team can also choose incremental backups taken with pg_basebackup and reconstructed at restore time with pg_combinebackup. Incremental backups reduce what each backup run writes, but they add a dependency chain that the restore runbook must document and test. Ask your partner or internal DBA to show the exact restore command sequence in your environment.

Replication needs a separate decision. A hot standby accepts read-only queries, which can offload reporting from the primary, but reporting queries on a standby must be checked against application expectations, especially for MySQL workloads that assumed certain read-after-write behavior. Synchronous replication makes a commit wait until named standbys confirm it, which protects committed data at the cost of write latency and availability when standbys are unreachable. Asynchronous replication keeps writes fast but can lose the most recent commits on failover. Decide which trade-off each database accepts, and record it alongside the recovery point objective.

PostgreSQL does not detect a failed primary and promote a standby on its own. Automatic failover therefore requires an external tool, plus a clear policy for fencing the old primary so two nodes do not accept writes. If the team chooses manual failover, the runbook must name who promotes the standby, how clients are redirected, and how the old primary is rebuilt. Either way, test the failover path on a non-production copy before go-live, and repeat the test after every topology change.

Connection pooling is the last layer to plan. Applications migrated from MySQL often open many short-lived connections, and PostgreSQL handles each connection as a separate server process, so connection counts need explicit limits. PgBouncer is a lightweight connection pooler for PostgreSQL that is worth evaluating against your application's pooling mode and session requirements, including prepared statements and session-level settings. Confirm the pooler version you deploy against the project's current release notes and security advisories. Restore tests and minor-version patching are ongoing duties rather than one-time tasks: schedule restore drills on a fixed cadence, and apply minor PostgreSQL releases, which replace executables and require a restart, as part of routine maintenance.

Questions buyers ask before hiring help to migrate from MySQL to PostgreSQL

How much does it cost to migrate from MySQL to PostgreSQL?

No single price applies, because cost follows the written scope. The main drivers are the number of schemas, tables, stored procedures, triggers and events, how much MySQL-specific SQL the applications contain, the data volume, and how many systems read and write the database.

Who is paid for what: your staff time for application owners, testing and approvals; the partner's discovery, conversion, data load, validation and cutover work; and the infrastructure for PostgreSQL and any temporary migration hosts, which runs in your own cloud or data center.

One-time versus recurring: most migration costs are one-time. Running PostgreSQL afterward, including backups, patching and support, is ongoing.

What to ask for in a quote: an itemized scope covering discovery, conversion, testing, cutover and post-cutover support, plus the assumptions behind data volume and application changes.

Can our own team migrate from MySQL to PostgreSQL in-house?

Yes, many teams can, provided they have database engineers who know both engines and application teams that can change their queries. Open-source tools such as pgloader automate much of the schema and data copy, but they do not rewrite application SQL or stored code, and that work usually takes the most time.

In-house efforts tend to stall in three places: unclear type mappings for dates, decimals and text columns, application queries that depend on MySQL behavior, and cutover planning that nobody has rehearsed.

Ask three questions: has anyone on the team run a migration of this size, can engineers commit time through several test cycles, and who approves the cutover. If the answers are uncertain, a partner can provide a fixed scope with written deliverables. Either way, your team keeps ownership of the production database.

How long does a MySQL to PostgreSQL migration take?

The duration depends on drivers you can measure during discovery, so ask for an estimate that states those drivers. The biggest are the amount of application SQL to change, the number of stored routines and triggers to rewrite, the total data size and how long a full load takes in your environment, and how many test cycles the business requires.

Other factors include the number of applications that connect to the database, whether they can be updated in step, the availability of application owners for testing, and the change approval process in your organization.

A reliable plan separates the schedule into discovery, conversion, repeated test loads, a parallel run and cutover. Ask the vendor to show which tasks are on the critical path and which depend on your teams. No duration is reliable until the inventory of schemas, queries and routines is complete.

What does our team need to provide for a MySQL to PostgreSQL migration?

The work needs four things from your side: schema access, representative test data, application owners and a maintenance window.

Schema access: read-only access to the MySQL schemas, indexes, routines, triggers and events, plus the character sets and collations in use.

Test data: a copy of production or a sanitized dataset that is large enough to expose type, encoding and date problems. Confirm that any copy meets your data handling rules.

Application owners: named people who can confirm which queries run, approve changes and test results for each application.

Maintenance window: an agreed period for the final data load and cutover. Its length depends on data volume and how much verification you require before reopening writes.

Confirm access paths and approval roles in writing before work starts.

Does MariaDB migrate to PostgreSQL differently from MySQL?

The core approach is the same, but MariaDB can differ from MySQL in areas that affect conversion. Some features exist in one product and not the other, and some syntax, system tables, default settings and storage engine behavior vary between versions. Those differences show up in schema definitions, stored routines and queries that rely on vendor-specific functions.

Before scoping, confirm the exact MySQL or MariaDB version in use, the storage engines and any extensions the applications depend on. Then compare the schema inventory against the tools you plan to use, because a tool that handles MySQL well may need configuration or manual steps for MariaDB-specific objects.

Because these differences change the effort, the vendor should test representative objects from your own environment before quoting.

How does a parallel run work during a MySQL to PostgreSQL migration?

A parallel run keeps both databases running with the same business data for a period, so the team can compare results before committing to PostgreSQL. In practice, the MySQL system stays live while changes are copied to PostgreSQL, or the migrated data is refreshed on a schedule. Applications or reporting jobs then run against both, and the team compares row counts, checksums, key business totals and sample query outputs.

The setup needs a defined method for keeping the two systems in step, a comparison plan, and agreed exit criteria. Mismatches usually come from type conversion, collation differences, time zone handling or application code that writes directly to MySQL.

The length of the run depends on your change volume and risk tolerance, so set it during planning instead of accepting an arbitrary period.

What happens to the MySQL system after cutover to PostgreSQL?

After cutover, the MySQL system is usually kept read-only for a defined retention period, so the team can compare data, recover from a missed issue and satisfy audit needs. Once the business accepts the new system, it is backed up, archived and decommissioned under your data retention policy.

Plan these steps before cutover: disable application writes to MySQL, confirm that no job still connects to it, export final backups, and record the decommissioning date and owner. Keep connection credentials and monitoring until the retirement is complete.

For the PostgreSQL side, confirm backups, point-in-time recovery, monitoring and patching responsibilities. PostgreSQL 17 and 18 are the current supported major versions, with support ending in 2029 and 2030, so select the target version against your upgrade plan.

How DeployOpen scopes, delivers and supports a MySQL to PostgreSQL migration

DeployOpen scopes each MySQL or MariaDB to PostgreSQL move as a written, fixed-scope engagement before any data moves. The work covers schema discovery, type mapping, planned transformations with pgloader, application query changes and a cutover plan tested in your own infrastructure. Ongoing support is available as a retainer.

How we scope

  • The written scope lists every MySQL or MariaDB schema, table group and data volume in the migration.
  • Schema discovery output, type mapping decisions and the list of stored routines and triggers to rewrite.
  • Application query inventory covering SQL that depends on MySQL-specific syntax, functions or session settings.

How we build

  • Load scripts are written for pgloader, with each type mapping rule written out and reviewed.
  • On-the-fly transformations are coded and tested against a representative copy of the source data before the first full load.
  • Application query changes are documented for your developers, with test cases for each rewritten statement.

How we hand over

  • Deliverables include the migration scripts, type mapping record, validation queries and a runbook for cutover.
  • Row count and checksum comparisons between MySQL and PostgreSQL are documented as acceptance evidence.
  • Cutover rehearsal results, rollback steps and the post-cutover checklist are handed to your operations team.

How we support

  • Post-cutover support covers defects found in migrated queries, schemas and load scripts during the agreed period.
  • Ongoing operations of the PostgreSQL cluster can be covered by a retainer for patching, backups and upgrades.
  • Major version planning, such as moving to a supported PostgreSQL release, is scoped separately in writing.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.