MySQL vs PostgreSQL: Comprehensive Guide for MySQL Users Learning PostgreSQL

Uncategorized

Introduction

If you already know MySQL well, learning PostgreSQL becomes much easier when you stop trying to memorize PostgreSQL from zero and instead ask:

“What is the PostgreSQL equivalent of the thing I already know in MySQL?”

That is exactly how this guide is structured.

We will compare MySQL and PostgreSQL components, resources, architecture, commands, tools, users, roles, schemas, storage, logs, backup, replication, monitoring, performance, and SQL syntax.

The goal is not just to say:

MySQL thing = PostgreSQL thing

The goal is to understand:

What does it mean?
How does it behave?
How should I think about it as a MySQL user?

1. Big Picture Mental Model

MySQL Mental Model

In MySQL, you usually think like this:

MySQL Server
 ├── Database / Schema
 │    ├── Tables
 │    ├── Views
 │    ├── Procedures
 │    └── Triggers
 ├── Users
 ├── Privileges
 ├── Storage Engines
 │    ├── InnoDB
 │    ├── MyISAM
 │    └── MEMORY
 └── Logs
      ├── Error log
      ├── Binary log
      ├── Slow query log
      └── General log

PostgreSQL Mental Model

In PostgreSQL, think like this:

PostgreSQL Server / Cluster
 ├── Databases
 │    ├── Schemas
 │    │    ├── Tables
 │    │    ├── Views
 │    │    ├── Functions
 │    │    ├── Sequences
 │    │    └── Types
 ├── Roles
 │    ├── Login roles
 │    └── Group roles
 ├── Tablespaces
 ├── WAL
 ├── Background processes
 └── Extensions

The most important difference:

MySQL:
Database and schema usually mean the same thing.

PostgreSQL:
A database contains schemas.
A schema contains tables.

So:

MySQL:
server → database → table

PostgreSQL:
server/cluster → database → schema → table

2. Core Component Mapping

MySQL ComponentPostgreSQL EquivalentMeaning
MySQL serverPostgreSQL server / clusterRunning database service
DatabaseDatabaseLogical database container
SchemaSchema inside databaseNamespace inside a database
TableTableStores rows and columns
ViewViewSaved query
Materialized viewMaterialized viewStored query result
UserRole with LOGINPostgreSQL users are login roles
GroupRole without LOGINUsed to group permissions
PrivilegePrivilegeControls access
Storage enginePostgreSQL storage systemPostgreSQL does not use engines like MySQL
InnoDB redo logWALWrite-Ahead Log
Binary logWAL / logical replicationUsed for recovery and replication
AUTO_INCREMENTIdentity / sequenceAuto-generated numbers
Performance Schemapg_stat_* viewsMonitoring and metrics
my.cnfpostgresql.confMain config
User host grantspg_hba.confClient authentication rules
mysqldumppg_dumpBackup tool
mysql CLIpsqlCommand-line client

3. Server, Instance, and Cluster

MySQL

In MySQL, you usually say:

MySQL instance
MySQL server
mysqld process

A MySQL instance usually means one running MySQL server process using one data directory.

Example:

mysqld

PostgreSQL

PostgreSQL commonly uses the word cluster.

A PostgreSQL cluster means:

One PostgreSQL data directory initialized by initdb

It contains:

Databases
Roles
WAL
System catalogs
Configuration

Important: in PostgreSQL, a cluster does not mean multiple servers in this context.

That confuses many people.

In PostgreSQL:

Cluster = one initialized PostgreSQL data directory

Example data directory:

/var/lib/postgresql/16/main

or:

/var/lib/pgsql/data

depending on Linux distribution.


4. Database and Schema Mapping

MySQL Database

In MySQL:

CREATE DATABASE shop;
USE shop;
CREATE TABLE customers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);

Here, shop is the database.

Many MySQL people also call this a schema.

So in MySQL:

Database ≈ Schema

PostgreSQL Database

In PostgreSQL:

CREATE DATABASE shop;

Then connect to it:

\c shop

Inside that database, you usually have a schema called:

public

Then you create tables inside the schema:

CREATE TABLE public.customers (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(100)
);

So in PostgreSQL:

Database contains schemas.
Schema contains tables.

PostgreSQL Object Path

A full table name in PostgreSQL can look like this:

database.schema.table

But inside SQL queries, you usually use:

SELECT * FROM public.customers;

You do not usually write the database name in front of the table because you are already connected to one database.

Important Difference

In MySQL, you can commonly do:

SELECT *
FROM db1.customers c
JOIN db2.orders o ON c.id = o.customer_id;

In PostgreSQL, cross-database queries are not naturally done this way. You usually work inside one database and use multiple schemas:

shop database
 ├── sales.customers
 ├── sales.orders
 ├── accounting.invoices
 └── audit.events

PostgreSQL encourages this style:

SELECT *
FROM sales.customers c
JOIN accounting.invoices i ON c.id = i.customer_id;

5. System Databases and System Schemas

MySQL System Databases

Common MySQL system databases:

MySQL DatabasePurpose
mysqlUsers, privileges, system metadata
information_schemaSQL-standard metadata views
performance_schemaPerformance and runtime metrics
sysFriendly views over performance schema

You may see:

SHOW DATABASES;

Output:

information_schema
mysql
performance_schema
sys
myapp

PostgreSQL System Databases and Schemas

PostgreSQL has special databases and schemas.

Common PostgreSQL databases:

PostgreSQL DatabasePurpose
postgresDefault administrative database
template0Clean database template
template1Default template for new databases

Common PostgreSQL schemas:

PostgreSQL SchemaPurpose
pg_catalogPostgreSQL system catalog
information_schemaSQL-standard metadata views
publicDefault user schema
pg_toastInternal storage for large values

In PostgreSQL, system metadata mostly lives in:

pg_catalog

Example:

SELECT *
FROM pg_catalog.pg_tables;

But as a beginner, you often use psql commands:

\l
\dt
\d table_name
\du

6. Users vs Roles

This is one of the biggest differences.

MySQL User

In MySQL, a user is identified by username and host:

'root'@'localhost'
'appuser'@'%'
'appuser'@'192.168.1.%'

Example:

CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'secret';
GRANT SELECT ON shop.* TO 'appuser'@'localhost';

In MySQL:

User identity = username + host

PostgreSQL Role

PostgreSQL has roles.

A role can be:

A user
A group
Both

A PostgreSQL “user” is simply:

A role with LOGIN permission

Example:

CREATE ROLE appuser WITH LOGIN PASSWORD 'secret';

This is equivalent to:

CREATE USER appuser WITH PASSWORD 'secret';

Because:

CREATE USER

is shorthand for:

CREATE ROLE ... LOGIN

PostgreSQL Group-Like Role

Create a role that cannot log in:

CREATE ROLE readonly;

Give permissions to that group role:

GRANT CONNECT ON DATABASE shop TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;

Assign it to a login role:

