Loading learning content...
Table design is where abstraction meets reality. Your ER diagrams and normalization decisions now become executable DDL statements that will handle real data at production scale. Every choice—data types, constraints, indexes, naming—has lasting consequences for performance, maintainability, and application development.
This is not clerical work. Thoughtful table design prevents countless hours of debugging, migration pain, and production incidents. Careless table design creates technical debt that compounds with every row added and every query written.
By the end of this page, you will master practical table design: selecting appropriate data types, defining comprehensive constraints, designing effective indexes, following professional naming conventions, and producing production-quality CREATE TABLE statements that withstand real-world demands.
Data type selection directly impacts storage efficiency, query performance, data integrity, and application compatibility. The general principle: choose the most specific type that accommodates all valid values.
| Use Case | Recommended Type | Avoid | Reason |
|---|---|---|---|
| Primary keys (auto) | BIGINT SERIAL / INT SERIAL | VARCHAR, UUID | Integer comparison is faster; auto-increment is efficient |
| Primary keys (distributed) | UUID / ULID | INT (collision risk) | Globally unique without coordination |
| Monetary values | DECIMAL(p,s) / NUMERIC | FLOAT, DOUBLE | Exact precision required; no floating-point errors |
| Timestamps | TIMESTAMP WITH TIME ZONE | VARCHAR, DATE alone | Timezone handling is critical for global systems |
| Boolean flags | BOOLEAN | INT, CHAR(1) | Semantic clarity; database-level validation |
| Short strings (fixed) | CHAR(n) | VARCHAR for fixed | Slightly more efficient for truly fixed-length |
| Variable strings | VARCHAR(n) | TEXT without limit | Enforces maximum length; prevents abuse |
| Long text | TEXT | VARCHAR(MAX) | Optimized for large content; no artificial limit |
| JSON data | JSONB (PostgreSQL) | TEXT with parsing | Indexable, queryable, validated structure |
| IP addresses | INET / CIDR | VARCHAR | Native operators, validation, efficient storage |
| Enumerations | ENUM type or FK to lookup | VARCHAR with values | Database enforces valid values |
Numeric precision errors are insidious—they may not manifest until edge cases appear in production.
12345678910111213141516171819202122232425
-- WRONG: Using FLOAT for moneyCREATE TABLE BadOrder ( total FLOAT -- NEVER do this for money!); INSERT INTO BadOrder VALUES (0.1 + 0.2);SELECT * FROM BadOrder;-- Returns: 0.30000000000000004 (floating-point error!) -- CORRECT: Using DECIMAL for moneyCREATE TABLE GoodOrder ( total DECIMAL(12, 2) -- 12 total digits, 2 after decimal); INSERT INTO GoodOrder VALUES (0.1 + 0.2);SELECT * FROM GoodOrder;-- Returns: 0.30 (exact) -- Choosing DECIMAL precision:-- DECIMAL(10, 2) → max $99,999,999.99 (8 digits before decimal)-- DECIMAL(12, 2) → max $9,999,999,999.99 (10 digits before decimal)-- DECIMAL(15, 2) → max $9,999,999,999,999.99 (13 digits before decimal) -- For currencies with more decimal places (crypto):-- DECIMAL(20, 8) → supports Bitcoin's 8 decimal placesVARCHAR limits should be informed by data requirements, not arbitrary round numbers.
123456789101112131415161718192021222324252627282930313233
-- PRACTICAL LENGTH GUIDELINES (based on real-world data) CREATE TABLE Customer ( -- Email: RFC 5321 allows 254 characters max email VARCHAR(254) NOT NULL, -- Names: 100 is usually sufficient, 200 for safety with international names first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, -- Phone: E.164 format max 15 digits + formatting phone VARCHAR(20), -- Addresses vary by country; 255 is a reasonable max street_address VARCHAR(255), city VARCHAR(100), -- Country/region codes: ISO standards country_code CHAR(2), -- ISO 3166-1 alpha-2 (US, UK, IN) postal_code VARCHAR(20), -- Varies wildly by country -- URLs: 2048 is practical limit for most use cases website VARCHAR(2048), -- For truly unbounded content: bio TEXT, notes TEXT); -- Common mistakes:-- ❌ VARCHAR(255) for everything (arbitrary, not based on data)-- ❌ VARCHAR(50) for email (too short!)-- ❌ TEXT for all strings (loses validation benefit)In interviews, explaining your data type choices demonstrates attention to detail. 'I'm using DECIMAL(12,2) for monetary amounts to avoid floating-point precision errors' shows you understand the real-world implications of type selection.
Constraints are the database's mechanism for enforcing business rules. They're not optional annotations—they're essential safeguards that prevent invalid data from entering the system.
| Constraint | Purpose | Example | When to Use |
|---|---|---|---|
| PRIMARY KEY | Unique identifier for rows | order_id INT PRIMARY KEY | Every table must have one |
| NOT NULL | Prevent missing values | email VARCHAR(254) NOT NULL | Required fields |
| UNIQUE | Prevent duplicate values | email VARCHAR(254) UNIQUE | Natural keys, business identifiers |
| FOREIGN KEY | Referential integrity | REFERENCES Customer(id) | All relationships |
| CHECK | Custom validation rules | CHECK (quantity > 0) | Business rule enforcement |
| DEFAULT | Auto-populate values | DEFAULT CURRENT_TIMESTAMP | Created dates, status defaults |
| EXCLUSION | Prevent overlapping ranges | EXCLUDE USING gist (room_id, tsrange...) | Reservations, scheduling |
Foreign keys are the implementation of relationships in ER diagrams. They require careful thought about referential actions.
123456789101112131415161718192021222324252627282930313233
-- FOREIGN KEY REFERENTIAL ACTIONS CREATE TABLE OrderItems ( order_item_id SERIAL PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), -- ON DELETE CASCADE: Delete items when order is deleted -- Good for: dependent entities, log entries, shopping cart items FOREIGN KEY (order_id) REFERENCES Orders(order_id) ON DELETE CASCADE ON UPDATE CASCADE, -- ON DELETE RESTRICT: Prevent deletion if items exist -- Good for: master data (categories, products in use) FOREIGN KEY (product_id) REFERENCES Products(product_id) ON DELETE RESTRICT ON UPDATE CASCADE); -- REFERENTIAL ACTION OPTIONS:-- CASCADE → Propagate the operation (delete/update children)-- RESTRICT → Prevent the operation if children exist-- SET NULL → Set FK to NULL (column must be nullable)-- SET DEFAULT→ Set FK to default value-- NO ACTION → Like RESTRICT but checked at transaction end -- CHOOSING THE RIGHT ACTION:-- Parent deletion should delete children? → CASCADE-- Parent deletion should be prevented? → RESTRICT-- Children can exist without parent? → SET NULL-- Need to preserve children with default? → SET DEFAULTCHECK constraints encode business logic at the database level, ensuring consistency regardless of which application writes data.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
CREATE TABLE Product ( product_id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, price DECIMAL(10, 2) NOT NULL, sale_price DECIMAL(10, 2), stock_quantity INT NOT NULL DEFAULT 0, min_order_qty INT NOT NULL DEFAULT 1, max_order_qty INT, status VARCHAR(20) NOT NULL DEFAULT 'active', -- Price must be positive CONSTRAINT chk_positive_price CHECK (price > 0), -- Sale price must be less than regular price CONSTRAINT chk_sale_price_valid CHECK (sale_price IS NULL OR (sale_price > 0 AND sale_price < price)), -- Stock cannot be negative CONSTRAINT chk_nonnegative_stock CHECK (stock_quantity >= 0), -- Min order must be positive; max must exceed min CONSTRAINT chk_order_qty_range CHECK (min_order_qty > 0 AND (max_order_qty IS NULL OR max_order_qty >= min_order_qty)), -- Status must be from allowed list CONSTRAINT chk_valid_status CHECK (status IN ('active', 'inactive', 'discontinued', 'coming_soon'))); -- Cross-column CHECK (PostgreSQL):CREATE TABLE Reservation ( reservation_id SERIAL PRIMARY KEY, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, -- Check-out must be after check-in CONSTRAINT chk_valid_dates CHECK (check_out_date > check_in_date), -- Booking can't be too far in advance CONSTRAINT chk_advance_booking CHECK (check_in_date <= CURRENT_DATE + INTERVAL '1 year'));Always name your constraints explicitly (CONSTRAINT chk_positive_price CHECK...). Auto-generated names like 'product_check1' are meaningless in error messages and maintenance. Named constraints make debugging and migration much easier.
Indexes are critical for query performance but have costs: storage space, write overhead, and maintenance during bulk operations. Strategic index design balances read optimization against these costs.
Index creation should be driven by access patterns, not speculation.
123456789101112131415161718192021222324252627282930
-- BASIC INDEX TYPES -- B-tree (default, most common): equality and range queriesCREATE INDEX idx_orders_customer_id ON Orders(customer_id);CREATE INDEX idx_orders_order_date ON Orders(order_date); -- Composite index: for multi-column queries-- Column order matters! Most selective column first, or leftmost columns in WHERECREATE INDEX idx_orders_customer_date ON Orders(customer_id, order_date DESC);-- Supports: WHERE customer_id = ?-- Supports: WHERE customer_id = ? AND order_date > ?-- Does NOT support: WHERE order_date > ? (without customer_id) -- Covering index: includes all columns needed by queryCREATE INDEX idx_orders_covering ON Orders(customer_id) INCLUDE (order_date, total_amount, status);-- Query can be answered entirely from index without table access -- Partial index: index only rows matching conditionCREATE INDEX idx_orders_pending ON Orders(order_date) WHERE status = 'pending';-- Smaller, faster index for common query pattern -- Hash index: equality only, faster for exact matchesCREATE INDEX idx_products_sku ON Products USING hash(sku); -- GiST/GIN: for complex types (arrays, JSON, full-text)CREATE INDEX idx_products_tags ON Products USING gin(tags);CREATE INDEX idx_documents_content ON Documents USING gin(to_tsvector('english', content));For composite indexes, put the most selective column (highest cardinality/most unique values) first—unless your queries always filter on a specific column regardless of selectivity. The leftmost columns must be present in WHERE for the index to be used.
Consistent naming conventions improve readability, reduce errors, and make schemas self-documenting. While conventions vary, consistency is more important than any specific choice.
| Element | Convention | Example | Rationale |
|---|---|---|---|
| Tables | Singular, PascalCase or snake_case | Customer, order_item | Consistency with model; singular = row represents one entity |
| Columns | snake_case | first_name, order_date | Readable, avoids case-sensitivity issues |
| Primary Keys | table_id or id | customer_id, id | Immediately identifiable as PK |
| Foreign Keys | referenced_table_id | customer_id (in Orders) | Clear relationship indication |
| Indexes | idx_table_columns | idx_orders_customer_id | Clear purpose and scope |
| Unique Constraints | uq_table_columns | uq_customer_email | Distinguishes from regular indexes |
| Check Constraints | chk_table_rule | chk_product_positive_price | Self-documenting constraint purpose |
| Foreign Keys | fk_child_parent | fk_orders_customer | Clear relationship direction |
| Junction Tables | parent1_parent2 or verb | student_course, enrollment | Clear M:N relationship |
123456789101112131415161718192021222324252627282930313233343536373839404142434445
-- WELL-NAMED SCHEMA CREATE TABLE customer ( customer_id SERIAL PRIMARY KEY, email VARCHAR(254) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, phone_number VARCHAR(20), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT true, CONSTRAINT uq_customer_email UNIQUE (email)); CREATE TABLE product_category ( category_id SERIAL PRIMARY KEY, category_name VARCHAR(100) NOT NULL, parent_category_id INT, display_order INT NOT NULL DEFAULT 0, CONSTRAINT fk_category_parent FOREIGN KEY (parent_category_id) REFERENCES product_category(category_id), CONSTRAINT chk_category_not_self_parent CHECK (parent_category_id <> category_id)); CREATE TABLE "order" ( -- Quoted because ORDER is reserved order_id SERIAL PRIMARY KEY, customer_id INT NOT NULL, order_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) NOT NULL DEFAULT 'pending', shipping_address_id INT, total_amount DECIMAL(12, 2), CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id), CONSTRAINT chk_order_valid_status CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled'))); -- Indexes with clear namesCREATE INDEX idx_order_customer_id ON "order"(customer_id);CREATE INDEX idx_order_date ON "order"(order_date DESC);CREATE INDEX idx_order_status ON "order"(status) WHERE status NOT IN ('delivered', 'cancelled');Avoid SQL reserved words as table/column names (order, user, group, table, index). If unavoidable, use quoted identifiers ("order") or prefixes (tbl_order, user_account). Different databases have different reserved words—check documentation.
Time-related data is deceptively complex. Poor handling of timestamps, timezones, and temporal queries leads to subtle bugs that are difficult to diagnose in production.
1234567891011121314151617181920212223242526272829303132333435
-- ALWAYS use TIMESTAMP WITH TIME ZONE for points in timeCREATE TABLE audit_log ( log_id SERIAL PRIMARY KEY, event_type VARCHAR(50) NOT NULL, event_data JSONB, -- Good: Timezone-aware timestamp created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Bad: Ambiguous - what timezone? -- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Use DATE for calendar dates (no time component)CREATE TABLE employee ( employee_id SERIAL PRIMARY KEY, -- Date of birth is a calendar date, not a point in time birth_date DATE, -- Hire date is also a calendar date hire_date DATE NOT NULL, -- But last_login is a point in time last_login TIMESTAMP WITH TIME ZONE); -- Use TIME for time-of-day without dateCREATE TABLE business_hours ( location_id INT REFERENCES location(location_id), day_of_week INT CHECK (day_of_week BETWEEN 0 AND 6), open_time TIME NOT NULL, close_time TIME NOT NULL, PRIMARY KEY (location_id, day_of_week));Most entities benefit from tracking creation and modification times.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
-- STANDARD AUDIT COLUMNS (add to most tables)CREATE TABLE product ( product_id SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, price DECIMAL(10, 2) NOT NULL, -- Audit columns created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, created_by INT REFERENCES user_account(user_id), updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_by INT REFERENCES user_account(user_id)); -- Auto-update updated_at with triggerCREATE OR REPLACE FUNCTION update_updated_at_column()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW;END;$$ LANGUAGE plpgsql; CREATE TRIGGER trigger_product_updated_at BEFORE UPDATE ON product FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- SOFT DELETE PATTERN (don't physically delete)CREATE TABLE customer ( customer_id SERIAL PRIMARY KEY, email VARCHAR(254) NOT NULL, -- Soft delete columns is_deleted BOOLEAN DEFAULT false NOT NULL, deleted_at TIMESTAMP WITH TIME ZONE, deleted_by INT REFERENCES user_account(user_id), -- Only non-deleted emails must be unique CONSTRAINT uq_customer_email_active UNIQUE NULLS NOT DISTINCT (email) WHERE (NOT is_deleted)); -- Partial index to exclude deleted rows from most queriesCREATE INDEX idx_customer_active ON customer(customer_id) WHERE NOT is_deleted;Always store timestamps in UTC (TIMESTAMP WITH TIME ZONE typically stores in UTC and converts on display). Convert to user's timezone in the application layer. Never rely on the database server's local timezone—it may change unexpectedly.
Let's bring together all concepts in a production-quality schema for an e-commerce order system.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
-- ==============================================-- E-COMMERCE ORDER SYSTEM - PRODUCTION SCHEMA-- ============================================== -- Customer (with soft delete and audit)CREATE TABLE customer ( customer_id BIGSERIAL PRIMARY KEY, email VARCHAR(254) NOT NULL, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, phone VARCHAR(20), -- Audit columns created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, -- Soft delete is_active BOOLEAN DEFAULT true NOT NULL, deactivated_at TIMESTAMP WITH TIME ZONE, -- Constraints CONSTRAINT uq_customer_email_active UNIQUE (email) -- Could add WHERE is_active for soft delete); -- Customer address (one-to-many)CREATE TABLE customer_address ( address_id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, address_type VARCHAR(20) NOT NULL DEFAULT 'shipping', is_default BOOLEAN DEFAULT false NOT NULL, recipient_name VARCHAR(200) NOT NULL, street_line_1 VARCHAR(255) NOT NULL, street_line_2 VARCHAR(255), city VARCHAR(100) NOT NULL, state_province VARCHAR(100), postal_code VARCHAR(20) NOT NULL, country_code CHAR(2) NOT NULL, phone VARCHAR(20), -- Audit created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, -- Constraints CONSTRAINT fk_address_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id) ON DELETE CASCADE, CONSTRAINT chk_address_type CHECK (address_type IN ('shipping', 'billing'))); -- Product category (self-referencing hierarchy)CREATE TABLE product_category ( category_id SERIAL PRIMARY KEY, parent_category_id INT, name VARCHAR(100) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE, description TEXT, display_order INT DEFAULT 0 NOT NULL, is_active BOOLEAN DEFAULT true NOT NULL, CONSTRAINT fk_category_parent FOREIGN KEY (parent_category_id) REFERENCES product_category(category_id), CONSTRAINT chk_category_not_self_parent CHECK (parent_category_id IS NULL OR parent_category_id <> category_id)); -- ProductCREATE TABLE product ( product_id BIGSERIAL PRIMARY KEY, sku VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, slug VARCHAR(200) NOT NULL UNIQUE, description TEXT, category_id INT NOT NULL, -- Pricing base_price DECIMAL(12, 2) NOT NULL, sale_price DECIMAL(12, 2), cost_price DECIMAL(12, 2), -- For margin calculation -- Inventory stock_quantity INT NOT NULL DEFAULT 0, low_stock_threshold INT DEFAULT 10, -- Status status VARCHAR(20) NOT NULL DEFAULT 'draft', is_featured BOOLEAN DEFAULT false NOT NULL, -- Metadata weight_kg DECIMAL(8, 3), -- Audit created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, published_at TIMESTAMP WITH TIME ZONE, -- Constraints CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES product_category(category_id), CONSTRAINT chk_product_base_price CHECK (base_price > 0), CONSTRAINT chk_product_sale_price CHECK (sale_price IS NULL OR (sale_price > 0 AND sale_price < base_price)), CONSTRAINT chk_product_status CHECK (status IN ('draft', 'active', 'inactive', 'discontinued')), CONSTRAINT chk_product_stock CHECK (stock_quantity >= 0)); -- OrderCREATE TABLE "order" ( order_id BIGSERIAL PRIMARY KEY, order_number VARCHAR(20) NOT NULL UNIQUE, -- Human-readable order number customer_id BIGINT NOT NULL, -- Status tracking status VARCHAR(20) NOT NULL DEFAULT 'pending', -- Address snapshot (copied from customer_address at order time) shipping_address JSONB NOT NULL, billing_address JSONB NOT NULL, -- Totals subtotal DECIMAL(12, 2) NOT NULL, shipping_cost DECIMAL(10, 2) NOT NULL DEFAULT 0, tax_amount DECIMAL(10, 2) NOT NULL DEFAULT 0, discount_amount DECIMAL(10, 2) NOT NULL DEFAULT 0, total_amount DECIMAL(12, 2) NOT NULL, -- Payment currency_code CHAR(3) NOT NULL DEFAULT 'USD', payment_method VARCHAR(50), payment_reference VARCHAR(255), -- Timestamps ordered_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, confirmed_at TIMESTAMP WITH TIME ZONE, shipped_at TIMESTAMP WITH TIME ZONE, delivered_at TIMESTAMP WITH TIME ZONE, cancelled_at TIMESTAMP WITH TIME ZONE, -- Notes customer_notes TEXT, internal_notes TEXT, -- Constraints CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id), CONSTRAINT chk_order_status CHECK (status IN ('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded')), CONSTRAINT chk_order_totals CHECK (total_amount = subtotal + shipping_cost + tax_amount - discount_amount), CONSTRAINT chk_order_amounts_positive CHECK (subtotal >= 0 AND shipping_cost >= 0 AND tax_amount >= 0 AND total_amount >= 0)); -- Order items (weak entity dependent on order)CREATE TABLE order_item ( order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, -- Quantity and pricing at time of order quantity INT NOT NULL, unit_price DECIMAL(12, 2) NOT NULL, -- Price at order time discount_percent DECIMAL(5, 2) DEFAULT 0 NOT NULL, line_total DECIMAL(12, 2) NOT NULL, -- Product snapshot (in case product changes/deleted) product_name VARCHAR(200) NOT NULL, product_sku VARCHAR(50) NOT NULL, -- Constraints PRIMARY KEY (order_id, product_id), CONSTRAINT fk_orderitem_order FOREIGN KEY (order_id) REFERENCES "order"(order_id) ON DELETE CASCADE, CONSTRAINT fk_orderitem_product FOREIGN KEY (product_id) REFERENCES product(product_id) ON DELETE RESTRICT, CONSTRAINT chk_orderitem_quantity CHECK (quantity > 0), CONSTRAINT chk_orderitem_price CHECK (unit_price >= 0), CONSTRAINT chk_orderitem_discount CHECK (discount_percent >= 0 AND discount_percent <= 100), CONSTRAINT chk_orderitem_linetotal CHECK (line_total = quantity * unit_price * (1 - discount_percent / 100))); -- ==============================================-- INDEXES-- ============================================== -- Customer indexesCREATE INDEX idx_customer_email ON customer(email);CREATE INDEX idx_customer_active ON customer(customer_id) WHERE is_active; -- Address indexesCREATE INDEX idx_address_customer ON customer_address(customer_id); -- Product indexesCREATE INDEX idx_product_category ON product(category_id);CREATE INDEX idx_product_status ON product(status) WHERE status = 'active';CREATE INDEX idx_product_featured ON product(is_featured, created_at DESC) WHERE is_featured; -- Order indexesCREATE INDEX idx_order_customer ON "order"(customer_id);CREATE INDEX idx_order_status ON "order"(status);CREATE INDEX idx_order_date ON "order"(ordered_at DESC);CREATE INDEX idx_order_customer_status ON "order"(customer_id, status, ordered_at DESC); -- Order item indexes (PK already covers order_id, product_id)-- Additional index if querying by product across ordersCREATE INDEX idx_orderitem_product ON order_item(product_id);When presenting table designs in interviews, structure your communication to demonstrate thoroughness and professionalism.
Interviewers care more about your reasoning than the specific design. Explain WHY you chose each data type, constraint, and index. 'I'm using DECIMAL(12,2) for monetary values because floating-point would introduce precision errors' demonstrates deeper understanding than just writing the DDL.
What's Next:
With table design complete, the final step is Design Review—validating your schema against requirements, evaluating performance characteristics, and preparing to defend your design in technical discussions. This is where good designs become great through systematic critique.
You now have the practical skills to translate ER models into production-quality table definitions. You understand data type selection, constraint specification, index design, and professional conventions. Next, we'll learn how to review and validate these designs systematically.