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
INTINTEGER or INT4Identical behaviorBIGINTBIGINT or INT8IdenticalTINYINT(1)BOOLEANMySQL uses TINYINT(1) for booleans; PostgreSQL has native BOOLEAN typeAUTO_INCREMENTSERIAL or GENERATED ALWAYS AS IDENTITYPostgreSQL IDENTITY columns are the modern standardVARCHAR(n)VARCHAR(n)Identical, but PostgreSQL TEXT is often preferred over VARCHAR with no limitTEXTTEXTIdenticalDATETIMETIMESTAMPPostgreSQL uses TIMESTAMP for datetime valuesDATETIME with timezoneTIMESTAMPTZUse TIMESTAMPTZ for timezone-aware timestamps in PostgreSQLUNSIGNED 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 attributesJSONJSON or JSONBUse JSONB for indexable, efficient JSON storage in PostgreSQLBLOBBYTEA or Large ObjectsBYTEA for up to ~1GB; Large Objects for larger binary dataMEDIUMTEXT / LONGTEXTTEXTPostgreSQL TEXT has no size limitDOUBLEDOUBLE PRECISION or FLOAT8Same semanticsDECIMAL(p,s)NUMERIC(p,s)Functionally identical; NUMERIC is the standard SQL nameSQL Syntax Differences
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
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
MySQL
SELECT `column` FROM `table`
PostgreSQL
SELECT "column" FROM "table"
Replace all backticks with double quotes
MySQL
SELECT * FROM t LIMIT 10 OFFSET 20
PostgreSQL
SELECT * FROM t LIMIT 10 OFFSET 20
Identical syntax — no change needed
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
MySQL
NOW(), DATE_FORMAT(), DATEDIFF()
PostgreSQL
NOW(), TO_CHAR(), date - date
Date functions differ significantly between MySQL and PostgreSQL
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
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
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
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
MySQL
Not supported natively (requires UNION workaround)
PostgreSQL
Natively supported: FULL OUTER JOIN
PostgreSQL supports FULL OUTER JOIN directly
MySQL
IFNULL(col, 'default')
PostgreSQL
COALESCE(col, 'default')
COALESCE is the SQL standard; works in both databases
Step-by-Step Migration Guide
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
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
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 );
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';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;
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.
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;
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;
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
FREEOpen-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
FREEFree PostgreSQL GUI for schema management and query testing.
Flyway / Liquibase
FREESchema version management tools for managing schema changes post-migration.
Frequently Asked Questions
How long does it take to migrate from MySQL to PostgreSQL?
What is the best tool to migrate MySQL to PostgreSQL?
Do I need to change my SQL queries after migrating from MySQL to PostgreSQL?
Can I run MySQL and PostgreSQL simultaneously during migration?
Ready to Migrate?
Test your PostgreSQL queries in our free SQL Playground before migrating.