<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rehman Afzal</title>
    <description>The latest articles on DEV Community by Rehman Afzal (@rehman_afzal_536).</description>
    <link>https://dev.to/rehman_afzal_536</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3913468%2Ff8f89a98-83e1-479c-9749-a6e7331b6a5b.jpg</url>
      <title>DEV Community: Rehman Afzal</title>
      <link>https://dev.to/rehman_afzal_536</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rehman_afzal_536"/>
    <language>en</language>
    <item>
      <title>How to Migrate MySQL to PostgreSQL Without Breaking Everything (With Real Examples)</title>
      <dc:creator>Rehman Afzal</dc:creator>
      <pubDate>Tue, 05 May 2026 08:09:40 +0000</pubDate>
      <link>https://dev.to/rehman_afzal_536/how-to-migrate-mysql-to-postgresql-without-breaking-everything-with-real-examples-2m21</link>
      <guid>https://dev.to/rehman_afzal_536/how-to-migrate-mysql-to-postgresql-without-breaking-everything-with-real-examples-2m21</guid>
      <description>&lt;p&gt;So you've outgrown MySQL. Maybe you need better JSON support, real window functions, or you're moving to a managed cloud database that defaults to Postgres.&lt;/p&gt;

&lt;p&gt;Whatever the reason — &lt;a href="https://dbconverter.site/sql-to-postgresql" rel="noopener noreferrer"&gt;MySQL to PostgreSQL&lt;/a&gt; migration trips up almost everyone the first time. The two dialects look similar but behave very differently under the hood.&lt;br&gt;
This guide walks through the actual syntax differences, real failure points, and how to convert a MySQL dump cleanly.&lt;/p&gt;

