intermediate 1 day to 2 weeks depending on database size and complexity100% Free Guide

How to Migrate from MySQL to PostgreSQL

Migrating from MySQL to PostgreSQL is one of the most common database migrations. Both are mature open-source relational databases, but they have significant differences in SQL dialect, data types, and behavior. PostgreSQL is stricter about SQL standards compliance, which can surface issues that MySQL silently accepts.

15

Data Type Mappings

12

Syntax Differences

9

Migration Steps

3

Free Tools

Data Type Mappings

MySQLPostgreSQLNotes
INTINTEGER or INT4Identical behavior
BIGINTBIGINT or INT8Identical
TINYINT(1)BOOLEANMySQL uses TINYINT(1) for booleans; PostgreSQL has native BOOLEAN type
AUTO_INCREMENTSERIAL or GENERATED ALWAYS AS IDENTITYPostgreSQL IDENTITY columns are the modern standard
VARCHAR(n)VARCHAR(n)Identical, but PostgreSQL TEXT is often preferred over VARCHAR with no limit
TEXTTEXTIdentical
DATETIMETIMESTAMPPostgreSQL uses TIMESTAMP for datetime values
DATETIME with timezoneTIMESTAMPTZUse TIMESTAMPTZ for timezone-aware timestamps in PostgreSQL
UNSIGNED INTINTEGER (with CHECK constraint)PostgreSQL has no UNSIGNED types; use CHECK (col >= 0)
ENUM('a','b')CREATE TYPE name AS ENUM ('a','b')PostgreSQL ENUMs are database types; MySQL ENUMs are column attributes
JSONJSON or JSONBUse JSONB for indexable, efficient JSON storage in PostgreSQL
BLOBBYTEA or Large ObjectsBYTEA for up to ~1GB; Large Objects for larger binary data
MEDIUMTEXT / LONGTEXTTEXTPostgreSQL TEXT has no size limit
DOUBLEDOUBLE PRECISION or FLOAT8Same semantics
DECIMAL(p,s)NUMERIC(p,s)Functionally identical; NUMERIC is the standard SQL name

SQL Syntax Differences

String Quoting

MySQL

Both single and double quotes for strings

PostgreSQL

Single quotes only for strings; double quotes for identifiers

Must convert: "string" → 'string' in all queries

Auto Increment

MySQL

id INT AUTO_INCREMENT PRIMARY KEY

PostgreSQL

id SERIAL PRIMARY KEY or id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY

SERIAL is a shorthand; IDENTITY is the SQL standard

Backtick Quoting

MySQL

SELECT `column` FROM `table`

PostgreSQL

SELECT "column" FROM "table"

Replace all backticks with double quotes

LIMIT / OFFSET

MySQL

SELECT * FROM t LIMIT 10 OFFSET 20

PostgreSQL

SELECT * FROM t LIMIT 10 OFFSET 20

Identical syntax — no change needed

String Concatenation

MySQL

CONCAT(a, b, c) or a + b

PostgreSQL

a || b || c or CONCAT(a, b, c)

|| is standard SQL concat; CONCAT() also works in PostgreSQL

Date Functions

MySQL

NOW(), DATE_FORMAT(), DATEDIFF()

PostgreSQL

NOW(), TO_CHAR(), date - date

Date functions differ significantly between MySQL and PostgreSQL

GROUP BY

MySQL

Allows non-aggregated columns not in GROUP BY

PostgreSQL

Strict: all non-aggregated columns must be in GROUP BY

Review all GROUP BY queries — many will fail in PostgreSQL

Case Sensitivity

MySQL

Table names case-insensitive on Windows/Mac

PostgreSQL

Column names fold to lowercase; identifiers case-sensitive with quotes

All table/column names should be lowercase without quotes in PostgreSQL

INSERT IGNORE

MySQL

INSERT IGNORE INTO t (col) VALUES (val)

PostgreSQL

