You have seen the types one by one. Here is the decision guide you can keep next to your editor.
The questions to ask
- What kind of thing is it? A number, text, date, true/false, structured data, raw bytes?
- Do I do arithmetic on it? If not, it may not be a number. A phone number or postal code is text, even though it looks like a number. It can start with
0, contain+or spaces, and you never add two phone numbers. - How large can it get? Think about the biggest realistic value, then add room for growth.
- Can it be unknown? Decide
NULLorNOT NULL. - Will I search, sort or join on it? Then keep it small. It will be in an index.
Cheat sheet
| Data | Recommended type | Why |
|---|---|---|
| Primary key | INT UNSIGNED AUTO_INCREMENT or BIGINT UNSIGNED | Compact, fast, sequential |
| Public ID | BINARY(16) UUID, next to the integer key | Not guessable |
| Name | VARCHAR(100) | Enough for real names |
VARCHAR(255) | Standard maximum is 254 | |
| Password hash | VARCHAR(255) | Hash formats grow over time |
| Phone number | VARCHAR(20) | Text, with country code |
| Postal code | VARCHAR(10) | Text, because of leading zeros and letters |
| Country / currency code | CHAR(2) / CHAR(3) | Fixed length |
| Slug, URL path | VARCHAR(100) to VARCHAR(255) | |
| Long URL | VARCHAR(2048) or TEXT | |
| Article body | MEDIUMTEXT | Up to 16 MB |
| Money | DECIMAL(10,2) or an integer in the smallest unit | Exact |
| Quantity, stock | INT UNSIGNED (or SMALLINT if small) | |
| Rating 1 to 5 | TINYINT UNSIGNED | One byte |
| Percentage | DECIMAL(5,2) | |
| Latitude / longitude | DECIMAL(9,6) | |
| Yes / no | BOOLEAN (TINYINT(1)) with NOT NULL | |
| Small stable list | ENUM, or VARCHAR plus CHECK | |
| Growing list | Lookup table and foreign key | |
| Date without time | DATE | |
| Moment in time | DATETIME (UTC) or TIMESTAMP | |
| IP address | VARBINARY(16) | INET6_ATON |
| Hash | BINARY(32) for SHA-256 | Or CHAR(64) |
| Flexible attributes | JSON | Not for data you filter on |
| Files | Path in VARCHAR; file elsewhere |
Mistakes seen again and again
- Using
VARCHAR(255)for everything. It hides the rules and makes indexes bigger than needed. - Storing dates as text, or numbers as text you compute with.
- Using
FLOATfor money. - Using
INTfor a table that will exceed 2 billion rows (orBIGINTfor a lookup table with ten rows). - A phone number as
INT. It loses leading zeros and cannot store+91. - One huge
TEXTcolumn for structured data that you later need to search. - A table with 100 columns. It often means that several things are mixed together (Normalization).
- Different types for the same fact.
users.idasINTandorders.user_idasBIGINTbreaks index use in joins and foreign keys. A foreign key column must match the referenced key’s type exactly (and sign).
CREATE TABLE users (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL);
CREATE TABLE posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
ERROR 3780 (HY000): Referencing column 'user_id' and referenced column 'id' in foreign key constraint 'posts_ibfk_1' are incompatible.
The foreign key was refused because BIGINT (signed) does not match INT UNSIGNED. Make the types identical:
CREATE TABLE posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
SHOW TABLES;
+-----------------+
| Tables_in_learn |
+-----------------+
| posts |
| users |
+-----------------+
2 rows in set
Changing your mind later
Changing a column’s type later rebuilds the table, which can take a long time on big tables (Online DDL). The earlier you pick the right type, the cheaper it is. Spend ten minutes on types before you create a table that will hold millions of rows.
Exercise: design the columns
You are designing a members table for a gym. Choose a type for: member id, full name, phone number, date of birth, monthly fee in rupees with paise, is_active flag and joined_at moment. Write the CREATE TABLE statement.
Show a possible solution
CREATE TABLE members (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
date_of_birth DATE NOT NULL,
monthly_fee DECIMAL(8,2) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
DESCRIBE members;
+---------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+-------------------+-------------------+
| id | int unsigned | NO | PRI | NULL | auto_increment |
| full_name | varchar(100) | NO | | NULL | |
| phone | varchar(20) | NO | | NULL | |
| date_of_birth | date | NO | | NULL | |
| monthly_fee | decimal(8,2) | NO | | NULL | |
| is_active | tinyint(1) | NO | | 1 | |
| joined_at | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+---------------+--------------+------+-----+-------------------+-------------------+
7 rows in set