AD JB Toolbox 01 EN

MariaDB Tutorial / Design Single and Composite Indexes for Real Queries

An index is a separate structure that can help locate and order rows, and the column order of a composite index influences which queries it supports. Review predicates, sorting, required columns and existing indexes, then weigh read benefits against write and storage costs.

Treat an index as maintained data rather than a free option

Ordered keys can narrow a search instead of examining every row, but inserts, updates and deletions also maintain indexes. More indexes are not automatically better. Consider query frequency, selectivity, joins and ordering together. Use Read EXPLAIN and Compare Plans Before and After an Index to inspect actual key selection and Improve Date Filters, Sorting, and Pagination Without Changing Results to compare predicate/pagination changes without altering result meaning.

Query or rule Candidate key order Check
Recent orders for a customer customer_id, ordered_at, order_id Equality followed by time range and tie-breaker
All orders by date A time-leading design may be needed Different from a customer-leading requirement
Unique product code UNIQUE(sku) Integrity as well as lookup
Existing foreign-key support Inspect SHOW INDEX Avoid accidental duplication

Check the MariaDB documentation for the syntax and conditions used here: building the best index for a given select.

Inspect indexes that may already exist

Connect to the learning server as course_user and select shopcourse. The command-line and UTF-8 SQL-file guide explains how to save the code as a UTF-8 file and execute it with SOURCE C:/mariadb-lab/lesson.sql;, replacing the path with the actual file. Run SQL inside the MariaDB client, rather than at the operating-system prompt.

Check SELECT VERSION(), DATABASE(); and SHOW TABLES; before creating c39_customers, c39_products, c39_orders, c39_items. These names belong only to this exercise. If they exist, inspect their definitions and rows first; remove them only when you have established that they are disposable exercise objects. Do not replace existing application tables.