GRANT readonly TO appuser;

Now appuser inherits permissions from readonly.

MySQL vs PostgreSQL User Mapping

MySQLPostgreSQL
UserRole with LOGIN
Group of permissionsRole without LOGIN
'user'@'host'Role + pg_hba.conf
CREATE USERCREATE ROLE ... LOGIN
GRANT ON db.*GRANT ON DATABASE, SCHEMA, TABLES, SEQUENCES

Important PostgreSQL Access Layers

PostgreSQL access usually has four layers:

1. Can the client reach the server?
2. Does pg_hba.conf allow this connection?
3. Can the role connect to the database?
4. Does the role have privileges on schema/table/sequence?

This surprises MySQL users because MySQL grants often feel more direct.


7. Authentication Mapping

MySQL Authentication

MySQL controls user access through user accounts like:

'appuser'@'localhost'
'appuser'@'%'

Examples:

CREATE USER 'appuser'@'%' IDENTIFIED BY 'secret';
GRANT SELECT ON shop.* TO 'appuser'@'%';

PostgreSQL Authentication

PostgreSQL separates:

Who you are       → role
Can you log in    → LOGIN attribute
Where from/how    → pg_hba.conf
What you can do   → GRANT privileges

The file:

pg_hba.conf

controls connection rules.

Example:

host    shop    appuser    192.168.1.0/24    scram-sha-256

Meaning:

Allow appuser to connect to database shop
from 192.168.1.x
using password authentication.

Common PostgreSQL Authentication Methods

MethodMeaning
trustNo password required
peerOS user must match database role
md5Password authentication
scram-sha-256Stronger password authentication
certClient certificate authentication
ldapLDAP authentication
pamPAM authentication

For modern PostgreSQL, scram-sha-256 is commonly preferred over md5.


8. Privileges and Grants

MySQL Grant Style

GRANT SELECT, INSERT, UPDATE
ON shop.*
TO 'appuser'@'localhost';

This grants privileges on all tables in database shop.

PostgreSQL Grant Style

PostgreSQL is more layered.

You may need:

GRANT CONNECT ON DATABASE shop TO appuser;
GRANT USAGE ON SCHEMA public TO appuser;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO appuser;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO appuser;

Why sequences?

Because PostgreSQL auto-increment values are often backed by sequence objects.

PostgreSQL Default Privileges

If you want future tables to automatically get permissions:

ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE ON TABLES TO appuser;

For future sequences:

ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO appuser;

Permission Mapping

MySQL PermissionPostgreSQL Equivalent
SELECTSELECT
INSERTINSERT
UPDATEUPDATE
DELETEDELETE
CREATECREATE
DROPobject ownership / DROP as owner
ALTERobject ownership / specific privileges
INDEXusually table ownership
EXECUTEEXECUTE on functions
USAGEschema/sequence/type usage
ALL PRIVILEGESALL PRIVILEGES

9. Storage Engine vs PostgreSQL Storage

MySQL Storage Engines

MySQL supports multiple storage engines:

EngineMeaning
InnoDBDefault transactional engine
MyISAMOlder non-transactional engine
MEMORYStores data in memory
CSVStores data in CSV files
ARCHIVECompressed archival storage
FEDERATEDRemote table access

Most production MySQL systems use:

InnoDB

PostgreSQL Storage

PostgreSQL does not work the same way.

You do not normally choose:

ENGINE=InnoDB

There is no typical equivalent of:

CREATE TABLE t (...) ENGINE=InnoDB;

In PostgreSQL, table storage is native.

You choose table types instead:

PostgreSQL Table TypeMeaning
Regular tableNormal persistent table
Temporary tableSession-local temporary table
Unlogged tableFaster table, not WAL-logged
Partitioned tableLogical parent table with partitions
Foreign tableTable backed by external data source
Materialized viewStored result of a query

Example regular table:

CREATE TABLE customers (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
);

Example unlogged table:

CREATE UNLOGGED TABLE staging_imports (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload JSONB
);

Unlogged tables are faster for writes, but they are not crash-safe in the same way regular tables are.


10. Tablespaces

MySQL

In MySQL/InnoDB, tablespaces exist, but many users mostly deal with:

ibdata files
.ibd files
datadir

With innodb_file_per_table, each table can have its own .ibd file.

PostgreSQL

PostgreSQL has tablespaces too.

A tablespace lets you place database objects in a different filesystem location.

Example:

CREATE TABLESPACE fastspace
LOCATION '/mnt/fastdisk/postgres';

Create table in that tablespace:

CREATE TABLE big_events (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_data JSONB
) TABLESPACE fastspace;

Mental model:

Tablespace = storage location for database objects

11. Auto Increment vs Sequence / Identity

MySQL AUTO_INCREMENT

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);

Insert:

INSERT INTO users (name) VALUES ('Alice');

MySQL automatically generates id.

PostgreSQL Identity

Modern PostgreSQL style:

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(100)
);

Another style:

CREATE TABLE users (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(100)
);

Difference:

PostgreSQL Identity TypeMeaning
GENERATED ALWAYSPostgreSQL always generates value unless explicitly overridden
GENERATED BY DEFAULTPostgreSQL generates value if you do not provide one

Older PostgreSQL style:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

SERIAL still works, but identity columns are the modern SQL-standard style.

PostgreSQL Sequence

PostgreSQL identity and serial columns use sequences.

A sequence is an independent object that generates numbers.

Example:

CREATE SEQUENCE order_id_seq;

Get next value:

SELECT nextval('order_id_seq');

Important:

PostgreSQL sequences are not rolled back.

If a transaction gets a sequence number and then rolls back, that number may be skipped.

That is normal.


12. Data Type Mapping

MySQL TypePostgreSQL TypeNotes
TINYINTSMALLINTPostgreSQL has no TINYINT
TINYINT(1)BOOLEANCommon MySQL boolean conversion
SMALLINTSMALLINTSame
INTINTEGER / INTSame
BIGINTBIGINTSame
DECIMALNUMERIC / DECIMALSame purpose
FLOATREAL4-byte floating number
DOUBLEDOUBLE PRECISION8-byte floating number
CHAR(n)CHAR(n)Same
VARCHAR(n)VARCHAR(n)Same
TEXTTEXTPostgreSQL uses this heavily
DATEDATESame
DATETIMETIMESTAMPPostgreSQL equivalent
TIMESTAMPTIMESTAMP / TIMESTAMPTZPrefer TIMESTAMPTZ for real-world time
TIMETIMESame
BLOBBYTEABinary data
JSONJSON / JSONBJSONB is usually preferred
ENUMENUM / lookup tablePostgreSQL supports custom enum types
SETArray / join tableNo direct same feature
UUIDUUIDPostgreSQL has strong native UUID support

Boolean Difference

MySQL often uses:

is_active TINYINT(1)

PostgreSQL uses:

is_active BOOLEAN

Values:

TRUE
FALSE

