#Keys: candidate, primary, composite, surrogate and natural
AddedA key is a set of columns whose values identify exactly one row. The vocabulary sounds academic, but every term answers a practical question:
- A super key is any set of columns that is unique —
(id),(id, name),(email, city)… Adding columns to a unique set keeps it unique, so most super keys are bloated. - A candidate key is a minimal super key: unique, and you can't drop any column without losing uniqueness. In
customers, bothidandemailare candidate keys. - The primary key is the one candidate key you pick as the row's official identity. Foreign keys point at it, and in InnoDB the table is physically stored in its order. The other candidate keys become
UNIQUEconstraints (sometimes called alternate keys). - A composite key has more than one column, like
order_items (order_id, product_id). - A natural key already means something in the real world (an email, an ISBN, a country code); a surrogate key is invented by the database purely to identify rows (
AUTO_INCREMENT, a UUID).
The analogy: a person has a passport number, a national ID number and an email address — three candidate keys. A company's HR system picks one as the official identity (the primary key) and usually invents its own employee number (a surrogate) so nothing breaks when a passport is renewed.
SELECT COUNT(*) AS total_rows,
COUNT(DISTINCT id) AS distinct_ids,
COUNT(DISTINCT email) AS distinct_emails,
COUNT(DISTINCT name) AS distinct_names,
COUNT(DISTINCT city) AS distinct_cities,
COUNT(city) AS non_null_cities
FROM customers;
SELECT table_name, index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS key_columns, non_unique
FROM information_schema.statistics
WHERE table_schema = 'ch07' AND table_name IN ('customers', 'order_items')
GROUP BY table_name, index_name, non_unique ORDER BY table_name, index_name;
INSERT INTO order_items VALUES (1, 1, 5, 899.00);+------------+--------------+-----------------+----------------+-----------------+-----------------+
| total_rows | distinct_ids | distinct_emails | distinct_names | distinct_cities | non_null_cities |
+------------+--------------+-----------------+----------------+-----------------+-----------------+
| 6 | 6 | 6 | 6 | 4 | 5 |
+------------+--------------+-----------------+----------------+-----------------+-----------------+
+-------------+------------+---------------------+------------+
| TABLE_NAME | INDEX_NAME | key_columns | NON_UNIQUE |
+-------------+------------+---------------------+------------+
| customers | email | email | 0 |
| customers | PRIMARY | id | 0 |
| order_items | PRIMARY | order_id,product_id | 0 |
| order_items | product_id | product_id | 1 |
+-------------+------------+---------------------+------------+
ERROR 1062 (23000): Duplicate entry '1-1' for key 'order_items.PRIMARY'- Six customers, six distinct
ids and six distinctemails: both are unique.cityhas only 4 distinct values and oneNULL, so it can't identify a row. nameis also unique in today's six rows — but that's an accident of the data. A candidate key is a rule about all possible data ("two customers can never share an email"), not an observation about current rows. Two people called Maya will sign up eventually. Data can disprove a key; it can never prove one.- The schema declares the rules:
PRIMARY (id)andUNIQUE (email)oncustomers— two candidate keys, one chosen as primary. Onorder_itemsthe primary key is composite:(order_id, product_id), "a product appears at most once per order". - Inserting
(1, 1)again fails withERROR 1062 … '1-1' for key 'order_items.PRIMARY': order 1 already has product 1. Neither column is unique on its own (order 1 has two products, product 1 is in two orders); only the pair is. - The extra non-unique index
product_idwas created automatically for the foreign key toproducts, because the primary key starts withorder_idand can't serve lookups by product alone.
Suppose email were the primary key and orders referenced it. The schema copies the natural key into every child row:
CREATE TABLE customers_nat (
email VARCHAR(100) PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE orders_nat (
id INT PRIMARY KEY,
customer_email VARCHAR(100) NOT NULL,
FOREIGN KEY (customer_email) REFERENCES customers_nat(email) ON UPDATE CASCADE
);
INSERT INTO customers_nat SELECT email, name FROM customers;
INSERT INTO orders_nat SELECT o.id, c.email FROM orders o JOIN customers c ON c.id = o.customer_id;
UPDATE customers_nat SET email = 'rohit.varma@example.com' WHERE email = 'rohit@example.com';
SELECT id, customer_email FROM orders_nat ORDER BY id;
UPDATE customers_nat SET email = 'maya@example.com' WHERE email = 'liam@example.com';+----+-------------------------+
| id | customer_email |
+----+-------------------------+
| 1 | rohit.varma@example.com |
| 2 | rohit.varma@example.com |
| 3 | maya@example.com |
| 4 | liam@example.com |
| 5 | liam@example.com |
| 6 | sofia@example.com |
| 7 | noah@example.com |
| 8 | maya@example.com |
+----+-------------------------+
ERROR 1062 (23000): Duplicate entry 'maya@example.com' for key 'customers_nat.PRIMARY'- Rohit changes his email. Because the email is the key, every order that references him must change too;
ON UPDATE CASCADErewrites orders1and2. With a million orders, one email change rewrites a million rows (and their index entries), inside one transaction. - Changing a key is also a uniqueness question: Liam can't take
maya@example.com, so the update fails withERROR 1062— correct, but it shows the key carries business meaning that can collide. - With the surrogate
customers.id, an email change touches one row; orders storecustomer_id = 1, which never changes. - Surrogate keys are also small (4–8 bytes, versus up to 400 bytes for a
VARCHAR(100)inutf8mb4), and every secondary index and every child table carries a copy of the primary key. - Natural keys still make sense when they're truly stable and short: ISO country codes (
CA,IN), currency codes (USD), and junction tables, whose natural composite key (order_id, product_id) is made of surrogates anyway.
The client's "rows affected" counts only the rows the statement changed directly:
UPDATE customers_nat SET email = 'rohit@example.com' WHERE email = 'rohit.varma@example.com';Query OK, 1 row affected (0.002 sec)
Rows matched: 1 Changed: 1 Warnings: 0- The update changed one row in
customers_natand, through the cascade, two rows inorders_nat— but the reported count is1. - If an application uses the affected-rows count to verify a migration, cascades make the number misleading. Check the child tables separately.