USE shopcourse;
CREATE TABLE c39_customers (
 customer_id INT NOT NULL PRIMARY KEY,
 customer_name VARCHAR(50) NOT NULL,
 city VARCHAR(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c39_products (
 product_id INT NOT NULL PRIMARY KEY,
 product_name VARCHAR(100) NOT NULL,
 category VARCHAR(30) NOT NULL,
 price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
 stock INT NOT NULL CHECK (stock >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c39_orders (
 order_id INT NOT NULL PRIMARY KEY,
 customer_id INT NOT NULL,
 ordered_at DATETIME NOT NULL,
 status VARCHAR(20) NOT NULL,
 shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 1000,
 CONSTRAINT fk_c39_order_customer FOREIGN KEY (customer_id)
  REFERENCES c39_customers(customer_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c39_items (
 order_id INT NOT NULL,
 product_id INT NOT NULL,
 quantity INT NOT NULL CHECK (quantity > 0),
 unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price >= 0),
 PRIMARY KEY (order_id, product_id),
 CONSTRAINT fk_c39_item_order FOREIGN KEY (order_id)
  REFERENCES c39_orders(order_id) ON DELETE RESTRICT,
 CONSTRAINT fk_c39_item_product FOREIGN KEY (product_id)
  REFERENCES c39_products(product_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO c39_customers VALUES
 (1,'김민수','서울'),(2,'이서연','부산'),
 (3,'박지우','서울'),(4,'최하늘','대전');
INSERT INTO c39_products VALUES
 (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50),
 (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5);
INSERT INTO c39_orders
 (order_id,customer_id,ordered_at,status,shipping_fee) VALUES
 (101,1,'2026-01-10 10:00:00','paid',1000),
 (102,1,'2026-01-20 11:00:00','paid',1000),
 (103,2,'2026-02-05 12:00:00','paid',1000),
 (104,3,'2026-02-10 13:00:00','canceled',1000);
INSERT INTO c39_items VALUES
 (101,1,2,3500),(101,2,3,1500),(102,3,1,8000),
 (103,1,1,3500),(103,3,2,8000),(104,2,1,1500);

The SQL retains Korean sample names and city strings, while the explanations use stable customer and product IDs. Products 1–4 are a notebook, pen, mug and lamp. Customers 1 and 3 have the Seoul city value; customers 2 and 4 have Busan and Daejeon values. Preserve the supplied UTF-8 literals when reproducing the results.

SHOW INDEX FROM c39_orders;

Primary and foreign keys can already require supporting indexes. Inspect actual columns and Seq_in_index rather than infer a difference from names alone. Check whether a key was created automatically and what constraint depends on it.

Create a customer, timestamp and identifier index

CREATE INDEX idx_c39_customer_date ON c39_orders(customer_id,ordered_at,order_id);
EXPLAIN SELECT order_id,ordered_at,status FROM c39_orders
WHERE customer_id=1 AND ordered_at>='2026-01-01' AND ordered_at<'2026-02-01'
ORDER BY ordered_at,order_id;
SELECT order_id FROM c39_orders
WHERE customer_id=1 AND ordered_at>='2026-01-01' AND ordered_at<'2026-02-01'
ORDER BY ordered_at,order_id;
SHOW INDEX FROM c39_orders;

The selected orders are 101 and 102. customer_id narrows to one customer, ordered_at expresses the period and order_id resolves equal-time ordering. With only four rows, the optimizer can reasonably prefer a full scan; this fixture does not prove a speedup.

Read leading-column, range and covering behavior carefully

Composite index usefulness generally depends on its leading columns. A date-only query should not be assumed to receive the same efficient range access from a customer-leading key. That is also different from claiming that no kind of index read is ever possible without a leading equality predicate.

Range conditions, sort direction, string prefixes and selected columns can alter the usable access path. A covering index can avoid some table-row access when it contains the needed information, but indiscriminately including every column increases key size and write cost. Inspect the chosen key and Extra in EXPLAIN.

Check overlapping indexes and constraint dependencies

Different names can conceal identical or overlapping column sequences, uniqueness and prefix lengths. Reinspect SHOW INDEX after creating the composite key to see how foreign-key support is represented. Dropping the only supporting index while retaining a foreign key can be rejected.

Before removal, identify the actual constraint dependencies and other queries, and prepare any required replacement. PRIMARY KEY and UNIQUE definitions also enforce data rules; a key that appears unused by one query is not automatically dispensable.

Preserve results while comparing access costs

Adding an index must not change the intended rows or sort meaning. Compare representative plans, actual reads, latency and write overhead using realistic distributions. Record environment and method; a warmer cache on the second run is not proof that the index caused the entire difference.

Before repeating the exercise, inspect the selected database, actual table definitions and stored rows, and back up anything that must be kept. Drop only this article’s disposable practice objects, with child tables before their referenced parents. DROP TABLE removes both definitions and rows and cannot be undone by an ordinary ROLLBACK; keep cleanup separate from transaction experiments.

DROP TABLE shopcourse.c39_items;
DROP TABLE shopcourse.c39_orders;
DROP TABLE shopcourse.c39_products;
DROP TABLE shopcourse.c39_customers;
More in This Category
MariaDB Tutorial / Read EXPLAIN and Compare Plans Before and After an Index

MariaDB Tutorial / Read EXPLAIN and Compare Plans Before and After an Index

Use a thousand-row fixture to compare EXPLAIN before and after indexing with the actual returned rows. Distinguish ANALYZE FORMAT=JSON execution data from statistics maintenance.

MariaDB Tutorial / Troubleshoot Local and Remote MariaDB Connections

MariaDB Tutorial / Troubleshoot Local and Remote MariaDB Connections

Diagnose MariaDB connections by layer, verify local TCP identity and actual table access, and review remote listener, host, firewall and certificate requirements without weakening checks by default.

MariaDB Tutorial / Group Results with GROUP BY and Filter Groups with HAVING

MariaDB Tutorial / Group Results with GROUP BY and Filter Groups with HAVING

Create category-level COUNT and SUM reports, distinguish WHERE from HAVING, and reconcile expected groups. Review output grain, ONLY_FULL_GROUP_BY, NULL keys and joined duplicates.

MariaDB Tutorial / Use Subqueries, EXISTS, and NOT EXISTS

MariaDB Tutorial / Use Subqueries, EXISTS, and NOT EXISTS

Choose scalar and existence subqueries according to the result needed. Compare paid-order customers and absent-order customers, inspect correlation keys and avoid NULL-related exclusion assumptions.

MariaDB Tutorial / Avoid Duplicate Totals When Joining Multiple Tables

MariaDB Tutorial / Avoid Duplicate Totals When Joining Multiple Tables

Trace row multiplication across customer, order, item and product joins. Reconcile correct goods and shipping totals, understand DISTINCT pitfalls and preaggregate details at the order grain.

MariaDB Tutorial / Combine Query Results with UNION and UNION ALL

MariaDB Tutorial / Combine Query Results with UNION and UNION ALL

Stack compatible customer-query results, compare UNION with UNION ALL and observe how source labels change duplicate detection. Check column meaning, result types and ordering scope.

MariaDB Tutorial / Import and Export CSV with Correct Paths, Types, and Encoding

MariaDB Tutorial / Import and Export CSV with Correct Paths, Types, and Encoding

Stage and validate a UTF-8 CSV before converting IDs and prices, then export through a server-side path. Review quoting, warnings, normalized duplicates, numeric bounds and field-level round-trip checks.

MariaDB Tutorial / Store Dates and Times with DATE, DATETIME, and TIMESTAMP

MariaDB Tutorial / Store Dates and Times with DATE, DATETIME, and TIMESTAMP

Choose DATE, DATETIME, or TIMESTAMP by the meaning of a time value. Run a controlled time-zone comparison, check version-dependent limits and invalid dates, and restore the original session settings.

MariaDB Tutorial / Choose String Types: CHAR, VARCHAR, and TEXT

MariaDB Tutorial / Choose String Types: CHAR, VARCHAR, and TEXT

Compare CHAR, VARCHAR, and TEXT with runnable Unicode and trailing-space examples. Check character counts versus byte counts, length errors, comparison rules, and the optional TEXT(M) syntax.

MariaDB Tutorial / Choose Conditional Values and Aggregates with CASE

MariaDB Tutorial / Choose Conditional Values and Aggregates with CASE

Use CASE for stock labels and conditional reports. Compare branch order, ELSE and NULL behavior, and understand why COUNT with ELSE 0 counts every non-NULL result.