Example:

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE
);

Date and Time Difference

MySQL:

created_at DATETIME

PostgreSQL:

created_at TIMESTAMP

Better for timezone-aware data:

created_at TIMESTAMPTZ

Important PostgreSQL note:

TIMESTAMPTZ does not store the timezone name.
It stores an absolute point in time and displays it according to session timezone.

JSON Difference

MySQL:

data JSON

PostgreSQL:

data JSONB

Example:

CREATE TABLE events (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload JSONB NOT NULL
);

Query:

SELECT *
FROM events
WHERE payload->>'type' = 'login';

JSONB indexing:

CREATE INDEX idx_events_payload
ON events USING GIN (payload);

13. SQL Syntax Mapping

Selecting Rows

Same:

SELECT *
FROM users;

Limit

Same:

SELECT *
FROM users
LIMIT 10;

Offset

Same:

SELECT *
FROM users
LIMIT 10 OFFSET 20;

String Concatenation

MySQL:

SELECT CONCAT(first_name, ' ', last_name) AS full_name
FROM users;

PostgreSQL:

SELECT first_name || ' ' || last_name AS full_name
FROM users;

PostgreSQL also supports:

SELECT CONCAT(first_name, ' ', last_name) AS full_name
FROM users;

Current Date and Time

MySQLPostgreSQL
NOW()NOW()
CURRENT_TIMESTAMPCURRENT_TIMESTAMP
CURDATE()CURRENT_DATE
CURTIME()CURRENT_TIME

MySQL:

SELECT CURDATE();

PostgreSQL:

SELECT CURRENT_DATE;

Case-Insensitive Search

MySQL often depends on collation:

SELECT *
FROM users
WHERE name LIKE '%john%';

PostgreSQL has ILIKE:

SELECT *
FROM users
WHERE name ILIKE '%john%';

Upsert

MySQL:

INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON DUPLICATE KEY UPDATE
    name = VALUES(name);

PostgreSQL:

INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email)
DO UPDATE SET
    name = EXCLUDED.name;

This is a major mapping:

MySQL ON DUPLICATE KEY UPDATE
=
PostgreSQL ON CONFLICT DO UPDATE

Replace

MySQL has:

REPLACE INTO users (id, name)
VALUES (1, 'Alice');

PostgreSQL does not have the same REPLACE INTO.

Use:

INSERT INTO users (id, name)
VALUES (1, 'Alice')
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name;

Insert Ignore

MySQL:

INSERT IGNORE INTO users (email, name)
VALUES ('alice@example.com', 'Alice');

PostgreSQL:

INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT DO NOTHING;

Group By Strictness

MySQL may allow loose grouping depending on SQL mode:

SELECT department, name, COUNT(*)
FROM employees
GROUP BY department;

PostgreSQL rejects this because name is neither grouped nor aggregated.

Correct PostgreSQL:

SELECT department, COUNT(*)
FROM employees
GROUP BY department;

This is important:

PostgreSQL is stricter and more SQL-standard in many areas.

14. Command-Line Tool Mapping

MySQL Client

mysql -u root -p

Connect to database:

mysql -u appuser -p shop

PostgreSQL Client

psql -U postgres

Connect to database:

psql -U appuser -d shop

Connect with host and port:

psql -h localhost -p 5432 -U appuser -d shop

Useful MySQL vs PostgreSQL Commands

TaskMySQLPostgreSQL
Show databasesSHOW DATABASES;\l
Use databaseUSE shop;\c shop
Show tablesSHOW TABLES;\dt
Describe tableDESCRIBE users;\d users
Show usersQuery mysql.user\du
Show current databaseSELECT DATABASE();SELECT current_database();
Show current userSELECT USER();SELECT current_user;
Show variablesSHOW VARIABLES;SHOW ALL;
Show process listSHOW PROCESSLIST;Query pg_stat_activity
Exitexit\q

PostgreSQL psql Commands

CommandMeaning
\lList databases
\c dbnameConnect to database
\dnList schemas
\dtList tables
\dvList views
\dmList materialized views
\dfList functions
\dsList sequences
\duList roles
\d table_nameDescribe table
\xExpanded output
\qQuit

Example:

\l
\c shop
\dn
\dt
\d users

15. Configuration Files

MySQL

Common MySQL config files:

/etc/my.cnf
/etc/mysql/my.cnf
/etc/mysql/mysql.conf.d/mysqld.cnf

Main config sections:

[mysqld]

[client]

[mysql]

Example:

[mysqld]
bind-address = 0.0.0.0
max_connections = 200
innodb_buffer_pool_size = 2G
slow_query_log = 1
long_query_time = 2

PostgreSQL

Main PostgreSQL config files:

postgresql.conf
pg_hba.conf
pg_ident.conf
FileMeaning
postgresql.confMain server settings
pg_hba.confHost-based authentication
pg_ident.confMaps OS users to DB users

Example postgresql.conf:

listen_addresses = '*'
max_connections = 200
shared_buffers = 2GB
work_mem = 16MB
maintenance_work_mem = 512MB
log_min_duration_statement = 2000

Example pg_hba.conf:

host    shop    appuser    192.168.1.0/24    scram-sha-256

16. Port and Service Mapping

ComponentMySQLPostgreSQL
Default port33065432
Server processmysqldpostgres
CLI clientmysqlpsql
Service namemysql / mysqldpostgresql
Admin databasemysqlpostgres

Check service:

systemctl status mysql

PostgreSQL:

systemctl status postgresql

17. Memory Resource Mapping

MySQL Important Memory Settings

MySQL SettingMeaning
innodb_buffer_pool_sizeMain InnoDB data/index cache
sort_buffer_sizePer-connection sort buffer
join_buffer_sizePer-connection join buffer
tmp_table_sizeMax internal temporary table size
max_heap_table_sizeMax MEMORY table size
key_buffer_sizeMyISAM index cache
max_connectionsMax client connections

Most important MySQL memory setting for InnoDB:

innodb_buffer_pool_size

PostgreSQL Important Memory Settings

PostgreSQL SettingMeaning
shared_buffersMain PostgreSQL buffer cache
work_memPer-operation memory for sort/hash
maintenance_work_memMemory for vacuum, create index, alter table
effective_cache_sizePlanner estimate of OS cache
wal_buffersMemory for WAL
max_connectionsMax client connections

Most important PostgreSQL memory settings:

shared_buffers
work_mem
maintenance_work_mem
effective_cache_size

Key Difference

MySQL/InnoDB heavily relies on:

InnoDB buffer pool

PostgreSQL relies on:

shared_buffers + operating system page cache

So do not map this too literally:

innodb_buffer_pool_size = shared_buffers

They are related, but not identical.


18. Connection Resource Mapping

MySQL

MySQL commonly uses threads for connections.

Command:

SHOW PROCESSLIST;

PostgreSQL

PostgreSQL traditionally uses a process per connection.

View active sessions:

