Key Takeaways
- Each database uses a different keyword for auto-increment PKs:
AUTO_INCREMENT,SERIAL/IDENTITY,GENERATED AS IDENTITY,IDENTITY(1,1), or implicitINTEGER PRIMARY KEY. - MySQL silently ignored
CHECKconstraints before version 8.0.16 (April 2019), so older schemas may contain invalid data. - Oracle's
DATEtype stores both date and time, unlike every other database whereDATEis date-only — a common migration gotcha. - SQLite didn't support
DROP COLUMNuntil version 3.35.0 (2021) and isn't suited for schemas that need frequent structural changes in production.
At a Glance
Five major SQL databases share the same core DDL concepts but use incompatible syntax. MySQL relies on AUTO_INCREMENT while SQL Server uses IDENTITY(1,1). Oracle stores all integers as NUMBER(p,s) while PostgreSQL has dedicated BIGINT and SMALLINT types. SQLite ignores declared type names entirely at the storage level. The table below summarizes the most important 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) |
Supported since SQLite 3.25.0 |
| IF NOT EXISTS | Supported | Supported | Not supported (use exception handling) | IF NOT EXISTS (SQL Server 2022+) or OBJECT_ID check |
Supported |
How Does Each Database Handle Auto-Increment Primary Keys?
Auto-incrementing integer primary keys are the most visible DDL difference between databases. Every database uses its own keyword or mechanism, and the underlying behaviour varies in subtle ways. MySQL's AUTO_INCREMENT is a column-level attribute. PostgreSQL offers SERIAL (a shorthand that creates a backing sequence) and GENERATED ALWAYS AS IDENTITY (the SQL:2003 standard form, preferred for new schemas from PostgreSQL 10 onwards). SQL Server uses IDENTITY(seed, increment). Oracle added native identity columns in 12c; older versions require a separate sequence object plus a trigger. SQLite takes the simplest approach: declare INTEGER PRIMARY KEY and the column automatically aliases the internal rowid.
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 (MySQL docs):
AUTO_INCREMENTreuses gaps by default. Before MySQL 8.0, the counter reset on server restart when the table was empty. - PostgreSQL (PostgreSQL docs):
SERIALcreates a backing sequence object.GENERATED ALWAYS AS IDENTITYis the SQL:2003 standard equivalent and is preferred for new schemas. - Oracle (Oracle docs): before 12c, sequences were separate objects fed into the column via a trigger or called explicitly in the
INSERTstatement. - SQL Server (SQL Server docs): once a row with an identity value is inserted, you cannot insert an explicit value without
SET IDENTITY_INSERT table ON. - SQLite (SQLite docs):
INTEGER PRIMARY KEYwithout theAUTOINCREMENTkeyword reuses deleted values. AddingAUTOINCREMENTprevents reuse at a small performance cost.
String Types
String storage is one of the most divergent areas across databases, particularly around Unicode support and maximum lengths. Oracle's requirement for VARCHAR2 instead of VARCHAR trips up nearly every developer migrating from MySQL or PostgreSQL. SQL Server's distinction between VARCHAR (single-byte ASCII) and NVARCHAR (Unicode UTF-16) is equally easy to get wrong in production.
| 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's documentation (Oracle SQL reference) reserves theVARCHARkeyword for potential future redefinition. Always useVARCHAR2. - SQL Server uses
N-prefix types for Unicode.NVARCHARandNCHARstore Unicode (UTF-16). PlainVARCHARis ASCII only. For any modern application, useNVARCHAR. - PostgreSQL's
TEXTis unlimited.TEXTandVARCHAR(n)have identical performance; the length limit is just a constraint, not a storage optimisation (PostgreSQL character types). - SQLite ignores type affinity at the storage level. SQLite stores any string as
TEXTregardless of whether you declare the column asVARCHAR(255)orCHAR(10). Type names are advisory only.
For portable DDL, VARCHAR(n) works on MySQL, PostgreSQL, Oracle (write it 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 gaps exist. Oracle stands out because it has no dedicated integer storage types — everything goes through the flexible NUMBER(p,s) format, which means integers may take more storage than the equivalent MySQL INT or PostgreSQL INTEGER. SQLite takes the opposite approach: the database decides actual storage size automatically based on the value stored, regardless of the declared type name.
| 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 to 8 bytes depending on value, regardless of the declared type name.
- MySQL supports
UNSIGNED.INT UNSIGNEDdoubles the positive range to 0 through roughly 4.3 billion. No other major database supports this modifier. - Use
DECIMALorNUMERICfor monetary values in all databases. Never useFLOATorDOUBLEfor money.
Which Databases Have a Native Boolean Type?
Boolean is one of the most inconsistent types across SQL databases. PostgreSQL is the only engine here with a true native BOOLEAN column that accepts TRUE, FALSE, and a range of equivalent string literals ('t', 'yes', 'on', and 1). SQL Server's BIT is close but is better described as a 1-bit integer. Oracle had no SQL-level boolean column at all until version 23c.
| 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 |
SQL Server's BIT accepts 1/0 and 'true'/'false' strings but is not a proper boolean type. Oracle's PL/SQL has a boolean type, but it cannot be used as a column type in SQL DDL prior to version 23c.
Oracle 23c introduced a native BOOLEAN column type (Oracle 23c data types), the first Oracle version to support it in SQL DDL. On older Oracle versions, use NUMBER(1) CHECK (col IN (0, 1)).
Date and Time Types
Date and time handling is where Oracle surprises developers most. Oracle's DATE type stores both date and time to the nearest second, unlike every other database where DATE is date-only. SQL Server's DATETIME2 (preferred over the deprecated DATETIME) offers 100-nanosecond precision and a wider year range. MySQL has no built-in timezone-aware timestamp type.
| 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 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 deprecated the older DATETIME type in favour of DATETIME2, which has higher precision (100ns vs 3ms) and a wider date range. Use DATETIME2 in all new SQL Server schemas.
Which Databases Enforce CHECK Constraints?
CHECK constraints enforce rules at the database level — for example, limiting a status column to a fixed set of values. PostgreSQL, Oracle, and SQL Server have always enforced them fully. MySQL silently ignored CHECK constraints before version 8.0.16, released in April 2019 (MySQL CHECK constraint docs). Any schema built on MySQL 5.7 or earlier that relied on CHECK for data integrity may contain dirty data.
-- 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 in 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. - PostgreSQL, Oracle, SQL Server: always enforced
CHECKconstraints fully. - SQLite: enforced
CHECKconstraints from version 3.25.0, released 2018 (SQLite 3.25.0 release notes). Very old SQLite builds may ignore them.
For maximum portability, always name your CHECK constraints: CONSTRAINT chk_name CHECK (...). Named constraints can be dropped by name later if the schema evolves.
DEFAULT Values
Specifying default values is mostly consistent across databases, but the function names for the current timestamp differ in every dialect. MySQL also has a unique extension that automatically refreshes a timestamp column on any UPDATE — no other database supports this without a trigger.
-- 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 all require a trigger to replicate this behaviour. SQLite has no trigger-free equivalent.
Generated (Computed) Columns
Generated columns derive their value from an expression evaluated automatically by the database. They're useful for storing pre-computed values like full names, order totals, or slugs without duplicating logic in application code. The syntax is similar across databases, but the STORED vs VIRTUAL distinction behaves differently depending on the engine.
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. SQLite has historically been the most restrictive — some operations still require recreating the table on older SQLite builds. Here are the most common operations across all five databases:
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 3.25.0+
ALTER TABLE users RENAME COLUMN phone TO phone_number;
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 3.35.0+ (March 2021)
ALTER TABLE users DROP COLUMN phone;
-- Older SQLite — not supported. Recreate the table.
SQLite's ALTER TABLE support has expanded significantly but remains limited compared to other databases. Renaming columns requires SQLite 3.25.0+ and dropping columns requires SQLite 3.35.0+. Always check your SQLite version before relying on these in production (SQLite ALTER TABLE docs).
Summary: Which Differences Matter Most in Practice
If you're designing a schema from scratch, focus your attention here:
- 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 when 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 suited 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 MySQL, PostgreSQL, SQLite, Oracle, SQL Server, and Microsoft Access dialects and exports ready-to-run CREATE TABLE scripts — see the free online database designer or try the demo with your own schema.
Frequently Asked Questions
How do you create an auto-increment primary key in each database?
MySQL uses AUTO_INCREMENT, PostgreSQL uses SERIAL or GENERATED ALWAYS AS IDENTITY, Oracle uses GENERATED ALWAYS AS IDENTITY (12c+) or a separate sequence on older versions, SQL Server uses IDENTITY(1,1), and SQLite uses INTEGER PRIMARY KEY, which auto-increments implicitly by aliasing the internal rowid.
Which databases enforce CHECK constraints?
PostgreSQL, Oracle, and SQL Server have always enforced CHECK constraints fully. MySQL ignored them before version 8.0.16 (released April 2019), so schemas built on MySQL 5.7 or earlier may contain data that violates defined CHECK rules. SQLite has enforced CHECK constraints since version 3.25.0 (2018).
What is the equivalent of VARCHAR across different databases?
MySQL and PostgreSQL both use VARCHAR(n). Oracle requires VARCHAR2(n) — using plain VARCHAR in Oracle is not recommended. SQL Server uses VARCHAR(n) for ASCII and NVARCHAR(n) for Unicode. SQLite stores strings as TEXT regardless of the declared column type.
Does Oracle have a native BOOLEAN column type?
Oracle 23c introduced a native BOOLEAN column type, the first Oracle version to support it in SQL DDL. On earlier Oracle versions, the standard workaround is NUMBER(1) CHECK (col IN (0, 1)), which enforces boolean semantics at the database level without a true boolean type.
Can you rename a column directly in SQLite?
Yes, but only on SQLite 3.25.0 or later (2018). Dropping columns requires SQLite 3.35.0 or later (2021). On older SQLite builds, both operations require recreating the table: create a new table with the new structure, copy data across, drop the original, then rename the new table.