Creating tables & indexes in MySQL

Designing the schema is where the real work happens. Good column types and constraints stop bad data from entering the database at all, and well-chosen indexes keep queries fast as the table grows.

CREATE TABLE

CREATE TABLE customers (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(120) NOT NULL,
  email VARCHAR(190) UNIQUE,
  city VARCHAR(80) NOT NULL DEFAULT 'Unknown',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NOT NULL,
  total DECIMAL(10,2) NOT NULL,
  status ENUM('NEW','PAID','SHIPPED') NOT NULL DEFAULT 'NEW',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (customer_id) REFERENCES customers(id)
);

CREATE INDEX idx_orders_customer ON orders(customer_id);

Use utf8mb4 so Hindi text and emojis store correctly — the older utf8 alias is a three-byte subset that cannot hold them. ENGINE=InnoDB gives you foreign keys, row-level locks and transactions.

Choosing column types

Type Use for Note
INT / BIGINT ids and counts UNSIGNED doubles the positive range
DECIMAL(10,2) money Exact; never use FLOAT for rupees
VARCHAR(n) names, emails Only the bytes used are stored
CHAR(n) fixed codes such as a 6-digit PIN code Padded to n characters
DATE / DATETIME dates and timestamps Store UTC for multi-region apps
ENUM(...) a short fixed list Adding a value needs ALTER TABLE
TEXT / JSON long notes, flexible payloads Cannot take a plain default

Keep lengths realistic: an index on VARCHAR(190) for email fits comfortably inside MySQL's index key limits.

Constraints

Constraint Guarantees
PRIMARY KEY Unique and not null; one per table
UNIQUE No duplicates, e.g. a GSTIN or an email
NOT NULL A value must be supplied
DEFAULT Value used when none is given
FOREIGN KEY The referenced row exists
CHECK A condition, enforced from MySQL 8.0.16
ALTER TABLE orders
  ADD COLUMN gst_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
  ADD CONSTRAINT chk_total_positive CHECK (total >= 0);

A foreign key can also say what happens to children: ON DELETE CASCADE removes the orders along with the customer, while the default ON DELETE RESTRICT blocks the delete.

Indexes

An index is a sorted structure that lets MySQL find rows without reading the whole table. Add them on the columns used in WHERE, JOIN and ORDER BY.

CREATE INDEX idx_orders_created ON orders(created_at);
CREATE UNIQUE INDEX uq_customers_email ON customers(email);
SHOW INDEX FROM orders;

Every index speeds reads but slows INSERT, UPDATE and DELETE and costs disk space, so add them deliberately and check query plans with EXPLAIN.

Normalise first

Store each fact once: customer details in customers, one row per order in orders, one row per line item in order_items. Repeating the customer's city inside every order row invites contradictions the day someone moves.

Common mistakes

  • Storing money in FLOAT and watching 1249.99 become 1249.9899.
  • Using VARCHAR(255) for everything, including a two-letter state code.
  • Creating a separate index per column instead of one composite index in the right order.
  • Omitting NOT NULL, so half the rows end up with a NULL city.
  • Choosing MyISAM, which silently ignores foreign keys and transactions.

Practice

Design a three-table schema for a sweet shop: products, orders and order_items, with correct types, foreign keys from each item to its order and product, a UNIQUE product SKU and an index on orders(created_at). Insert five rows into each table and run EXPLAIN on a query that filters orders by date.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India