SELECT *
FROM pg_stat_activity;

Useful query:

SELECT
    pid,
    usename,
    datname,
    state,
    query
FROM pg_stat_activity;

Connection Pooling

PostgreSQL often benefits from connection pooling because too many direct connections can become expensive.

Common pooler:

PgBouncer

Mental mapping:

MySQLPostgreSQL
Many app connections may be okay depending on setupUse connection pooling earlier
SHOW PROCESSLISTpg_stat_activity
Kill connection with KILL idSELECT pg_terminate_backend(pid);

PostgreSQL terminate session:

SELECT pg_terminate_backend(12345);

Cancel running query:

SELECT pg_cancel_backend(12345);

19. Transaction Mapping

MySQL

START TRANSACTION;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;

PostgreSQL

BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;

Same general concept.

Rollback:

ROLLBACK;

Important PostgreSQL Difference

In PostgreSQL, if an error happens inside a transaction, the transaction becomes failed.

Example:

BEGIN;

SELECT 1 / 0;

SELECT * FROM users;

PostgreSQL will say the transaction is aborted until you run:

ROLLBACK;

This surprises MySQL users.

Transactional DDL

PostgreSQL supports transactional DDL for many operations.

Example:

BEGIN;

CREATE TABLE test_table (
    id INT
);

ROLLBACK;

After rollback, the table is gone.

In MySQL, many DDL operations cause implicit commits.


20. MVCC Mapping

Both MySQL/InnoDB and PostgreSQL use MVCC.

MVCC means:

Multi-Version Concurrency Control

It allows readers and writers to avoid blocking each other in many cases.

MySQL/InnoDB MVCC

InnoDB stores undo information in undo logs.

Old row versions are reconstructed from undo data.

PostgreSQL MVCC

PostgreSQL stores multiple row versions directly in the table.

When you update a row, PostgreSQL creates a new row version.

Old versions are later cleaned up by:

VACUUM

Important PostgreSQL Concept: Dead Tuples

When PostgreSQL updates or deletes rows, old row versions remain for a while.

These old row versions are called:

dead tuples

They are cleaned by:

autovacuum

This is one of the biggest PostgreSQL operational topics.


21. Vacuum vs MySQL Purge

MySQL/InnoDB

InnoDB uses undo logs and purge threads to clean old row versions.

You usually do not manually run something called vacuum.

PostgreSQL

PostgreSQL needs vacuum.

Manual vacuum:

VACUUM users;

Vacuum and update statistics:

VACUUM ANALYZE users;

Full table rewrite:

VACUUM FULL users;

Important:

VACUUM FULL is heavier and takes stronger locks.
Do not treat it as a routine command.

Autovacuum

PostgreSQL normally runs autovacuum automatically.

Autovacuum handles:

Cleaning dead tuples
Preventing transaction ID wraparound
Updating table statistics

For MySQL users, think:

PostgreSQL autovacuum is not optional housekeeping.
It is essential database maintenance.

22. Index Mapping

Common Index Types

MySQL IndexPostgreSQL Equivalent
Primary keyPrimary key
Unique indexUnique index
Normal indexB-tree index
Composite indexComposite index
Full-text indexFull-text search index
Spatial indexGiST/SP-GiST, often with PostGIS
Functional indexExpression index
Prefix indexExpression or specialized index
Invisible indexNo exact same common equivalent

Basic Index

MySQL:

CREATE INDEX idx_users_email
ON users(email);

PostgreSQL:

CREATE INDEX idx_users_email
ON users(email);

Same.

Unique Index

CREATE UNIQUE INDEX idx_users_email_unique
ON users(email);

Composite Index

CREATE INDEX idx_orders_customer_status
ON orders(customer_id, status);

Same concept as MySQL.

Expression Index

PostgreSQL is excellent here.

CREATE INDEX idx_users_lower_email
ON users (LOWER(email));

Then query:

SELECT *
FROM users
WHERE LOWER(email) = LOWER('Alice@Example.com');

Partial Index

This is a powerful PostgreSQL feature.

CREATE INDEX idx_active_users_email
ON users(email)
WHERE is_active = TRUE;

This indexes only active users.

MySQL does not have the same general partial-index feature.

PostgreSQL Index Types

PostgreSQL Index TypeUse
B-treeDefault, equality/range
HashEquality
GINJSONB, arrays, full-text
GiSTGeometric, full-text, ranges
SP-GiSTPartitioned search structures
BRINVery large naturally ordered tables

Example JSONB GIN index:

CREATE INDEX idx_events_payload
ON events USING GIN (payload);

Example BRIN index:

CREATE INDEX idx_logs_created_at_brin
ON logs USING BRIN (created_at);

BRIN is useful for huge append-only tables where data is naturally ordered by time.


23. Explain Plan Mapping

MySQL

EXPLAIN
SELECT *
FROM users
WHERE email = 'alice@example.com';

MySQL also supports:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alice@example.com';

in newer versions.

PostgreSQL

EXPLAIN
SELECT *
FROM users
WHERE email = 'alice@example.com';

Better:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alice@example.com';

Even better for details:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM users
WHERE email = 'alice@example.com';

PostgreSQL Plan Terms

TermMeaning
Seq ScanFull table scan
Index ScanUses index and table
Index Only ScanUses only index if possible
Bitmap Index ScanBuilds bitmap from index
Nested LoopJoin by looping
Hash JoinJoin using hash table
Merge JoinJoin sorted inputs
SortSort operation
AggregateAggregation
HashAggregateAggregation using hash table

Important:

EXPLAIN ANALYZE actually runs the query.

So be careful with:

EXPLAIN ANALYZE
DELETE FROM large_table;

24. Logs Mapping

MySQL Logs

MySQL LogMeaning
Error logServer errors and startup messages
General query logAll client queries
Slow query logSlow SQL statements
Binary logReplication and point-in-time recovery
Relay logReplica receives source binary log events
Redo logInnoDB crash recovery
Undo logMVCC rollback data

PostgreSQL Logs

PostgreSQL Log/ResourceMeaning
Server logErrors, messages, slow queries
WALWrite-Ahead Log for recovery and replication
Archive WALSaved WAL files for PITR
Autovacuum logsVacuum activity if enabled
Checkpoint logsCheckpoint activity if enabled

Slow Query Logging

MySQL:

slow_query_log = 1
long_query_time = 2

PostgreSQL:

log_min_duration_statement = 2000

This logs queries taking longer than 2000 milliseconds.

Statement Logging

PostgreSQL:

log_statement = 'none'
log_statement = 'ddl'
log_statement = 'mod'
log_statement = 'all'

Common safer setting:

log_statement = 'ddl'

25. WAL vs Binary Log

This is a major concept.

MySQL Binary Log

MySQL binary log records changes for:

Replication
Point-in-time recovery
Auditing changes

Common formats:

STATEMENT
ROW
MIXED

PostgreSQL WAL

