Migration Reference

Syntax and behavior differences when moving to PostgreSQL

ConceptMySQLPostgreSQL
Data Types
IntegerINTINTEGER
Large integerBIGINTBIGINT
Small integerTINYINT / SMALLINTSMALLINT
Unsigned integerINT UNSIGNEDINTEGER with CHECK (col >= 0)
BooleanTINYINT(1)BOOLEAN
Variable-length stringVARCHAR(n)VARCHAR(n) or TEXT
Unlimited textTEXT / MEDIUMTEXT / LONGTEXTTEXT
Date and timeDATETIMETIMESTAMP
Timezone-aware timestampTIMESTAMP (UTC-stored)TIMESTAMPTZ
Floating pointFLOAT / DOUBLEREAL (4-byte) / DOUBLE PRECISION (8-byte)
Exact decimalDECIMAL(p,s)NUMERIC(p,s)
Binary dataBLOB / MEDIUMBLOB / LONGBLOBBYTEA
UUIDVARCHAR(36)UUID
Enumerated typeENUM('a','b','c')CREATE TYPE name AS ENUM ('a','b','c')
JSONJSONJSONB
Array(not supported)INTEGER[], TEXT[], etc.
Auto-increment
Auto-incrementing primary keyINT AUTO_INCREMENT PRIMARY KEYINTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
Serial shorthandINT AUTO_INCREMENTSERIAL
Get last inserted IDLAST_INSERT_ID()RETURNING id
DDL
List tablesSHOW TABLES\dt or SELECT tablename FROM pg_tables WHERE schemaname = 'public'
Describe table structureDESCRIBE tablename or SHOW COLUMNS FROM tablename\d tablename or SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'name'
List databasesSHOW DATABASES\l or SELECT datname FROM pg_database
Switch databaseUSE dbname\c dbname (psql only — reconnect required)
Identifier quoting`backticks`"double quotes"
Conditional createCREATE TABLE IF NOT EXISTSCREATE TABLE IF NOT EXISTS
Add columnALTER TABLE t ADD COLUMN c INTALTER TABLE t ADD COLUMN c INTEGER
Rename columnALTER TABLE t CHANGE old_name new_name INTALTER TABLE t RENAME COLUMN old_name TO new_name
Change column typeALTER TABLE t MODIFY COLUMN c VARCHAR(255)ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(255)
DML
Upsert (insert or update)INSERT ... ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT DO UPDATE SET ...
Limit rows returnedSELECT ... LIMIT 10SELECT ... LIMIT 10
Limit with offsetLIMIT 10, 20 (offset, count)LIMIT 20 OFFSET 10
Return values from INSERT/UPDATE/DELETELAST_INSERT_ID() (insert only)RETURNING col1, col2
Truncate tableTRUNCATE TABLE tTRUNCATE TABLE t or TRUNCATE TABLE t RESTART IDENTITY CASCADE
String Functions
String concatenationCONCAT(a, b) or (no || by default)a || b or CONCAT(a, b)
Null substitutionIFNULL(val, default)COALESCE(val, default)
String lengthLENGTH() (bytes) / CHAR_LENGTH() (chars)LENGTH() (chars) / OCTET_LENGTH() (bytes)
Find substring positionLOCATE(substr, str)POSITION(substr IN str) or STRPOS(str, substr)
Extract substringSUBSTR(str, pos, len) or SUBSTRING(str, pos, len)SUBSTRING(str FROM pos FOR len) or SUBSTR(str, pos, len)
Replace substringREPLACE(str, from, to)REPLACE(str, from, to)
Upper/lower caseUPPER() / LOWER()UPPER() / LOWER()
Pad stringLPAD(str, len, pad) / RPAD(str, len, pad)LPAD(str, len, pad) / RPAD(str, len, pad)
Regex matchcol REGEXP 'pattern'col ~ 'pattern' (case-sensitive) col ~* 'pattern' (case-insensitive) col !~ 'pattern' (not match)
Split stringNo split — use SUBSTRING_INDEX()STRING_TO_TABLE(str, delim) (PG16+) or regexp_split_to_table(str, pattern)
Date & Time
Current timestampNOW() or CURRENT_TIMESTAMPNOW() or CURRENT_TIMESTAMP
Current dateCURDATE() or CURRENT_DATECURRENT_DATE
Add interval to dateDATE_ADD(date, INTERVAL 1 DAY) DATE_ADD(date, INTERVAL 2 MONTH)date + INTERVAL '1 day' date + INTERVAL '2 months'
Difference between datesDATEDIFF(end, start) (days only) TIMESTAMPDIFF(unit, start, end)end::date - start::date (days) EXTRACT(EPOCH FROM (end - start)) (seconds)
Extract part of dateYEAR(d) / MONTH(d) / DAY(d) DATE_PART('year', d)EXTRACT(YEAR FROM d) DATE_PART('year', d)
Format date as stringDATE_FORMAT(d, '%Y-%m-%d')TO_CHAR(d, 'YYYY-MM-DD')
Parse string to dateSTR_TO_DATE('15/01/2024', '%d/%m/%Y')TO_DATE('15/01/2024', 'DD/MM/YYYY') TO_TIMESTAMP('15/01/2024 10:30', 'DD/MM/YYYY HH24:MI')
Unix timestamp (epoch)UNIX_TIMESTAMP() FROM_UNIXTIME(epoch)EXTRACT(EPOCH FROM NOW()) TO_TIMESTAMP(epoch)
Truncate to time unitDATE(NOW()) (day only) DATE_FORMAT(d, '%Y-%m-01') (month)DATE_TRUNC('month', d) DATE_TRUNC('hour', d)
Conditional
IF expressionIF(condition, true_val, false_val)CASE WHEN condition THEN true_val ELSE false_val END
Return NULL if equalNULLIF(a, b)NULLIF(a, b)
Greatest / Least of valuesGREATEST(a, b, c) / LEAST(a, b, c)GREATEST(a, b, c) / LEAST(a, b, c)
Aggregates
Aggregate stringsGROUP_CONCAT(col SEPARATOR ', ')STRING_AGG(col, ', ')
Count distinctCOUNT(DISTINCT col)COUNT(DISTINCT col)
Arbitrary value from groupANY_VALUE(col)ANY_VALUE(col) (PG16+) or MIN(col) / MAX(col)
Aggregate into JSON arrayJSON_ARRAYAGG(col)JSON_AGG(col) or JSONB_AGG(col)
JSON
Extract JSON valueJSON_EXTRACT(doc, '$.key') doc->>'$.key'doc->>'key' (text) doc->'key' (jsonb)
JSON contains checkJSON_CONTAINS(doc, '"value"', '$.key')doc @> '{"key": "value"}'
Set JSON valueJSON_SET(doc, '$.key', 'value')jsonb_set(doc, '{key}', '"value"')
Build JSON objectJSON_OBJECT('k', v)jsonb_build_object('k', v) JSON_OBJECT('k': v) (PG17+)
Indexes
Full-text search indexFULLTEXT INDEXCREATE INDEX ON t USING GIN (to_tsvector('english', col))
Partial index(not supported)CREATE INDEX ON t (col) WHERE condition
Expression / function indexCREATE INDEX ON t ((LOWER(col)))CREATE INDEX ON t (LOWER(col))
Non-blocking index build(online DDL in some versions)CREATE INDEX CONCURRENTLY ON t (col)
Transactions
Start transactionBEGIN or START TRANSACTIONBEGIN
SavepointSAVEPOINT nameSAVEPOINT name ROLLBACK TO SAVEPOINT name RELEASE SAVEPOINT name
Set isolation levelSET TRANSACTION ISOLATION LEVEL SERIALIZABLEBEGIN ISOLATION LEVEL SERIALIZABLE
Users & Auth
Create a userCREATE USER 'alice'@'%' IDENTIFIED BY 'pass'CREATE ROLE alice LOGIN PASSWORD 'pass'
Grant table accessGRANT SELECT ON db.table TO 'alice'@'%'GRANT SELECT ON table TO alice
Grant all on schemaGRANT ALL ON db.* TO 'alice'@'%'GRANT ALL ON ALL TABLES IN SCHEMA public TO alice GRANT USAGE ON SCHEMA public TO alice
Apply privilege changesFLUSH PRIVILEGES(automatic — no FLUSH needed)
Gotchas
Case-sensitive string comparisonCase-insensitive by default (latin1_swedish_ci)Case-sensitive by default
Empty string vs NULL'' ≠ NULL (but some contexts treat them similarly)'' ≠ NULL (strictly different)
String truncationSilently truncates strings that exceed column widthRaises error (22001: string_data_right_truncation)
Zero date'0000-00-00' is a valid special valueNot valid — use NULL instead
Integer division5 / 2 = 2 (integer division)5 / 2 = 2 (integer division)
GROUP BY strictnessAllows selecting non-aggregated columns not in GROUP BY (ONLY_FULL_GROUP_BY optional)Strict — all non-aggregated columns must be in GROUP BY
Backslash in stringsBackslash is an escape character by defaultBackslash is a literal character (standard_conforming_strings=on by default since PG9.1)
Schemas vs databasesSchema and database are synonymousSchemas are namespaces within a database. public is the default schema.