At a Glance
The table below summarises the most important DDL differences. Each section below drills into one topic with side-by-side code examples.
| Feature | MySQL | PostgreSQL | Oracle | SQL Server | SQLite |
|---|---|---|---|---|---|
| Auto-increment PK | AUTO_INCREMENT |
SERIAL / IDENTITY |
GENERATED AS IDENTITY |
IDENTITY(1,1) |
INTEGER PRIMARY KEY |
| Variable string | VARCHAR(n) |
VARCHAR(n) |
VARCHAR2(n) |
NVARCHAR(n) |
TEXT |
| Large text | LONGTEXT |
TEXT |
CLOB |
NVARCHAR(MAX) |
TEXT |
| Boolean | TINYINT(1) |
BOOLEAN |
NUMBER(1) |
BIT |
INTEGER |
| Auto timestamp | DEFAULT CURRENT_TIMESTAMP |
DEFAULT NOW() |
DEFAULT SYSTIMESTAMP |
DEFAULT GETDATE() |
DEFAULT CURRENT_TIMESTAMP |
| CHECK constraints | Enforced (MySQL 8.0.16+) | Always enforced | Always enforced | Always enforced | Parsed; enforced since 3.25.0 |
| Generated columns | GENERATED ALWAYS AS (...) STORED/VIRTUAL |
GENERATED ALWAYS AS (...) STORED |
GENERATED ALWAYS AS (...) VIRTUAL |
AS (...) PERSISTED/computed |
GENERATED ALWAYS AS (...) STORED/VIRTUAL |
| Rename column (ALTER) | RENAME COLUMN (MySQL 8+) |
RENAME COLUMN |
RENAME COLUMN |
sp_rename (stored procedure) |
Not supported directly |
| IF NOT EXISTS | Supported | Supported | Not supported (use exception handling) | IF NOT EXISTS (SQL Server 2022+) or OBJECT_ID check |
Supported |
Primary Keys and Auto-Increment
Auto-incrementing integer primary keys are the most visible DDL difference between databases. Every database has its own keyword or mechanism, and the underlying behaviour varies in subtle ways.
MySQL
CREATE TABLE users (
id INT UNSIGNED NOT NULL
AUTO_INCREMENT,
PRIMARY KEY (id)
);
PostgreSQL
-- modern standard (PG 10+)
CREATE TABLE users (
id INT GENERATED ALWAYS
AS IDENTITY PRIMARY KEY
);
-- legacy shorthand
-- id SERIAL PRIMARY KEY
Oracle
-- Oracle 12c+
CREATE TABLE users (
id NUMBER GENERATED ALWAYS
AS IDENTITY PRIMARY KEY
);
-- Oracle 11g and older:
-- use a SEQUENCE + trigger
SQL Server
CREATE TABLE users (
id INT NOT NULL
IDENTITY(1, 1),
CONSTRAINT PK_users
PRIMARY KEY (id)
);
SQLite
CREATE TABLE users (
id INTEGER PRIMARY KEY
-- AUTOINCREMENT keyword
-- is optional and changes
-- the reuse behaviour
);
Key behavioural notes:
- MySQL —
AUTO_INCREMENTreuses gaps in SQLite style unless you useNO_AUTO_VALUE_ON_ZERO. The counter resets on server restart if the table is empty (before MySQL 8.0). - PostgreSQL —
SERIALcreates a backing sequence object.GENERATED ALWAYS AS IDENTITYis the SQL:2003 standard equivalent and is preferred for new schemas. - Oracle — Before 12c, sequences were always separate objects and had to be fed into the column via a trigger or called explicitly in the
INSERT. - SQL Server —
IDENTITY(seed, increment). Once a row with an identity value is inserted, you cannot insert an explicit value withoutSET IDENTITY_INSERT table ON. - SQLite —
INTEGER PRIMARY KEY(without theAUTOINCREMENTkeyword) is an alias for the internalrowidand reuses deleted values. AddingAUTOINCREMENTprevents reuse but has a small performance cost.
String Types
String storage is one of the most divergent areas across databases, particularly around Unicode support and maximum lengths.
| Use case | MySQL | PostgreSQL | Oracle | SQL Server | SQLite |
|---|---|---|---|---|---|
| Short variable string | VARCHAR(n) |
VARCHAR(n) |
VARCHAR2(n) |
NVARCHAR(n) |
TEXT |
| Fixed-length string | CHAR(n) |
CHAR(n) |
CHAR(n) |
NCHAR(n) |
TEXT |
| Large text / CLOB | LONGTEXT |
TEXT (unlimited) |
CLOB |
NVARCHAR(MAX) |
TEXT |
Max VARCHAR length |
65,535 bytes | 1 GB | 32,767 bytes | 4,000 chars (NVARCHAR) |
Unlimited (stored as TEXT) |
Important differences to be aware of:
- Oracle uses
VARCHAR2, notVARCHAR— Oracle'sVARCHARis reserved and may behave differently in future versions. Always useVARCHAR2. - SQL Server stores Unicode by default with
Nprefix types —NVARCHARandNCHARstore Unicode (UTF-16). PlainVARCHARis ASCII only. For any modern application useNVARCHAR. - PostgreSQL's
TEXTis unlimited —TEXTandVARCHAR(n)have the same performance; the length limit is just a constraint, not a storage optimisation. - SQLite ignores type affinity at storage level — SQLite stores any string as
TEXTregardless of whether you declare the column asVARCHAR(255)orCHAR(10). Type names are only advisory.
If you're writing portable DDL, VARCHAR(n) works on MySQL, PostgreSQL, Oracle (as VARCHAR2), and SQL Server. Always use NVARCHAR on SQL Server for Unicode safety.
Numeric Types
Integer and decimal types are broadly consistent in naming, but there are gaps:
| MySQL | PostgreSQL | Oracle | SQL Server | SQLite |
|---|---|---|---|---|
TINYINT (1 byte) |
SMALLINT (2 bytes) |
NUMBER(3) |
TINYINT (1 byte) |
INTEGER |
SMALLINT (2 bytes) |
SMALLINT (2 bytes) |
NUMBER(5) |
SMALLINT (2 bytes) |
INTEGER |
INT (4 bytes) |
INTEGER (4 bytes) |
NUMBER(10) |
INT (4 bytes) |
INTEGER |
BIGINT (8 bytes) |
BIGINT (8 bytes) |
NUMBER(19) |
BIGINT (8 bytes) |
INTEGER |
DECIMAL(p,s) |
NUMERIC(p,s) |
NUMBER(p,s) |
DECIMAL(p,s) |
NUMERIC(p,s) |
Notable differences:
- Oracle has no separate integer types — everything is
NUMBER(p,s).INTEGERis an alias forNUMBER(38)and lacks the compact storage of a true 4-byte integer. - SQLite uses dynamic typing — any integer you declare gets stored in 1–8 bytes depending on value, regardless of the declared type name.
- MySQL supports
UNSIGNED—INT UNSIGNEDdoubles the positive range (0 to ~4.3B). No other major database supports this modifier. - Use
DECIMALorNUMERICfor monetary values in all databases. Never useFLOATorDOUBLEfor money.
Boolean Type
Boolean is one of the most inconsistent types across SQL databases:
| Database | Boolean DDL | True / False values |
|---|---|---|
| MySQL | TINYINT(1) |
1 / 0 |
| PostgreSQL | BOOLEAN |
TRUE/FALSE, 't'/'f', 1/0 |
| Oracle | NUMBER(1) or CHAR(1) |
1/0 or 'Y'/'N' by convention |
| SQL Server | BIT |
1 / 0 |
| SQLite | INTEGER |
1 / 0 |
PostgreSQL is the only database here with a true native boolean type. Oracle has no boolean type at all in SQL (PL/SQL has one, but it cannot be used as a column type). SQL Server's BIT accepts 1/0 and 'true'/'false' strings but is not a proper boolean.
Oracle 23c introduced a native BOOLEAN column type — the first Oracle version to support it. If you're on an older Oracle version, use NUMBER(1) CHECK (col IN (0, 1)).
Date and Time Types
| Use case | MySQL | PostgreSQL | Oracle | SQL Server | SQLite |
|---|---|---|---|---|---|
| Date only | DATE |
DATE |
DATE (includes time!) |
DATE |
TEXT / NUMERIC |
| Date + time | DATETIME |
TIMESTAMP |
TIMESTAMP |
DATETIME2 |
TEXT (ISO 8601) |
| Date + time + timezone | Not natively supported | TIMESTAMPTZ |
TIMESTAMP WITH TIME ZONE |
DATETIMEOFFSET |
Not supported |
| Auto-set on insert | DEFAULT CURRENT_TIMESTAMP |
DEFAULT NOW() |
DEFAULT SYSTIMESTAMP |
DEFAULT GETDATE() |
DEFAULT CURRENT_TIMESTAMP |
Critical Oracle gotcha: Oracle's DATE type stores both date and time (to the nearest second). This is unlike every other database where DATE is date-only. If you query WHERE event_date = DATE '2026-05-01' in Oracle and the column has a time component, you'll get no results. Use TRUNC(event_date) to strip the time, or use TIMESTAMP columns from the start.
SQL Server has deprecated the older DATETIME type in favour of DATETIME2, which has higher precision (100ns vs 3ms) and a wider range. Use DATETIME2 in new SQL Server schemas.
CHECK Constraints
CHECK constraints let you enforce rules at the database level — for example, that a status column can only hold certain values, or that a price must be positive.
-- Works the same way in MySQL 8.0.16+, PostgreSQL, Oracle, and SQL Server
CREATE TABLE orders (
id INT PRIMARY KEY,
status VARCHAR(20) NOT NULL,
total_cents INT NOT NULL,
CONSTRAINT chk_status CHECK (status IN ('pending', 'paid', 'cancelled')),
CONSTRAINT chk_total CHECK (total_cents >= 0)
);
The SQL above is portable across all five databases with minor syntax adjustments. The key differences are enforcement history:
- MySQL —
CHECKconstraints were parsed but silently ignored before MySQL 8.0.16. Any existing schema built on MySQL 5.7 or earlier that usesCHECKfor data integrity may have dirty data in it. - PostgreSQL, Oracle, SQL Server — always enforced
CHECKconstraints fully. - SQLite — enforced
CHECKconstraints from version 3.25.0 (2018). Very old SQLite builds may ignore them.
For maximum portability and correctness, always use CHECK constraints with a named CONSTRAINT chk_name so they can be dropped by name later.
DEFAULT Values
Specifying default values for columns is mostly consistent, but the function names for current timestamp differ:
-- MySQL
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-- PostgreSQL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- triggers or rules needed for auto-update
-- Oracle
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
-- SQL Server
created_at DATETIME2 NOT NULL DEFAULT GETDATE(),
updated_at DATETIME2 NOT NULL DEFAULT GETDATE()
-- SQLite
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
MySQL's ON UPDATE CURRENT_TIMESTAMP is a MySQL-only extension that automatically refreshes the column on any UPDATE. PostgreSQL, Oracle, and SQL Server require a trigger to replicate this behaviour. SQLite has no trigger-free equivalent.
Generated (Computed) Columns
Generated columns derive their value from an expression and are recalculated automatically. They are useful for storing pre-computed values like full names or totals without duplicating logic in application code.
MySQL
ALTER TABLE products ADD COLUMN
total_price DECIMAL(10,2)
GENERATED ALWAYS AS
(quantity * unit_price)
STORED;
PostgreSQL
ALTER TABLE products ADD COLUMN
total_price DECIMAL(10,2)
GENERATED ALWAYS AS
(quantity * unit_price)
STORED;
-- VIRTUAL not yet supported
Oracle
ALTER TABLE products ADD (
total_price NUMBER
GENERATED ALWAYS AS
(quantity * unit_price)
VIRTUAL
);
SQL Server
ALTER TABLE products ADD
total_price AS
(quantity * unit_price)
PERSISTED;
SQLite
-- Must be in CREATE TABLE
total_price REAL
GENERATED ALWAYS AS
(quantity * unit_price)
STORED;
STORED vs VIRTUAL — a stored (persisted) generated column writes the computed value to disk, making reads fast but writes slightly slower. A virtual column recomputes on every read. PostgreSQL only supports STORED. Oracle defaults to VIRTUAL. MySQL supports both.
ALTER TABLE Differences
Modifying an existing table is where dialects diverge most sharply. Here are the most common operations:
Add a column
-- MySQL, PostgreSQL, SQLite
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Oracle
ALTER TABLE users ADD (phone VARCHAR2(20));
-- SQL Server
ALTER TABLE users ADD phone NVARCHAR(20);
Rename a column
-- MySQL 8+, PostgreSQL
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- Oracle 12c R2+
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- SQL Server (uses a system stored procedure, not standard SQL)
EXEC sp_rename 'users.phone', 'phone_number', 'COLUMN';
-- SQLite — not supported directly.
-- You must recreate the table.
Change a column's data type
-- MySQL
ALTER TABLE users MODIFY COLUMN age SMALLINT;
-- PostgreSQL
ALTER TABLE users ALTER COLUMN age TYPE SMALLINT;
-- Oracle
ALTER TABLE users MODIFY (age NUMBER(5));
-- SQL Server
ALTER TABLE users ALTER COLUMN age SMALLINT;
-- SQLite — not supported. Recreate the table.
Drop a column
-- MySQL, PostgreSQL, Oracle, SQL Server
ALTER TABLE users DROP COLUMN phone;
-- SQLite — not supported before version 3.35.0 (2021).
-- Upgrade or recreate the table.
SQLite has historically had very limited ALTER TABLE support. Adding columns and renaming tables always worked, but renaming columns requires SQLite 3.25.0+ and dropping columns requires SQLite 3.35.0+. Check your SQLite version before relying on these operations.
Summary: Which Differences Matter Most in Practice
If you're designing a schema from scratch, here's where to focus your attention:
- Primary keys — the biggest immediate syntax difference. Know your database's keyword before writing your first
CREATE TABLE. - String types — use
VARCHAR2on Oracle,NVARCHARon SQL Server, andVARCHAReverywhere else. SQLite accepts anything and stores it asTEXT. - Booleans — only PostgreSQL has a true native
BOOLEAN. UseTINYINT(1),BIT, orNUMBER(1)with aCHECKconstraint elsewhere. - Oracle's
DATEincludes time — this surprises almost everyone coming from MySQL or PostgreSQL. UseTIMESTAMPin Oracle if you only want a date. - CHECK enforcement history — if you're working with a MySQL 5.7 or earlier schema,
CHECKconstraints were ignored. Verify data integrity before migrating. - SQLite ALTER TABLE limitations — SQLite is not suitable for schemas that need frequent structural changes in production. Column drops and renames require a recent SQLite version.
- Timestamp defaults —
CURRENT_TIMESTAMPworks in MySQL and SQLite; PostgreSQL prefersNOW(); Oracle usesSYSTIMESTAMP; SQL Server usesGETDATE()orSYSDATETIME().
Whichever database you're targeting, modelling your schema visually before writing DDL makes it easier to catch type mismatches and missing constraints early. SQL Designer supports both MySQL and PostgreSQL dialects and exports ready-to-run CREATE TABLE scripts — try the demo with your own schema.