PostgreSQL uses:

WAL = Write-Ahead Log

WAL is used for:

Crash recovery
Streaming replication
Point-in-time recovery
Logical decoding

Mapping

MySQLPostgreSQL
Binary logWAL for replication/recovery concepts
Redo logWAL
Relay logReceived WAL on standby
Binlog positionLSN
GTIDReplication slots / LSN concepts
Row-based replicationLogical replication / WAL decoding

PostgreSQL uses LSN:

Log Sequence Number

Example:

SELECT pg_current_wal_lsn();

26. Backup and Restore Mapping

MySQL Logical Backup

mysqldump -u root -p shop > shop.sql

Restore:

mysql -u root -p shop < shop.sql

PostgreSQL Logical Backup

pg_dump -U postgres shop > shop.sql

Restore:

psql -U postgres -d shop < shop.sql

PostgreSQL Custom Format Backup

pg_dump -U postgres -Fc shop > shop.dump

Restore:

pg_restore -U postgres -d shop shop.dump

Backup All Databases

MySQL:

mysqldump -u root -p --all-databases > all.sql

PostgreSQL:

pg_dumpall -U postgres > all.sql

Physical Backup

MySQL:

Filesystem snapshot
MySQL Enterprise Backup
Percona XtraBackup

PostgreSQL:

pg_basebackup
Filesystem snapshot
Storage snapshot

Example:

pg_basebackup -U replication_user -D /backup/pgbase -Fp -Xs -P

Point-In-Time Recovery

MySQL PITR uses:

Full backup + binary logs

PostgreSQL PITR uses:

Base backup + archived WAL

27. Replication Mapping

MySQL Replication

Common terms:

Source
Replica
Binary log
Relay log
GTID
Semi-sync replication

Older terms:

Master
Slave

PostgreSQL Replication

Common terms:

Primary
Standby
WAL streaming
Replication slot
Synchronous replication
Logical replication

Mapping Table

MySQLPostgreSQL
Source/masterPrimary
Replica/slaveStandby
Binary logWAL
Relay logWAL received by standby
Binlog positionLSN
GTIDReplication slots / LSN tracking
Row-based replicationLogical replication
Statement-based replicationNo exact direct default equivalent
Semi-sync replicationSynchronous replication
Read replicaHot standby

PostgreSQL Physical Streaming Replication

This replicates the whole PostgreSQL cluster.

Good for:

Read replicas
Failover
Disaster recovery

PostgreSQL Logical Replication

This replicates selected tables.

Good for:

Selective replication
Data migration
Partial sync
Version upgrades

Example publication:

CREATE PUBLICATION shop_pub
FOR TABLE customers, orders;

Example subscription:

CREATE SUBSCRIPTION shop_sub
CONNECTION 'host=primary dbname=shop user=repl password=secret'
PUBLICATION shop_pub;

28. High Availability Mapping

MySQL HA Tools

Common MySQL HA options:

Group Replication
InnoDB Cluster
MHA
Orchestrator
ProxySQL
Semi-sync replication

PostgreSQL HA Tools

Common PostgreSQL HA tools:

Patroni
repmgr
pg_auto_failover
Pacemaker/Corosync
HAProxy
PgBouncer

Conceptual Mapping

MySQL HA ConceptPostgreSQL Equivalent
Primary with replicasPrimary with standbys
Automatic failoverPatroni/repmgr/pg_auto_failover
ProxySQL routingHAProxy/PgBouncer
Group ReplicationNo exact default equivalent
InnoDB ClusterPostgreSQL HA stack using external tools

PostgreSQL core provides strong replication primitives, but automatic HA is commonly built with external tooling.


29. Partitioning Mapping

MySQL Partitioning

Example:

CREATE TABLE orders (
    id BIGINT,
    order_date DATE
)
PARTITION BY RANGE (YEAR(order_date)) (
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026)
);

PostgreSQL Partitioning

PostgreSQL uses declarative partitioning.

Example:

CREATE TABLE orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY,
    order_date DATE NOT NULL,
    amount NUMERIC(10,2)
) PARTITION BY RANGE (order_date);

Create partitions:

CREATE TABLE orders_2025
PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE orders_2026
PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

Partitioning Types

MySQLPostgreSQL
RANGERANGE
LISTLIST
HASHHASH
KEYNo exact same style

PostgreSQL partitioning is commonly used for:

Large time-series tables
Log tables
Multi-tenant data
Data retention

30. Temporary Tables

MySQL

CREATE TEMPORARY TABLE temp_users (
    id INT,
    name VARCHAR(100)
);

PostgreSQL

CREATE TEMP TABLE temp_users (
    id INT,
    name TEXT
);

PostgreSQL temporary tables are session-local.

You can also use:

CREATE TEMPORARY TABLE temp_users (
    id INT,
    name TEXT
);

Same meaning.


31. Views and Materialized Views

Views

MySQL:

CREATE VIEW active_users AS
SELECT *
FROM users
WHERE is_active = 1;

PostgreSQL:

CREATE VIEW active_users AS
SELECT *
FROM users
WHERE is_active = TRUE;

Materialized Views

PostgreSQL supports materialized views natively:

CREATE MATERIALIZED VIEW monthly_sales AS
SELECT
    date_trunc('month', order_date) AS month,
    SUM(amount) AS total_sales
FROM orders
GROUP BY date_trunc('month', order_date);

Refresh:

REFRESH MATERIALIZED VIEW monthly_sales;

MySQL does not have true built-in materialized views in the same way. You usually simulate them with tables, scheduled jobs, or triggers.


32. Functions, Procedures, and Triggers

MySQL

MySQL supports:

Stored procedures
Functions
Triggers
Events

Example procedure:

DELIMITER //

CREATE PROCEDURE GetUsers()
BEGIN
    SELECT * FROM users;
END //

DELIMITER ;

PostgreSQL

PostgreSQL supports:

Functions
Procedures
Triggers
Rules
Extensions

PostgreSQL function example:

CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER AS $$
DECLARE
    total INTEGER;
BEGIN
    SELECT COUNT(*) INTO total FROM users;
    RETURN total;
END;
$$ LANGUAGE plpgsql;

Call:

SELECT get_user_count();

Procedure example:

CREATE PROCEDURE deactivate_user(p_user_id BIGINT)
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE users
    SET is_active = FALSE
    WHERE id = p_user_id;
END;
$$;

Call:

CALL deactivate_user(10);

Trigger Example in PostgreSQL

CREATE OR REPLACE FUNCTION update_modified_time()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_modified_time();

33. Events vs Scheduled Jobs

MySQL Event Scheduler

MySQL has an event scheduler:

CREATE EVENT cleanup_old_logs
ON SCHEDULE EVERY 1 DAY
DO
DELETE FROM logs
WHERE created_at < NOW() - INTERVAL 30 DAY;

PostgreSQL

PostgreSQL core does not have exactly the same built-in event scheduler.