INSERT INTO t (col) VALUES (val) ON CONFLICT DO NOTHING

ON CONFLICT is PostgreSQL's upsert mechanism

ON DUPLICATE KEY UPDATE

MySQL

INSERT INTO t (...) VALUES (...) ON DUPLICATE KEY UPDATE col = value

PostgreSQL

INSERT INTO t (...) VALUES (...) ON CONFLICT (col) DO UPDATE SET col = EXCLUDED.col

EXCLUDED refers to the rejected row in PostgreSQL

FULL OUTER JOIN

MySQL

Not supported natively (requires UNION workaround)

PostgreSQL

Natively supported: FULL OUTER JOIN

PostgreSQL supports FULL OUTER JOIN directly

IFNULL

MySQL

IFNULL(col, 'default')

PostgreSQL

COALESCE(col, 'default')

COALESCE is the SQL standard; works in both databases

Step-by-Step Migration Guide

1

Schema Assessment and Inventory

Export your MySQL schema and catalog all tables, views, stored procedures, triggers, and foreign key relationships. Identify MySQL-specific features that need PostgreSQL equivalents.

mysqldump --no-data --routines --triggers mydb > schema.sql
2

Set Up Target PostgreSQL Instance

Install PostgreSQL, create the target database, and configure connection parameters. Install pgloader or your chosen migration tool.

createdb target_db
psql target_db
3

Convert Schema

Convert the MySQL DDL to PostgreSQL-compatible DDL. Replace data types, AUTO_INCREMENT with SERIAL, ENUM types, backticks with double quotes, and MySQL-specific options.

-- MySQL:
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL
) ENGINE=InnoDB;

-- PostgreSQL:
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL
);
4

Migrate Data

Use pgloader for automated, efficient data migration. It handles type conversions automatically and provides detailed error reports.

-- pgloader command file:
LOAD DATABASE
  FROM mysql://user:pass@localhost/source_db
  INTO postgresql://user:pass@localhost/target_db
SET work_mem to '16MB',
    maintenance_work_mem to '512MB';
5

Migrate Stored Procedures and Functions

MySQL stored procedures use a different syntax than PostgreSQL PL/pgSQL. Each procedure must be manually rewritten for PostgreSQL.

-- MySQL Procedure
CREATE PROCEDURE GetUser(IN userId INT)
BEGIN
  SELECT * FROM users WHERE id = userId;
END;

-- PostgreSQL Function
CREATE OR REPLACE FUNCTION get_user(p_user_id INT)
RETURNS TABLE(id INT, email VARCHAR) AS $$
BEGIN
  RETURN QUERY SELECT u.id, u.email FROM users u WHERE u.id = p_user_id;
END;
$$ LANGUAGE plpgsql;
6

Update Application Queries

Update application code: replace backtick quoting with double quotes or no quotes, replace MySQL-specific functions with PostgreSQL equivalents, fix GROUP BY strictness issues, and update UPSERT syntax.

7

Validate Data Integrity

Compare row counts between source MySQL and target PostgreSQL for all tables. Spot-check critical business records. Run application integration tests against the PostgreSQL database.

-- Run in both MySQL and PostgreSQL, compare results:
SELECT table_name, COUNT(*)
FROM information_schema.tables
JOIN (SELECT table_name FROM information_schema.tables WHERE table_schema = 'mydb') t USING(table_name)
GROUP BY table_name;
8

Performance Testing and Index Optimization

Run EXPLAIN ANALYZE on your most critical queries. PostgreSQL's query planner may choose different strategies than MySQL. Add or adjust indexes as needed. Run ANALYZE to update table statistics.

ANALYZE;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
9

Cutover

Plan the cutover: take the MySQL database read-only, run a final incremental sync to PostgreSQL, update application connection strings, switch DNS/load balancer, and monitor closely for 24-48 hours.

Common Issues & Solutions

