MySQL Create Table
What Does Creating a Table Involve? In simple terms: A table is like a spreadsheet with strict rules. Before storing data, you define its columns — each with a name and a type (number, text, date) — a
What Does Creating a Table Involve?
In simple terms: A table is like a spreadsheet with strict rules. Before storing data, you define its columns — each with a name and a type (number, text, date) — and the rules the data must follow: which values are required, which must be unique, and how tables relate to each other. A well-designed table prevents bad data from ever being saved.
Technically: CREATE TABLE defines a table's columns, data types, defaults, and constraints: PRIMARY KEY, UNIQUE, NOT NULL, CHECK, and FOREIGN KEY. It also defines indexes for fast searches and table options such as the storage engine (InnoDB) and character set. From PHP, the statement is executed with mysqli::query().
Anatomy of a CREATE TABLE Statement
CREATE TABLE IF NOT EXISTS products (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
sku VARCHAR(30) NOT NULL,
name VARCHAR(150) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT UNSIGNED NOT NULL DEFAULT 0,
category VARCHAR(50) NOT NULL DEFAULT 'general',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_products_sku (sku),
KEY idx_products_category (category),
CONSTRAINT chk_products_price CHECK (price >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Line by Line
| Part | Meaning |
|---|---|
id INT UNSIGNED NOT NULL AUTO_INCREMENT | A whole number that MySQL assigns automatically (1, 2, 3, …) for each new row |
sku VARCHAR(30) NOT NULL | Text up to 30 characters; a value is required |
price DECIMAL(10,2) | Exact decimal number with 2 decimal places (up to 99,999,999.99) |
stock ... DEFAULT 0 | If no value is given on insert, 0 is used |
is_active BOOLEAN | Stored as TINYINT(1): 1 for true, 0 for false |
created_at ... DEFAULT CURRENT_TIMESTAMP | Filled automatically with the time of insertion |
updated_at ... ON UPDATE CURRENT_TIMESTAMP | Updated automatically whenever the row changes |
PRIMARY KEY (id) | Uniquely identifies every row; automatically indexed |
UNIQUE KEY uq_products_sku (sku) | No two products may share a SKU |
KEY idx_products_category (category) | An index to speed up filtering by category |
CHECK (price >= 0) | Rejects negative prices (enforced since MySQL 8.0.16) |
ENGINE=InnoDB | The standard engine, supporting transactions and foreign keys |
Choosing Data Types
| Data | Recommended Type | Notes |
|---|---|---|
| IDs | INT UNSIGNED or BIGINT UNSIGNED with AUTO_INCREMENT | BIGINT for very large tables |
| Short text (names, emails, SKUs) | VARCHAR(n) | n is the maximum number of characters |
| Long text (descriptions, posts) | TEXT / MEDIUMTEXT | Cannot have a plain default value; not fully indexable |
| Money | DECIMAL(10,2) (or an INT/BIGINT of paise) | Never FLOAT/DOUBLE: they are approximate (see PHP Floats) |
| Quantities, counts | INT UNSIGNED | Unsigned when negative values are impossible |
| Yes/no flags | BOOLEAN (TINYINT(1)) | |
| Date and time | DATETIME | Stores the value as given; store UTC consistently (see Timezones) |
| Date only | DATE | Birthdays, due dates |
| Fixed set of values | VARCHAR matched to a PHP enum, or ENUM(...) | Changing an ENUM list requires altering the table |
| Structured flexible data | JSON | Validated JSON; use sparingly |
Note:
TIMESTAMPconverts values between the connection's time zone and UTC and is limited to the year 2038.DATETIMEis simpler and has no such limit.
Relationships With Foreign Keys
A foreign key links a column to the primary key of another table and guarantees that the referenced row exists:
CREATE TABLE IF NOT EXISTS orders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
customer_id INT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_orders_customer_created (customer_id, created_at),
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id)
REFERENCES customers (id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ON DELETE Option | When the Referenced Customer Is Deleted |
|---|---|
RESTRICT | The deletion is rejected while orders exist (safest for important records) |
CASCADE | The related orders are deleted too |
SET NULL | customer_id is set to NULL (the column must be nullable) |
Foreign keys protect data integrity: an order can never refer to a customer that does not exist. The referencing column must have the same type as the referenced one (here, both INT UNSIGNED).
Creating the Shop Schema From PHP
The following script creates all four tables of the example database. It runs with an account that has the CREATE privilege (see MySQL Create DB):
<?php
// bin/create-tables.php — run with: php bin/create-tables.php
$db = new mysqli('127.0.0.1', getenv('DB_ADMIN_USER') ?: 'root', getenv('DB_ADMIN_PASSWORD') ?: '', 'gocourse_shop');
$db->set_charset('utf8mb4');
$options = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci';
$tables = [
'customers' => "CREATE TABLE IF NOT EXISTS customers (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
city VARCHAR(100) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_customers_email (email)
) $options",
'products' => "CREATE TABLE IF NOT EXISTS products (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
sku VARCHAR(30) NOT NULL,
name VARCHAR(150) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT UNSIGNED NOT NULL DEFAULT 0,
category VARCHAR(50) NOT NULL DEFAULT 'general',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_products_sku (sku),
KEY idx_products_category (category),
CONSTRAINT chk_products_price CHECK (price >= 0)
) $options",
'orders' => "CREATE TABLE IF NOT EXISTS orders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
customer_id INT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_orders_customer_created (customer_id, created_at),
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE RESTRICT
) $options",
'order_items' => "CREATE TABLE IF NOT EXISTS order_items (
order_id INT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
quantity INT UNSIGNED NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
CONSTRAINT fk_items_product FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT,
CONSTRAINT chk_items_quantity CHECK (quantity > 0)
) $options",
];
try {
foreach ($tables as $name => $sql) {
$db->query($sql);
echo "Table ready: $name\n";
}
} catch (mysqli_sql_exception $e) {
fwrite(STDERR, "Failed: " . $e->getMessage() . "\n");
exit(1);
}
$result = $db->query('DESCRIBE products');
echo "\n";
while ($column = $result->fetch_assoc()) {
echo rtrim(sprintf('%-11s %-14s %-4s %s', $column['Field'], $column['Type'], $column['Null'], $column['Key'])) . "\n";
}
Output (MySQL 8.4)
Table ready: customers
Table ready: products
Table ready: orders
Table ready: order_items
id int unsigned NO PRI
sku varchar(30) NO UNI
name varchar(150) NO
price decimal(10,2) NO
stock int unsigned NO
category varchar(50) NO MUL
is_active tinyint(1) NO
created_at datetime NO
updated_at datetime NO
Step-by-Step Explanation
- The tables are created in dependency order:
customersandproductsfirst, becauseordersandorder_itemsreference them with foreign keys. IF NOT EXISTSmakes the script safe to run again.order_itemsuses a composite primary key(order_id, product_id): each product appears at most once per order.order_items.unit_pricestores the price at the time of the order, so later price changes do not alter past orders.DESCRIBE productsreturns one row per column.PRI,UNI, andMULin theKeycolumn indicate primary key, unique index, and a non-unique index.BOOLEANappears astinyint(1).
Changing Tables Later: Migrations
Applications evolve: new columns, new indexes, new tables. These changes use ALTER TABLE:
ALTER TABLE customers ADD COLUMN phone VARCHAR(20) NULL AFTER email;
ALTER TABLE products ADD INDEX idx_products_name (name);
In real projects, every schema change is stored as a versioned migration file in the repository, and a tool applies them in order on each environment (development, staging, production). Popular tools include Laravel Migrations, Doctrine Migrations, and Phinx. Migrations keep all databases consistent and make changes reviewable like any other code.
Common Mistakes
Using FLOAT for Money
Effect: Rounding errors in totals.
Fix: Use DECIMAL(10,2) or integer paise.
Missing Unique Constraints
Effect: Duplicate emails or SKUs are inserted when two requests run at the same time, even if PHP checked first.
Fix: Enforce uniqueness in the database with UNIQUE keys.
Foreign Key Type Mismatch
Effect: Error 3780 / "incompatible" foreign key when, for example, INT references INT UNSIGNED.
Fix: Use exactly the same type for both columns.
Creating Tables in the Wrong Order
Effect: Foreign keys fail because the referenced table does not exist yet.
Fix: Create referenced tables first.
Using the Old utf8 Character Set or MyISAM
Effect: No emoji support; no transactions or foreign keys.
Fix: Use utf8mb4 and InnoDB.
Best Practices
- Give every table an auto-increment primary key (or a deliberate composite key).
- Use precise types:
DECIMALfor money,DATETIMEfor timestamps,VARCHARwith realistic lengths. - Enforce rules in the database:
NOT NULL,UNIQUE,CHECK, and foreign keys. - Add indexes for columns used in searches, joins, and sorting.
- Manage schema changes with versioned migrations.
Official reference: https\://www.php.net/manual/en/mysqli.query.php