Common options:

cron
pg_cron extension
application scheduler
external job scheduler

With pg_cron, PostgreSQL can schedule jobs from inside the database.

Conceptual mapping:

MySQLPostgreSQL
Event Schedulerpg_cron, cron, external scheduler

34. Extensions vs Plugins

MySQL Plugins

MySQL supports plugins for:

Authentication
Storage engines
Full-text parsers
Audit
Replication

PostgreSQL Extensions

PostgreSQL has a powerful extension system.

List available extensions:

SELECT *
FROM pg_available_extensions;

Install extension:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

Popular PostgreSQL extensions:

ExtensionPurpose
pg_stat_statementsQuery performance tracking
uuid-osspUUID generation
pgcryptoCryptographic functions
citextCase-insensitive text
postgisSpatial/geographic data
pg_trgmTrigram search
btree_ginExtra GIN support
pg_cronScheduled jobs

Example:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Then create trigram index:

CREATE INDEX idx_users_name_trgm
ON users USING GIN (name gin_trgm_ops);

Useful for fuzzy search.


35. Monitoring Mapping

MySQL Monitoring

Common commands:

SHOW STATUS;
SHOW GLOBAL STATUS;
SHOW VARIABLES;
SHOW PROCESSLIST;

Common schemas:

performance_schema
sys
information_schema

PostgreSQL Monitoring

PostgreSQL uses catalog and statistics views.

Common views:

PostgreSQL ViewMeaning
pg_stat_activityActive sessions and queries
pg_stat_databaseDatabase-level stats
pg_stat_user_tablesTable-level stats
pg_stat_user_indexesIndex-level stats
pg_stat_bgwriterBackground writer stats
pg_stat_walWAL stats
pg_locksLock information
pg_stat_replicationReplication status
pg_stat_statementsQuery stats extension

Active Queries

MySQL:

SHOW PROCESSLIST;

PostgreSQL:

SELECT
    pid,
    usename,
    datname,
    state,
    query,
    query_start
FROM pg_stat_activity
ORDER BY query_start;

Locks

PostgreSQL:

SELECT *
FROM pg_locks;

Useful lock query:

SELECT
    a.pid,
    a.usename,
    a.query,
    l.locktype,
    l.mode,
    l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid;

36. Query Performance Mapping

MySQL

You may use:

EXPLAIN
slow query log
performance_schema
sys schema
SHOW STATUS

PostgreSQL

You commonly use:

EXPLAIN ANALYZE
pg_stat_statements
pg_stat_activity
pg_stat_user_tables
pg_stat_user_indexes
auto_explain

pg_stat_statements

Enable extension:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

Query slow/expensive SQL:

SELECT
    query,
    calls,
    total_exec_time,
    mean_exec_time,
    rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

This is somewhat like using MySQL Performance Schema or slow query analysis.


37. Locking Mapping

MySQL/InnoDB

Common lock concepts:

Row locks
Gap locks
Next-key locks
Metadata locks
Table locks
Deadlocks

PostgreSQL

Common lock concepts:

Row locks
Table locks
Advisory locks
Predicate locks
Deadlocks

Row Lock

Same style:

SELECT *
FROM orders
WHERE id = 100
FOR UPDATE;

PostgreSQL Lock Modes

Common PostgreSQL table lock modes include:

ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE

The strongest common lock is:

ACCESS EXCLUSIVE

It blocks almost everything.

Advisory Locks

PostgreSQL has advisory locks:

SELECT pg_advisory_lock(12345);

Release:

SELECT pg_advisory_unlock(12345);

These are application-controlled locks.


38. Isolation Levels

MySQL

Common isolation levels:

READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

MySQL/InnoDB default is commonly:

REPEATABLE READ

PostgreSQL

PostgreSQL supports:

READ COMMITTED
REPEATABLE READ
SERIALIZABLE

PostgreSQL default:

READ COMMITTED

Set isolation level:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

or:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Important mapping:

MySQLPostgreSQL
Common default: REPEATABLE READDefault: READ COMMITTED
Gap locks under InnoDBDifferent MVCC/locking behavior
SerializableSerializable Snapshot Isolation

39. Character Set and Collation

MySQL

MySQL often uses:

CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci

Example:

CREATE DATABASE shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;

Many MySQL collations are case-insensitive by default.

That means:

WHERE name = 'alice'

may match:

Alice
ALICE
alice

depending on collation.

PostgreSQL

PostgreSQL supports collations too, but case-sensitive behavior is more common by default.

Case-insensitive search:

SELECT *
FROM users
WHERE name ILIKE 'alice';

Or use LOWER():

SELECT *
FROM users
WHERE LOWER(name) = LOWER('Alice');

Or use citext extension:

CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE users (
    email CITEXT UNIQUE
);

CITEXT means case-insensitive text.


40. Full-Text Search

MySQL

MySQL full-text index:

CREATE FULLTEXT INDEX idx_articles_body
ON articles(body);

Query:

SELECT *
FROM articles
WHERE MATCH(title, body) AGAINST ('database tutorial');

PostgreSQL

PostgreSQL full-text search uses tsvector and tsquery.

Simple query:

SELECT *
FROM articles
WHERE to_tsvector('english', title || ' ' || body)
      @@ plainto_tsquery('english', 'database tutorial');

Better with generated column:

ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
    to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
) STORED;

Index:

CREATE INDEX idx_articles_search
ON articles USING GIN (search_vector);

Query:

SELECT *
FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'database tutorial');

41. JSON Mapping

MySQL JSON

CREATE TABLE events (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    payload JSON
);

Query:

SELECT JSON_EXTRACT(payload, '$.type')
FROM events;

PostgreSQL JSONB

CREATE TABLE events (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload JSONB
);

Get field as JSON:

SELECT payload->'type'
FROM events;

Get field as text:

SELECT payload->>'type'
FROM events;

Filter:

SELECT *
FROM events
WHERE payload->>'type' = 'login';

Containment:

SELECT *
FROM events
WHERE payload @> '{"type": "login"}';

Index:

CREATE INDEX idx_events_payload
ON events USING GIN (payload);

42. Constraints Mapping

ConstraintMySQLPostgreSQL
Primary keyYesYes
Foreign keyYes with InnoDBYes
UniqueYesYes
Not nullYesYes
CheckSupported in modern MySQLStrong support
Exclusion constraintNo common equivalentYes

PostgreSQL Check Constraint

CREATE TABLE products (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    price NUMERIC(10,2) NOT NULL,
    CONSTRAINT chk_price_positive CHECK (price >= 0)
);

Foreign Key

Same idea:

CREATE TABLE orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers(id)
);

Deferrable Constraints

PostgreSQL supports deferrable constraints.

CREATE TABLE child (
    id BIGINT PRIMARY KEY,
    parent_id BIGINT,
    CONSTRAINT fk_parent
        FOREIGN KEY (parent_id)
        REFERENCES parent(id)
        DEFERRABLE INITIALLY DEFERRED
);