GROUP BY errors: column must appear in GROUP BY clause

PostgreSQL enforces SQL standard GROUP BY. Add all non-aggregated SELECT columns to GROUP BY, or wrap them in an aggregate function. Review every GROUP BY query in your codebase.

Case sensitivity: queries that worked in MySQL fail in PostgreSQL

PostgreSQL folds unquoted identifiers to lowercase. Rename all tables and columns to lowercase in both MySQL and PostgreSQL before migrating. Avoid mixed-case identifiers.

TINYINT(1) boolean values stored as 0/1 not true/false

Map MySQL TINYINT(1) to PostgreSQL BOOLEAN. pgloader does this automatically. Verify that application code handles true/false (not 1/0) from PostgreSQL.

Date/time function differences (DATE_FORMAT, DATEDIFF, etc.)

Replace MySQL-specific date functions: DATE_FORMAT() → TO_CHAR(), DATEDIFF() → (date1 - date2), DATE_ADD() → date + INTERVAL 'n days', NOW() stays the same.

Stored procedures require complete rewrite in PL/pgSQL

MySQL and PostgreSQL use different procedural languages. Each stored procedure must be manually ported to PL/pgSQL syntax. Consider this the largest time investment in the migration.

Recommended Migration Tools

pgloader

FREE

Open-source ETL tool specifically designed for MySQL → PostgreSQL migration. Handles type conversions automatically.

AWS Database Migration Service

Managed cloud migration service supporting MySQL → PostgreSQL with continuous replication.

pgAdmin

FREE

Free PostgreSQL GUI for schema management and query testing.

Flyway / Liquibase

FREE

Schema version management tools for managing schema changes post-migration.

Detailed Practical Migration Guide

MySQL to PostgreSQL Migration: A Practical Guide for Developers Who've Been There

💡 Mindset Shift: Ready to ditch MySQL for PostgreSQL? Read our high-level overview: [So You Want to Ditch MySQL for PostgreSQL. Let's Talk](/blog/ditch-mysql-for-postgresql).

So you've decided to migrate from MySQL to PostgreSQL. Maybe your team finally hit that wall where MySQL just isn't cutting it anymore — complex queries slowing down, missing features you desperately need, or you're simply following the industry trend toward Postgres. Whatever brought you here, you're making a solid choice. But let's be honest: migrations are messy, stressful, and full of surprises.

This guide won't sugarcoat things. We'll walk through the whole process — the what, the why, the how, and the "oh no, why is this breaking?" moments — in a way that actually makes sense.


Why Bother Switching at All?

Before diving into the *how*, let's talk about the *why*. You shouldn't migrate just because it's trendy. Here are some genuinely good reasons people make the switch:

PostgreSQL handles complex queries better. Window functions, CTEs (Common Table Expressions), recursive queries — Postgres was built for analytical workloads that make MySQL sweat.

Stricter SQL standards compliance. MySQL has historically been lenient (some would say *too* lenient) about data integrity. PostgreSQL enforces the rules more closely, which means fewer silent data corruption bugs down the road.

Rich data types. Native JSON/JSONB support, arrays, hstore, UUID, geometric types — Postgres gives you a lot of tools out of the box that MySQL simply doesn't have.

Better concurrency model. PostgreSQL uses MVCC (Multi-Version Concurrency Control) more aggressively, which means reads don't block writes and vice versa in most scenarios.

Open source with no "community vs enterprise" split. Unlike MySQL (owned by Oracle), PostgreSQL is truly community-driven. What you see is what you get — no feature paywalls.


What's Actually Different Between MySQL and PostgreSQL?

This is where most guides gloss over the painful details. Let's not do that.

1. Case Sensitivity in Identifiers

MySQL is case-insensitive for table and column names by default (on most platforms). PostgreSQL lowercases everything unless you quote it.

-- MySQL: This works fine
SELECT * FROM Users;
SELECT * FROM users; -- same table

-- PostgreSQL: These are different!
SELECT * FROM "Users";  -- specific table named Users
SELECT * FROM users;    -- different table

Practical tip: Stick to lowercase identifiers everywhere and you'll avoid this headache entirely.

2. AUTO_INCREMENT vs SERIAL / IDENTITY

MySQL uses AUTO_INCREMENT. PostgreSQL uses SERIAL (older style) or GENERATED ALWAYS AS IDENTITY (modern, SQL-standard way).

-- MySQL
CREATE TABLE products (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255)
);