&lt;p&gt;Why MySQL Dumps Don't Import Directly into PostgreSQL&lt;br&gt;
Here's a typical MySQL dump:&lt;br&gt;
sqlCREATE TABLE &lt;code&gt;users&lt;/code&gt; (&lt;br&gt;
  &lt;code&gt;id&lt;/code&gt; INT(11) NOT NULL AUTO_INCREMENT,&lt;br&gt;
  &lt;code&gt;username&lt;/code&gt; VARCHAR(100) NOT NULL,&lt;br&gt;
  &lt;code&gt;is_active&lt;/code&gt; TINYINT(1) DEFAULT 1,&lt;br&gt;
  &lt;code&gt;created_at&lt;/code&gt; DATETIME DEFAULT CURRENT_TIMESTAMP,&lt;br&gt;
  PRIMARY KEY (&lt;code&gt;id&lt;/code&gt;)&lt;br&gt;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;&lt;br&gt;
Paste this into psql and you get errors immediately. Here is every line that breaks and why:&lt;/p&gt;

&lt;p&gt;Backtick identifiers — not valid SQL standard, PostgreSQL uses double quotes&lt;br&gt;
AUTO_INCREMENT — MySQL-specific keyword, PostgreSQL uses SERIAL&lt;br&gt;
TINYINT(1) — no equivalent type, PostgreSQL uses BOOLEAN&lt;br&gt;
DATETIME — works differently, PostgreSQL uses TIMESTAMP&lt;br&gt;
ENGINE=InnoDB — MySQL storage engine declaration, remove entirely&lt;br&gt;
DEFAULT CHARSET=utf8mb4 — MySQL encoding clause, remove entirely&lt;/p&gt;

&lt;p&gt;The same table written for PostgreSQL:&lt;br&gt;
sqlCREATE TABLE "users" (&lt;br&gt;
  "id" SERIAL NOT NULL,&lt;br&gt;
  "username" VARCHAR(100) NOT NULL,&lt;br&gt;
  "is_active" BOOLEAN DEFAULT TRUE,&lt;br&gt;
  "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,&lt;br&gt;
  PRIMARY KEY ("id")&lt;br&gt;
);&lt;br&gt;
Six things changed. Let's go through each one properly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identifiers — Backticks to Double Quotes
MySQL uses backticks. PostgreSQL uses double quotes, or no quotes at all for simple lowercase names.
sql-- MySQL
SELECT &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;user_name&lt;/code&gt; FROM &lt;code&gt;orders&lt;/code&gt;;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- PostgreSQL&lt;br&gt;
SELECT "id", "user_name" FROM "orders";&lt;/p&gt;

&lt;p&gt;-- or just (if names are simple lowercase)&lt;br&gt;
SELECT id, user_name FROM orders;&lt;/p&gt;

&lt;p&gt;Gotcha: PostgreSQL is case-sensitive with quoted identifiers. "UserName" and "username" are two different columns. If your MySQL schema used mixed-case names, decide upfront whether to keep the quotes or normalize everything to lowercase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auto Increment — AUTO_INCREMENT to SERIAL
sql-- MySQL
id INT NOT NULL AUTO_INCREMENT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- PostgreSQL (classic, still works everywhere)&lt;br&gt;
id SERIAL NOT NULL&lt;/p&gt;

&lt;p&gt;-- PostgreSQL (modern SQL standard, preferred)&lt;br&gt;
id INT GENERATED ALWAYS AS IDENTITY&lt;br&gt;
SERIAL is shorthand that creates a sequence and wires it as the default. GENERATED ALWAYS AS IDENTITY is the proper SQL standard approach and prevents accidental manual inserts from overwriting the sequence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Booleans — TINYINT(1) to BOOLEAN
MySQL has no native boolean type. It uses TINYINT(1) with 0 and 1. PostgreSQL has a real BOOLEAN type.
sql-- MySQL
is_active TINYINT(1) DEFAULT 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- PostgreSQL&lt;br&gt;
is_active BOOLEAN DEFAULT TRUE&lt;br&gt;
Your INSERT statements need updating too:&lt;br&gt;
sql-- MySQL&lt;br&gt;
INSERT INTO users (is_active) VALUES (1);&lt;/p&gt;

&lt;p&gt;-- PostgreSQL&lt;br&gt;
INSERT INTO users (is_active) VALUES (TRUE);&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Date and Time — DATETIME to TIMESTAMP
sql-- MySQL
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- PostgreSQL&lt;br&gt;
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP&lt;br&gt;
-- ON UPDATE has no PostgreSQL equivalent — use a trigger instead&lt;br&gt;
If you need ON UPDATE CURRENT_TIMESTAMP behavior, you need a trigger in PostgreSQL:&lt;br&gt;
sqlCREATE OR REPLACE FUNCTION update_updated_at()&lt;br&gt;
RETURNS TRIGGER AS $$&lt;br&gt;
BEGIN&lt;br&gt;
  NEW.updated_at = NOW();&lt;br&gt;
  RETURN NEW;&lt;br&gt;
END;&lt;br&gt;
$$ LANGUAGE plpgsql;&lt;/p&gt;

&lt;p&gt;CREATE TRIGGER set_updated_at&lt;br&gt;
BEFORE UPDATE ON users&lt;br&gt;
FOR EACH ROW&lt;br&gt;
EXECUTE FUNCTION update_updated_at();&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Functions That Have Different Names&lt;br&gt;
Some common MySQL functions either do not exist or have different names in PostgreSQL:&lt;br&gt;
sql-- MySQL                              -- PostgreSQL&lt;br&gt;
IFNULL(col, 'default')          →    COALESCE(col, 'default')&lt;br&gt;
GROUP_CONCAT(col)               →    STRING_AGG(col, ',')&lt;br&gt;
LIMIT 10, 20                    →    LIMIT 10 OFFSET 20&lt;br&gt;
RAND()                          →    RANDOM()&lt;br&gt;
NOW() is the same in both — one less thing to change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;INSERT IGNORE and ON DUPLICATE KEY&lt;br&gt;
sql-- MySQL: silently skip duplicates&lt;br&gt;
INSERT IGNORE INTO users (email) VALUES ('&lt;a href="mailto:a@b.com"&gt;a@b.com&lt;/a&gt;');&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;-- MySQL: upsert&lt;br&gt;
INSERT INTO users (id, email)&lt;br&gt;
VALUES (1, '&lt;a href="mailto:a@b.com"&gt;a@b.com&lt;/a&gt;')&lt;br&gt;
ON DUPLICATE KEY UPDATE email = VALUES(email);&lt;br&gt;
sql-- PostgreSQL: skip duplicates&lt;br&gt;
INSERT INTO users (email) VALUES ('&lt;a href="mailto:a@b.com"&gt;a@b.com&lt;/a&gt;')&lt;br&gt;
ON CONFLICT DO NOTHING;&lt;/p&gt;

&lt;p&gt;-- PostgreSQL: upsert&lt;br&gt;
INSERT INTO users (id, email)&lt;br&gt;
VALUES (1, '&lt;a href="mailto:a@b.com"&gt;a@b.com&lt;/a&gt;')&lt;br&gt;
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;&lt;/p&gt;

&lt;p&gt;The Migration Process Step by Step&lt;br&gt;
Step 1 — Export cleanly from MySQL&lt;br&gt;
bashmysqldump -u root -p \&lt;br&gt;
  --no-tablespaces \&lt;br&gt;
  --skip-add-locks \&lt;br&gt;
  --skip-comments \&lt;br&gt;
  mydb &amp;gt; mydb_dump.sql&lt;br&gt;
The --no-tablespaces and --skip-add-locks flags produce a cleaner dump that is much easier to transform.&lt;br&gt;
Step 2 — Convert the dump&lt;br&gt;
For small schemas you can use sed for the basics:&lt;br&gt;
bashsed -i 's/`/"/g' mydb_dump.sql&lt;br&gt;
sed -i 's/AUTO_INCREMENT/SERIAL/g' mydb_dump.sql&lt;br&gt;
sed -i 's/TINYINT(1)/BOOLEAN/g' mydb_dump.sql&lt;br&gt;
sed -i 's/ ENGINE=InnoDB[^;]*//g' mydb_dump.sql&lt;br&gt;
sed -i 's/ DEFAULT CHARSET=utf8mb4//g' mydb_dump.sql&lt;br&gt;
For anything larger or more complex, sed will miss edge cases — multiline statements, character encoding issues, nested quoting. I use &lt;a href="https://dbconverter.site/" rel="noopener noreferrer"&gt;DBConverter&lt;/a&gt; for this: upload the dump, pick PostgreSQL as the target, and download a file that is ready to import. It handles the type mapping, identifier rewriting, and engine clause removal automatically.&lt;br&gt;
Step 3 — Always import into staging first&lt;br&gt;
bashpsql -U postgres -d mydb_staging -f mydb_dump_converted.sql&lt;br&gt;
Never import directly into production. Check for errors, run your test suite, verify row counts match your source.&lt;br&gt;
Step 4 — Resync your sequences&lt;br&gt;
After importing data with explicit IDs, your SERIAL sequences will be out of sync. Fix each one:&lt;br&gt;
sqlSELECT setval(&lt;br&gt;
  pg_get_serial_sequence('users', 'id'),&lt;br&gt;
  (SELECT MAX(id) FROM users)&lt;br&gt;
);&lt;br&gt;
Run this for every table with a serial column or you will hit duplicate key errors on the first insert.&lt;br&gt;
Step 5 — Audit your queries&lt;br&gt;
Run EXPLAIN ANALYZE on your slowest queries. PostgreSQL's planner is different and you may need new indexes. Watch out for:&lt;/p&gt;

&lt;p&gt;LIKE '%term%' — PostgreSQL will not use a B-tree index on a leading wildcard, use full-text search instead&lt;br&gt;
GROUP BY is stricter — every non-aggregated column must be in the GROUP BY clause&lt;br&gt;
String comparison is case-sensitive — WHERE name = 'Alice' will not match 'alice' in PostgreSQL&lt;/p&gt;

&lt;p&gt;Common Import Errors and What They Mean&lt;br&gt;
syntax error at or near "'" around backtick positions&lt;br&gt;
→ Backticks were not fully replaced. Find and replace all backticks with double quotes.&lt;br&gt;
column "is_active" is of type boolean but expression is of type integer&lt;br&gt;
→ Your INSERT statements still use 1 and 0. Replace with TRUE and FALSE.&lt;br&gt;
function ifnull(...) does not exist&lt;br&gt;
→ Replace IFNULL with COALESCE throughout.&lt;br&gt;
syntax error at or near "ENGINE"&lt;br&gt;
→ The ENGINE=InnoDB tail was not stripped from your CREATE TABLE statements.&lt;br&gt;
duplicate key value violates unique constraint on first insert after migration&lt;br&gt;
→ Sequence is out of sync. Run the setval fix from Step 4 above.&lt;/p&gt;

&lt;p&gt;Summary&lt;br&gt;
The six things you must handle in every MySQL to PostgreSQL migration:&lt;/p&gt;

&lt;p&gt;Backticks → double quotes&lt;br&gt;
AUTO_INCREMENT → SERIAL&lt;br&gt;
TINYINT(1) → BOOLEAN&lt;br&gt;
DATETIME → TIMESTAMP&lt;br&gt;
MySQL-specific functions → PostgreSQL equivalents&lt;br&gt;
INSERT IGNORE / ON DUPLICATE KEY → ON CONFLICT&lt;/p&gt;

&lt;p&gt;For small schemas, the sed commands above get you most of the way. For anything production-grade, let a tool like &lt;a href="https://dbconverter.site/" rel="noopener noreferrer"&gt;DBConverter&lt;/a&gt; handle the rewriting so you can focus on testing the result rather than editing SQL line by line.&lt;/p&gt;

&lt;p&gt;Hit a migration edge case not covered here? Drop it in the comments.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgressql</category>
      <category>mysql</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