This lets constraint checking happen at transaction commit.

Very powerful. Not something most MySQL users commonly use.


43. Generated Columns

MySQL

CREATE TABLE users (
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    full_name VARCHAR(201)
        GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) STORED
);

PostgreSQL

CREATE TABLE users (
    first_name TEXT,
    last_name TEXT,
    full_name TEXT
        GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
);

Both support generated columns, but syntax and function support differ.


44. Error Handling and Strictness

PostgreSQL is stricter than MySQL in many situations.

Example: Invalid Integer

MySQL may allow or coerce depending on SQL mode:

INSERT INTO users(id) VALUES ('abc');

PostgreSQL will reject invalid type conversion.

Example: Group By

PostgreSQL requires correct grouping.

Example: Transaction Error

PostgreSQL aborts the whole transaction after an error until rollback.

This strictness is actually one reason many developers like PostgreSQL: fewer silent surprises.


45. DDL Differences

Create Database

MySQL:

CREATE DATABASE shop;

PostgreSQL:

CREATE DATABASE shop;

Same.

Use Database

MySQL:

USE shop;

PostgreSQL:

\c shop

In PostgreSQL, \c is a psql command, not SQL.

Create Table

MySQL:

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

PostgreSQL:

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

Drop Table

Same:

DROP TABLE users;

Truncate

Same:

TRUNCATE TABLE users;

PostgreSQL can restart identity:

TRUNCATE TABLE users RESTART IDENTITY;

46. Admin Commands Mapping

TaskMySQLPostgreSQL
Start serversystemctl start mysqlsystemctl start postgresql
Stop serversystemctl stop mysqlsystemctl stop postgresql
Restart serversystemctl restart mysqlsystemctl restart postgresql
Reload configsystemctl reload mysqlSELECT pg_reload_conf(); or service reload
Check variablesSHOW VARIABLES;SHOW ALL;
Check statusSHOW STATUS;pg_stat_* views
List sessionsSHOW PROCESSLIST;pg_stat_activity
Kill sessionKILL id;pg_terminate_backend(pid)

PostgreSQL reload config:

SELECT pg_reload_conf();

47. Information Schema and Catalogs

MySQL

SELECT *
FROM information_schema.tables
WHERE table_schema = 'shop';

PostgreSQL

SELECT *
FROM information_schema.tables
WHERE table_schema = 'public';

PostgreSQL also has richer internal catalogs:

SELECT *
FROM pg_catalog.pg_class;

List user tables:

SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname = 'public';

List columns:

SELECT
    column_name,
    data_type,
    is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
  AND table_name = 'users';

48. Security Model Mapping

MySQL Security Model

MySQL commonly combines:

User
Host
Password
Privileges

Example:

CREATE USER 'reporter'@'10.%' IDENTIFIED BY 'secret';
GRANT SELECT ON shop.* TO 'reporter'@'10.%';

PostgreSQL Security Model

PostgreSQL separates:

Role
Database permission
Schema permission
Object permission
Authentication rule

Example:

CREATE ROLE reporter WITH LOGIN PASSWORD 'secret';

GRANT CONNECT ON DATABASE shop TO reporter;
GRANT USAGE ON SCHEMA public TO reporter;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporter;

And in pg_hba.conf:

host    shop    reporter    10.0.0.0/8    scram-sha-256

PostgreSQL Row-Level Security

PostgreSQL has strong row-level security.

Enable:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

Create policy:

CREATE POLICY user_orders_policy
ON orders
FOR SELECT
USING (customer_id = current_setting('app.customer_id')::BIGINT);

This is more advanced, but powerful for multi-tenant systems.


49. Schema Design Difference

MySQL Style

You may create multiple databases:

shop
shop_audit
shop_reporting

And query between them:

SELECT *
FROM shop.orders o
JOIN shop_reporting.order_summary s ON o.id = s.order_id;

PostgreSQL Style

You may create one database with multiple schemas:

shop database
 ├── app
 ├── audit
 ├── reporting
 └── staging

Query:

SELECT *
FROM app.orders o
JOIN reporting.order_summary s ON o.id = s.order_id;

This is one of the best PostgreSQL habits to learn early.


50. Practical Side-by-Side Example

Let us build the same small application schema in both databases.

MySQL Version

CREATE DATABASE shop;
USE shop;

CREATE TABLE customers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    order_total DECIMAL(10,2) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers(id)
);

CREATE INDEX idx_orders_customer_id
ON orders(customer_id);

PostgreSQL Version

CREATE DATABASE shop;

Connect:

\c shop

Create schema:

CREATE SCHEMA app;

Create tables:

CREATE TABLE app.customers (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE app.orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    order_total NUMERIC(10,2) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id)
        REFERENCES app.customers(id)
);

CREATE INDEX idx_orders_customer_id
ON app.orders(customer_id);

Key Differences in This Example

MySQLPostgreSQL
AUTO_INCREMENTGENERATED ALWAYS AS IDENTITY
TINYINT(1)BOOLEAN
DATETIMETIMESTAMPTZ
DECIMALNUMERIC
Database used as namespaceSchema used as namespace
USE shop\c shop

51. Common Migration Mapping

When converting MySQL schema to PostgreSQL:

MySQLConvert To PostgreSQL
AUTO_INCREMENTGENERATED BY DEFAULT AS IDENTITY
TINYINT(1)BOOLEAN
DATETIMETIMESTAMP or TIMESTAMPTZ
LONGTEXTTEXT
BLOBBYTEA
JSONJSONB
ENUMPostgreSQL enum or lookup table
UNSIGNED INTLarger signed type or check constraint
ON DUPLICATE KEY UPDATEON CONFLICT DO UPDATE
INSERT IGNOREON CONFLICT DO NOTHING
BackticksDouble quotes or no quotes
IFNULL()COALESCE()
DATE_FORMAT()to_char()
STR_TO_DATE()to_date() / to_timestamp()
GROUP_CONCAT()string_agg()

Function Mapping