-- PostgreSQL (modern way)
CREATE TABLE products (
  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name VARCHAR(255)
);

3. String Quoting

MySQL lets you use double quotes for string values. PostgreSQL strictly uses single quotes for strings and double quotes for identifiers (column/table names).

-- MySQL (works)
SELECT * FROM users WHERE name = "Alice";

-- PostgreSQL (WRONG — double quotes mean identifier)
SELECT * FROM users WHERE name = "Alice";  -- error or wrong behavior

-- PostgreSQL (correct)
SELECT * FROM users WHERE name = 'Alice';

4. LIMIT with OFFSET

Both support LIMIT and OFFSET, but the syntax differs slightly in edge cases. You're mostly fine here, but watch out when migrating raw SQL strings.

5. Boolean Values

MySQL uses TINYINT(1) to fake booleans. PostgreSQL has a real BOOLEAN type.

-- MySQL
is_active TINYINT(1) DEFAULT 1

-- PostgreSQL
is_active BOOLEAN DEFAULT TRUE

6. Date and Time Handling

PostgreSQL is stricter about date formats and timezone handling. MySQL tends to silently accept garbage dates (like 0000-00-00). Postgres will throw an error.

7. Full-Text Search Syntax

Both databases support full-text search, but the syntax is completely different. If you're using MySQL's MATCH ... AGAINST, you'll need to rewrite those queries for PostgreSQL's tsvector and tsquery approach.


Planning the Migration: Don't Skip This Part

Rushing into a migration without a plan is how you end up with 3 AM war room calls. Here's what you should nail down before touching a single production database.

