CREATE TABLE in MySQL
The CREATE TABLE statement is how you define the structure of your data — columns, types, constraints, indexes, and storage options. A well-designed table definition enforces data integrity at the database level, independent of your application code.
This tutorial covers everything from basic column definitions to composite keys, foreign keys, table options, and advanced creation patterns.
Basic CREATE TABLE Syntax
CREATE TABLE [IF NOT EXISTS] table_name ( column_name data_type [column_constraints], column_name data_type [column_constraints], ... [table_constraints] ) [table_options];
A Complete Real-World Example
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(254) NOT NULL,
username VARCHAR(50) NOT NULL,
password_hash CHAR(60) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
bio TEXT,
avatar_url VARCHAR(2048),
is_active TINYINT(1) NOT NULL DEFAULT 1,
role ENUM('user','moderator','admin') NOT NULL DEFAULT 'user',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
last_login_at DATETIME,
PRIMARY KEY (id),
UNIQUE KEY uq_email (email),
UNIQUE KEY uq_username (username),
INDEX idx_role (role),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci
COMMENT='Application user accounts';Column Definitions
Each column definition follows this pattern:
column_name data_type [NOT NULL | NULL] [DEFAULT value]
[AUTO_INCREMENT] [UNIQUE] [PRIMARY KEY]
[COMMENT 'text'] [COLLATE collation]NULL and NOT NULL
CREATE TABLE column_examples ( -- NOT NULL: must have a value on every insert required_name VARCHAR(100) NOT NULL, -- NULL: optional, defaults to NULL if not provided optional_bio TEXT NULL, -- NULL is the default, so NULL keyword is optional -- NOT NULL with a default: required in the sense that it always has a value, -- but you don't need to specify it in every INSERT is_active TINYINT(1) NOT NULL DEFAULT 1 );
DEFAULT Values
CREATE TABLE defaults_demo ( -- Literal defaults status VARCHAR(20) NOT NULL DEFAULT 'active', priority TINYINT NOT NULL DEFAULT 3, is_published TINYINT(1) NOT NULL DEFAULT 0, -- Dynamic defaults (expressions in parentheses -- MySQL 8.0.13+) created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, uuid_col VARCHAR(36) DEFAULT (UUID()), hash_id CHAR(8) DEFAULT (SUBSTRING(MD5(RAND()), 1, 8)), -- NULL as default (explicit) deleted_at DATETIME DEFAULT NULL );
AUTO_INCREMENT
CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, -- ... other columns ... PRIMARY KEY (id) ); -- You can also set the starting value CREATE TABLE invoices ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, -- ... other columns ... PRIMARY KEY (id) ) AUTO_INCREMENT = 1000; -- first invoice will be #1000 -- Change the starting value after creation ALTER TABLE invoices AUTO_INCREMENT = 5000;
Primary Key
The primary key uniquely identifies each row in a table. Every InnoDB table should have one — without a primary key, InnoDB generates a hidden 6-byte row ID that you can't reference.
Single-column primary key:
-- Inline syntax (on the column itself) CREATE TABLE products ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(200) NOT NULL ); -- Table-level syntax (equivalent, more explicit) CREATE TABLE products ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(200) NOT NULL, PRIMARY KEY (id) );
Composite Primary Key
-- Composite primary key (must use table-level syntax) CREATE TABLE order_items ( order_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, quantity INT UNSIGNED NOT NULL DEFAULT 1, unit_price DECIMAL(10, 2) NOT NULL, PRIMARY KEY (order_id, product_id) ); -- The combination of order_id + product_id must be unique -- Each column individually can repeat
UNIQUE Constraints
CREATE TABLE users ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, email VARCHAR(254) NOT NULL, username VARCHAR(50) NOT NULL, phone VARCHAR(20), -- can be NULL PRIMARY KEY (id), -- Single-column unique UNIQUE KEY uq_email (email), UNIQUE KEY uq_username (username), -- NULL values are exempt from UNIQUE constraints -- Multiple rows can have NULL in a UNIQUE column UNIQUE KEY uq_phone (phone) -- multiple NULL phones allowed ); -- Composite unique constraint CREATE TABLE team_members ( team_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, role VARCHAR(50), PRIMARY KEY (team_id, user_id), -- A user can be in the same team once, but different teams multiple times -- The PK already enforces this here; in other patterns you'd use: UNIQUE KEY uq_team_user (team_id, user_id) );
Foreign Keys
Foreign keys enforce referential integrity between tables. When a column in one table references the primary key of another, a foreign key constraint ensures:
- You can't insert a row with a non-existent parent ID
- You can't delete a parent row while child rows still reference it (unless ON DELETE CASCADE is set)
CREATE TABLE categories (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE posts (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(500) NOT NULL,
body MEDIUMTEXT NOT NULL,
author_id INT UNSIGNED NOT NULL,
category_id INT UNSIGNED, -- nullable: post may have no category
PRIMARY KEY (id),
-- Foreign key with explicit name (recommended for debugging)
CONSTRAINT fk_posts_author
FOREIGN KEY (author_id)
REFERENCES users (id)
ON DELETE RESTRICT -- prevent deleting a user who has posts
ON UPDATE CASCADE, -- if user id changes, update posts too
CONSTRAINT fk_posts_category
FOREIGN KEY (category_id)
REFERENCES categories (id)
ON DELETE SET NULL -- if category deleted, set post category to NULL
ON UPDATE CASCADE
);Referential Action | ON DELETE behavior | ON UPDATE behavior |
|---|---|---|
RESTRICT (default) | Error if parent row has children | Error if parent key has children |
CASCADE | Delete child rows automatically | Update child FK to match new parent key |
SET NULL | Set child FK column to NULL | Set child FK column to NULL |
NO ACTION | Same as RESTRICT (deferred check in standard SQL) | Same as RESTRICT |
SET DEFAULT | Set child FK to column DEFAULT (rare, limited support) | Set child FK to column DEFAULT |
Inline vs Table-Level Foreign Keys
-- Inline foreign key (less common, no constraint name) author_id INT UNSIGNED NOT NULL REFERENCES users(id), -- Table-level foreign key with name (recommended) CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE RESTRICT,
posts_ibfk_1 if you don't. Named constraints are much easier to reference in ALTER TABLE, error messages, and debugging.Regular Indexes
CREATE TABLE sessions ( id CHAR(64) NOT NULL, user_id INT UNSIGNED NOT NULL, data JSON, ip_address VARCHAR(45), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, PRIMARY KEY (id), -- Simple index INDEX idx_user_id (user_id), -- Composite index (covers queries filtering by both user_id and expires_at) INDEX idx_user_expires (user_id, expires_at), -- Descending index (MySQL 8.0+) — useful for ORDER BY col DESC INDEX idx_created_desc (created_at DESC) );
user_id = 5 AND status = 'active' and user_id is more selective (fewer matching rows), put user_id first. MySQL can also use a composite index (a, b) for queries that filter on only column a.Table Options
CREATE TABLE example ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL ) ENGINE = InnoDB -- storage engine (InnoDB is default and recommended) DEFAULT CHARSET = utf8mb4 -- default character set for this table COLLATE = utf8mb4_unicode_ci -- default collation for this table AUTO_INCREMENT = 1 -- starting value for auto_increment ROW_FORMAT = DYNAMIC -- InnoDB row format COMMENT = 'A descriptive comment about this table' COMPRESSION = 'NONE'; -- or 'ZLIB' / 'LZ4' for compressed tables
Option | Common Values | Notes |
|---|---|---|
ENGINE | InnoDB, MyISAM, MEMORY, CSV | Always use InnoDB for new tables |
DEFAULT CHARSET | utf8mb4 | Overrides database default |
COLLATE | utf8mb4_unicode_ci, utf8mb4_bin | Overrides database default |
AUTO_INCREMENT | Any positive integer | Sets the next AUTO_INCREMENT value |
ROW_FORMAT | DYNAMIC, COMPACT, COMPRESSED | DYNAMIC is the modern default |
COMMENT | Any string up to 1024 chars | Shows in SHOW TABLE STATUS and SHOW CREATE TABLE |
CREATE TABLE IF NOT EXISTS
-- Idempotent table creation (no error if already exists) CREATE TABLE IF NOT EXISTS migrations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, migration_name VARCHAR(255) NOT NULL UNIQUE, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );
CREATE TABLE AS SELECT
You can create a new table and populate it with data from a query in one statement. The new table's columns are derived from the SELECT result.
-- Create a summary table from existing data CREATE TABLE monthly_sales_summary AS SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS order_count, SUM(total) AS total_revenue, AVG(total) AS avg_order_value FROM orders WHERE created_at >= '2024-01-01' GROUP BY DATE_FORMAT(created_at, '%Y-%m'); -- Important: CTAS does NOT copy indexes or constraints from the source -- You must add them manually: ALTER TABLE monthly_sales_summary ADD PRIMARY KEY (month), ADD INDEX idx_revenue (total_revenue);
CREATE TABLE LIKE
CREATE TABLE ... LIKE creates an empty copy of an existing table, including all column definitions, indexes, and constraints — but no data.
-- Create an empty copy with the same structure (including indexes) CREATE TABLE orders_archive LIKE orders; -- Useful for: -- 1. Creating a staging table for bulk imports CREATE TABLE users_import LIKE users; LOAD DATA INFILE '/tmp/new_users.csv' INTO TABLE users_import ...; -- validate, then move to production: INSERT INTO users SELECT * FROM users_import; -- 2. Partitioned archive tables CREATE TABLE orders_2023 LIKE orders; INSERT INTO orders_2023 SELECT * FROM orders WHERE YEAR(created_at) = 2023; DELETE FROM orders WHERE YEAR(created_at) = 2023;
Viewing Table Definitions
-- Show the CREATE TABLE statement (full definition) SHOW CREATE TABLE usersG -- Quick column overview DESCRIBE users; DESC users; -- Detailed column info from information_schema SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'myapp' AND TABLE_NAME = 'users' ORDER BY ORDINAL_POSITION;
+--------------+-----------------+-------------+-------------------+---------------------------+ | COLUMN_NAME | COLUMN_TYPE | IS_NULLABLE | COLUMN_DEFAULT | EXTRA | +--------------+-----------------+-------------+-------------------+---------------------------+ | id | int unsigned | NO | NULL | auto_increment | | email | varchar(254) | NO | NULL | | | is_active | tinyint(1) | NO | 1 | | | created_at | timestamp | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | updated_at | timestamp | NO | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP| +--------------+-----------------+-------------+-------------------+---------------------------+