MySQL FunctionPostgreSQL Function
IFNULL(a,b)COALESCE(a,b)
NOW()NOW()
CURDATE()CURRENT_DATE
CONCAT()CONCAT() or `
GROUP_CONCAT()string_agg()
DATE_FORMAT()to_char()
JSON_EXTRACT()->, ->>, jsonb_path_query
RAND()random()
UUID()gen_random_uuid() with pgcrypto

Example MySQL:

SELECT GROUP_CONCAT(name)
FROM users;

PostgreSQL:

SELECT string_agg(name, ',')
FROM users;

52. Quoting Difference

MySQL

MySQL often uses backticks:

SELECT `user`, `order`
FROM `my_table`;

PostgreSQL

PostgreSQL uses double quotes for identifiers:

SELECT "user", "order"
FROM "my_table";

But best practice:

Avoid reserved words and mixed-case identifiers.
Use lowercase snake_case.

Good PostgreSQL style:

CREATE TABLE customer_orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_status TEXT NOT NULL
);

Avoid:

CREATE TABLE "CustomerOrders" (
    "ID" BIGINT
);

Because then you must quote it forever.


53. Naming Conventions

Recommended PostgreSQL style:

lowercase
snake_case
plural or singular consistently
avoid reserved words
avoid quoted identifiers

Good:

customer_orders
created_at
is_active
order_total

Avoid:

CustomerOrders
Order
User
CreatedAt

54. PostgreSQL Features MySQL Users Should Appreciate

PostgreSQL has several powerful features that MySQL users may not expect.

Strong JSONB

payload JSONB

with GIN indexes.

Partial Indexes

CREATE INDEX idx_active_users
ON users(email)
WHERE is_active = TRUE;

Expression Indexes

CREATE INDEX idx_lower_email
ON users(LOWER(email));

Rich Data Types

PostgreSQL supports:

UUID
JSONB
ARRAY
RANGE
INET
CIDR
MACADDR
TSVECTOR
GEOMETRY through PostGIS

Extensions

CREATE EXTENSION postgis;
CREATE EXTENSION pg_stat_statements;
CREATE EXTENSION pgcrypto;

Transactional DDL

Rollback many schema changes safely.

Powerful CTEs

WITH active_customers AS (
    SELECT *
    FROM customers
    WHERE is_active = TRUE
)
SELECT *
FROM active_customers;

Returning Clause

PostgreSQL can return inserted/updated/deleted rows:

INSERT INTO users (name)
VALUES ('Alice')
RETURNING id, name;

This is extremely useful.

MySQL has some newer returning support in limited areas, but PostgreSQL’s RETURNING is widely used and very natural.


55. Common PostgreSQL Surprises for MySQL Users

Surprise 1: Users Are Roles

User = role with LOGIN

Surprise 2: Schema Is Not Database

database → schema → table

Surprise 3: Permissions Need Schema Grants

This is not enough:

GRANT SELECT ON ALL TABLES IN SCHEMA public TO appuser;

You also often need:

GRANT USAGE ON SCHEMA public TO appuser;

Surprise 4: Sequences Need Permissions

For identity/serial inserts, you may need:

GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO appuser;

Surprise 5: Failed Transaction Requires Rollback

After an error:

ROLLBACK;

Surprise 6: Case Sensitivity

PostgreSQL does not behave like case-insensitive MySQL collations by default.

Use:

ILIKE
LOWER()
citext

Surprise 7: Vacuum Matters

Autovacuum is essential.

Surprise 8: No USE database

Use:

\c database_name

Surprise 9: No Backticks

Use double quotes only when necessary.

Better: avoid quoting by using lowercase names.

Surprise 10: EXPLAIN ANALYZE Runs the Query

Do not use carelessly with destructive SQL.


56. Quick Reference Cheat Sheet

Object Hierarchy

MySQL:
server → database/schema → table

PostgreSQL:
cluster/server → database → schema → table

User System

MySQL:
'user'@'host'

PostgreSQL:
role + pg_hba.conf

Auto Increment

MySQL:
AUTO_INCREMENT

PostgreSQL:
GENERATED ALWAYS AS IDENTITY
sequence

Logs

MySQL:
binary log, redo log, slow query log

PostgreSQL:
WAL, server log, slow query via log_min_duration_statement

Monitoring

MySQL:
SHOW PROCESSLIST
SHOW STATUS
performance_schema

PostgreSQL:
pg_stat_activity
pg_stat_database
pg_stat_user_tables
pg_stat_statements

Backup

MySQL:
mysqldump

PostgreSQL:
pg_dump
pg_restore
pg_basebackup

Replication

MySQL:
binlog replication

PostgreSQL:
WAL streaming replication
logical replication

57. Recommended Learning Order for a MySQL Expert

Study PostgreSQL in this order:

Step 1: Basic Navigation

Learn:

\l
\c
\dn
\dt
\d
\du

Step 2: Database vs Schema

Understand:

database → schema → table

Step 3: Roles and Permissions

Understand:

role with LOGIN = user
role without LOGIN = group-like role

Step 4: Data Type Differences

Focus on:

BOOLEAN
TEXT
TIMESTAMPTZ
NUMERIC
JSONB
UUID
ARRAY

Step 5: Auto Increment

Learn:

GENERATED ALWAYS AS IDENTITY

and sequences.

Step 6: Upsert

Learn:

ON CONFLICT DO UPDATE
ON CONFLICT DO NOTHING

Step 7: Monitoring

Learn:

pg_stat_activity
pg_stat_user_tables
pg_stat_statements

Step 8: Performance

Learn:

EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS)

Step 9: Vacuum

Understand:

MVCC
dead tuples
autovacuum
VACUUM ANALYZE

Step 10: Backup and Replication

Learn:

pg_dump
pg_restore
pg_basebackup
WAL
streaming replication
logical replication

58. Final Mental Translation

When you see this in MySQL:

Create user appuser@localhost and grant access to database shop.

Think PostgreSQL:

Create a role with LOGIN.
Allow connection through pg_hba.conf.
Grant CONNECT on database.
Grant USAGE on schema.
Grant privileges on tables.
Grant privileges on sequences if needed.

When you see this in MySQL:

Create database shop and tables inside it.

Think PostgreSQL:

Create database shop.
Create schema app or use public.
Create tables inside that schema.

When you see this in MySQL:

AUTO_INCREMENT

Think PostgreSQL:

IDENTITY column backed by sequence.

When you see this in MySQL:

Binary log replication

Think PostgreSQL:

WAL streaming or logical replication.

When you see this in MySQL:

Performance Schema

Think PostgreSQL:

pg_stat_* views and pg_stat_statements.

When you see this in MySQL:

InnoDB purge cleans old versions.

Think PostgreSQL:

Autovacuum cleans dead tuples.

Conclusion

PostgreSQL is not “MySQL with different syntax.” It has a different internal model.

But once you map the major ideas, it becomes much easier:

MySQL database/schema        → PostgreSQL database + schema
MySQL user@host              → PostgreSQL role + pg_hba.conf
AUTO_INCREMENT               → identity/sequence
Binary log                   → WAL
SHOW PROCESSLIST             → pg_stat_activity
Performance Schema           → pg_stat_* views
mysqldump                    → pg_dump
ON DUPLICATE KEY UPDATE      → ON CONFLICT DO UPDATE
InnoDB purge                 → VACUUM/autovacuum
Storage engines              → PostgreSQL native storage + table types

The most important PostgreSQL concepts for a MySQL user are:

Roles
Schemas
Sequences
WAL
MVCC
Autovacuum
pg_hba.conf
JSONB
Extensions
EXPLAIN ANALYZE
pg_stat_* views

Once these are clear, PostgreSQL starts to feel very logical — just stricter, more layered, and more explicit than MySQL.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x