Audit Your Current MySQL Setup

  • How large is your database? (GBs, TBs?)
  • How many tables, views, stored procedures, and triggers do you have?
  • Are you using MySQL-specific functions or features extensively?
  • What does your application ORM look like? (Sequelize, SQLAlchemy, ActiveRecord, etc.)
  • Do you have any raw SQL queries embedded in the codebase?
  • Identify the Hard Parts Early

    Run a search through your codebase for MySQL-specific syntax:

  • AUTO_INCREMENT
  • TINYINT(1) used as booleans
  • GROUP BY with non-aggregated columns (MySQL allows this, Postgres doesn't by default)
  • IFNULL() (use COALESCE() in Postgres)
  • NOW() vs CURRENT_TIMESTAMP (both work in Postgres, but some MySQL variants have nuances)
  • CONCAT() with NULL values behaves differently
  • Decide on Your Migration Approach

    There are three main strategies:

    Big Bang Migration — Take the system offline, migrate everything, bring it back up on PostgreSQL. Simple but risky for large systems with zero downtime requirements.

    Parallel Run — Run both databases simultaneously. Write to both, read from MySQL until Postgres is verified, then cut over. Complex but safe.

    Incremental Migration — Migrate table by table or feature by feature, using something like a dual-write proxy. Best for large, complex systems.

    For most small-to-medium projects, a well-planned Big Bang migration with a tested rollback plan works fine.


    The Actual Migration Process

    Step 1: Schema Migration

    Start with the schema — no data yet. You want to translate your MySQL DDL (Data Definition Language) into PostgreSQL-compatible DDL.

    Do it manually for small databases. It's tedious but gives you full control and helps you catch issues early.

    Use tools for larger databases:

  • pgLoader — The most popular tool for this. It can migrate schema and data in one shot from a live MySQL instance.
  • AWS Schema Conversion Tool (SCT) — Useful if you're in the AWS ecosystem.
  • ora2pg — Though named for Oracle, it handles MySQL too.
  • Using pgLoader:

    pgloader mysql://user:password@localhost/mydb \
             postgresql://user:password@localhost/mydb

    It handles a lot of type conversions automatically, but always review the output.

    Step 2: Data Migration

    Once schema is in place, move the data. pgLoader can do both at once, which is convenient.

    Watch out for:

  • Encoding issues (MySQL often uses latin1 or mixed encodings; PostgreSQL defaults to UTF-8)
  • Zero dates (0000-00-00) that MySQL allows but Postgres rejects
  • Large BLOB/TEXT fields that might need special handling
  • If you're doing it manually with dumps:

    # Export from MySQL
    mysqldump -u root -p --compatible=postgresql mydb > dump.sql
    
    # This won't work perfectly — you'll need to clean up the dump

    Honestly, stick with pgLoader or a purpose-built ETL pipeline for anything beyond a toy database.

    Step 3: Update Application Code

    This is where the real work is. Update your:

    ORM configuration: Change the database adapter. In Python (SQLAlchemy), swap mysql+pymysql:// for postgresql://. In Node.js (Sequelize), change the dialect from mysql to postgres.

    Raw SQL queries: Hunt down every raw SQL string and fix MySQL-specific syntax.

    Stored procedures and triggers: Rewrite in PL/pgSQL instead of MySQL's stored procedure syntax.

    Connection pooling: PostgreSQL handles connections differently. Tools like PgBouncer are commonly used to manage connection pools.

    Step 4: Test Everything

    Don't skip this. Run your full test suite. Then run it again.

    Pay special attention to:

  • Any query that uses GROUP BY — Postgres is strict about including non-aggregated columns
  • Transactions and rollback behavior
  • Any place where your app might have been relying on MySQL's lenient behavior and silently getting wrong results
  • Step 5: Performance Tuning

    Your PostgreSQL instance won't be perfectly tuned out of the box. Key things to look at:

  • postgresql.conf settings: shared_buffers, work_mem, max_connections, effective_cache_size — these defaults are conservative and need tuning for production.
  • Indexes: Your MySQL indexes migrate, but you should review and potentially add PostgreSQL-specific index types (GIN indexes for JSONB, for example).
  • VACUUM and AUTOVACUUM: Postgres requires periodic maintenance that MySQL doesn't. Make sure autovacuum is properly configured.
  • EXPLAIN ANALYZE: Use this heavily to understand query plans and find slow queries.

  • Common Errors You'll Hit (And How to Fix Them)

    "column must appear in the GROUP BY clause"

    You had a loose GROUP BY in MySQL. In Postgres, if you select a column, it must either be in GROUP BY or be wrapped in an aggregate function.

    -- MySQL allows this, Postgres doesn't
    SELECT name, MAX(salary), department FROM employees GROUP BY department;
    
    -- Fix: add name to GROUP BY or use an aggregate
    SELECT MAX(name), MAX(salary), department FROM employees GROUP BY department;

    "operator does not exist: integer = text"

    Postgres is strict about type matching. MySQL would silently cast. You need to be explicit.

    -- Fix: cast explicitly
    WHERE id = '123'::integer
    -- or better, fix the application code to send the right type

    "invalid input syntax for type boolean"

    You're sending 1 or 0 where Postgres expects true/false.

    "syntax error at or near 'LIMIT'"

    Postgres doesn't support LIMIT without SELECT in some edge cases. Also check if you have raw MySQL syntax like LIMIT 10, 5 (offset first) — Postgres uses LIMIT 5 OFFSET 10.


    Should You Use a Migration Tool or Do It By Hand?

    Use a tool if:

  • Your database is large (millions of rows)
  • You have complex schemas with many foreign keys, triggers, and views
  • You need a live migration with minimal downtime
  • Go manual if:

  • Your database is relatively small
  • You want full control and understanding of every change
  • You have significant custom logic that tools might mangle
  • In practice, most teams use pgLoader for the heavy lifting and then fix things up manually. It's a good balance.


    Post-Migration Checklist

    Before calling it done, go through this:

  • [ ] All tables, indexes, and constraints are present in PostgreSQL
  • [ ] Row counts match between MySQL and PostgreSQL
  • [ ] Application connects and authenticates successfully
  • [ ] Core user flows work end-to-end
  • [ ] Background jobs and cron tasks work
  • [ ] Logging and monitoring is set up for the new database
  • [ ] Backups are configured and tested
  • [ ] PostgreSQL performance is benchmarked and baselines are set
  • [ ] Rollback plan is documented and tested

  • The Honest Truth About Timelines

    Small databases (< 10GB, simple schema): 1–3 days of focused effort.

    Medium databases (10–100GB, moderate complexity): 1–3 weeks, including testing.

    Large or complex databases (100GB+, lots of stored procedures, strict zero-downtime requirements): Plan for months, not days.

    Don't let anyone pressure you into an unrealistic timeline. A botched migration is far more expensive than a careful, slow one.


    Wrapping Up

    Migrating from MySQL to PostgreSQL isn't the kind of task you knock out in an afternoon, but it's absolutely doable — and for most teams, worth it. PostgreSQL is a genuinely powerful database that will serve you well for years.

    The key is to respect the differences between the two systems, plan thoroughly, test obsessively, and have a rollback plan ready. Most migrations that go wrong aren't because of technical impossibilities — they're because someone rushed.

    Take your time, lean on tools like pgLoader, fix your type mismatches and GROUP BY issues, and tune your Postgres config before going live. Follow those steps and you'll come out the other side with a faster, more reliable database and a story to tell at your next team retrospective.

    Good luck — you've got this.


    *Have questions or ran into a migration issue not covered here? Drop a comment below or reach out through the [SQLMarrow community](https://sqlmarrow.com). We're always happy to dig into the details.*

    Frequently Asked Questions

    How long does it take to migrate from MySQL to PostgreSQL?
    Timeline varies significantly: schema-only migration: 1-2 days. Small database (<10GB, few stored procedures): 1 week. Medium database (10-100GB, complex application): 2-4 weeks. Large database (>100GB, many stored procedures/triggers): 1-3 months. The biggest time investment is usually rewriting stored procedures and fixing GROUP BY strictness issues in application queries.
    What is the best tool to migrate MySQL to PostgreSQL?
    pgloader is the most popular free tool for MySQL → PostgreSQL migration. It handles data type conversions automatically, migrates data efficiently in parallel, and provides detailed error reports. For cloud environments, AWS DMS (Database Migration Service) supports continuous replication for zero-downtime migrations.
    Do I need to change my SQL queries after migrating from MySQL to PostgreSQL?
    Yes, almost certainly. The most common changes needed: 1) Fix GROUP BY strictness (add all non-aggregated columns). 2) Replace backtick quoting with double quotes. 3) Replace MySQL-specific functions (DATE_FORMAT, IFNULL, REPLACE INTO). 4) Update INSERT IGNORE to ON CONFLICT DO NOTHING. 5) Fix string quoting (double quotes become identifiers in PostgreSQL). Run your full test suite against PostgreSQL to catch all issues.
    Can I run MySQL and PostgreSQL simultaneously during migration?
    Yes. A dual-write or shadow-write strategy runs both databases in parallel during the migration period. New writes go to both MySQL and PostgreSQL. This allows validation of data consistency before the final cutover. pglogical or AWS DMS can handle continuous replication from MySQL to PostgreSQL during this period.

    Ready to Migrate?

    Test your PostgreSQL queries in our free SQL Playground before migrating.