🔑 What Is a Composite Key?
A composite key is a primary key made up of two or more columns that together uniquely identify a row in a table.
Use a composite key when no single column is unique on its own, but the combination is.
👉 Example: Composite Key in Action
Let’s say we’re building a table to track which students are enrolled in which courses.
Without Composite Key:
-- This table might allow duplicates
CREATE TABLE Enrollments (
student_id INT,
course_id INT
);
Nothing stops the same student from enrolling in the same course multiple times!
With Composite Key:
CREATE TABLE Enrollments (
student_id INT,
course_id INT,
PRIMARY KEY (student_id, course_id)
);
Now:
student_idalone is not uniquecourse_idalone is not unique- But together → each
(student_id, course_id)pair is unique
📌 Why Use Composite Keys?
| When to Use | Why |
|---|---|
| Tracking many-to-many relationships | Ensures unique pairs |
| Bridging/junction tables | e.g., students-courses, authors-books |
| No natural single-column key | But the combination is unique |
⚠️ Things to Keep in Mind
- Composite keys enforce uniqueness across multiple columns.
- They can also be used as foreign keys in other tables.
- Some developers prefer to add an auto-increment
idas the primary key instead—but that’s a design choice.
🔎 What Is a Candidate Key?
A candidate key is any column (or combination of columns) in a table that can uniquely identify each row.
- Every table can have multiple candidate keys
- One of them is chosen to be the primary key
- The rest are called alternate keys
🔑 Think of candidate keys as “potential primary keys”
👉 Example: Users Table
CREATE TABLE Users (
user_id INT,
username VARCHAR(80),
email VARCHAR(150),
phone_number VARCHAR(30)
);
Let’s have some hands own experience in SQL queries by creating a TEST DB. Check https://railsdrop.com/2025/04/25/rails-8-app-part-13-2-test-sql-queries/
Assume:
user_idis uniqueusernameis uniqueemailis unique
Candidate Keys:
user_idusernameemail
You can choose any one of them as the primary key, depending on your design needs.
-- Choosing user_id as the primary key
PRIMARY KEY (user_id)
The rest (username, email) are alternate keys.
📌 Characteristics of Candidate Keys
| Property | Description |
|---|---|
| Uniqueness | Must uniquely identify each row |
| Non-null | Cannot contain NULL values |
| Minimality | Must be the smallest set of columns that uniquely identifies a row (no extra columns) |
| No duplicates | No two rows have the same value(s) |
👥 Candidate Key vs Composite Key
| Concept | Explanation |
|---|---|
| Candidate Key | Any unique identifier (single or multiple columns) |
| Composite Key | A candidate key that uses multiple columns |
So: All composite keys are candidate keys, but not all candidate keys are composite.
💡 When Designing a Database
- Find all possible candidate keys
- Choose one as the primary key
- (Optional) Define other candidate keys as unique constraints
CREATE TABLE Users (
user_id INT PRIMARY KEY,
username VARCHAR UNIQUE,
email VARCHAR UNIQUE
);
Let’s walk through a real-world example using a schema we are already working on: a shopping app that sells clothing for women, men, kids, and infants.
We’ll look at how candidate keys apply to real tables like Users, Products, Orders, etc.
🛍️ Example Schema: Shopping App
1. Users Table
CREATE TABLE Users (
user_id SERIAL PRIMARY KEY,
email VARCHAR UNIQUE,
username VARCHAR UNIQUE,
phone_number VARCHAR
);
Candidate Keys:
user_id✅email✅username✅
We chose user_id as the primary key, but both email and username could also uniquely identify a user — so they’re candidate keys.
2. Products Table
CREATE TABLE Products (
product_id SERIAL PRIMARY KEY,
sku VARCHAR UNIQUE,
name VARCHAR,
category VARCHAR
);
Candidate Keys:
product_id✅sku✅ (Stock Keeping Unit – a unique identifier for each product)
skuis a candidate key. We useproduct_idas the primary key, but you could useskuif you wanted a natural key instead.
3. Orders Table
CREATE TABLE Orders (
order_id SERIAL PRIMARY KEY,
user_id INT REFERENCES Users(user_id),
order_number VARCHAR UNIQUE,
created_at TIMESTAMP
);
Candidate Keys:
order_id✅order_number✅
You might use order_number (e.g., "ORD-20250417-0012") for external reference and order_id internally. Both are unique identifiers = candidate keys.
4. OrderItems Table (Join Table)
This table links orders to the specific products and quantities purchased.
CREATE TABLE OrderItems (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES Orders(order_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
Candidate Key:
- Composite key:
(order_id, product_id)✅
Here, a combination of order_id and product_id uniquely identifies a row — i.e., what product was ordered in which order — making it a composite candidate key, and we’ve selected it as the primary key.
👀 Summary of Candidate Keys by Table
| Table | Candidate Keys | Primary Key Used |
|---|---|---|
| Users | user_id, email, username | user_id |
| Products | product_id, sku | product_id |
| Orders | order_id, order_number | order_id |
| OrderItems | (order_id, product_id) | (order_id, product_id) |
Let’s explore how to implement candidate keys in both SQL and Rails (Active Record). Since we are working on a shopping app in Rails 8, I’ll show how to enforce uniqueness and data integrity in both layers:
🔹 1. Candidate Keys in SQL (PostgreSQL Example)
Let’s take the Users table with multiple candidate keys (email, username, and user_id).
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL UNIQUE,
phone_number VARCHAR(20)
);
user_id: chosen as the primary keyemailandusername: candidate keys, enforced viaUNIQUEconstraints
💎 Composite Key Example (OrderItems)
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
This sets (order_id, product_id) as a composite candidate key and primary key.
🔸 2. Candidate Keys in Rails (ActiveRecord)
Now let’s do the same with Rails models + migrations + validations.
✅ users Migration (with candidate keys)
# db/migrate/xxxxxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email, null: false
t.string :username, null: false
t.string :phone_number
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :username, unique: true
end
end
✅ User Model
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :username, presence: true, uniqueness: true
end
✅ These are candidate keys —
usernamecould be primary keys, but we are usingidinstead.
✅ Composite Key with OrderItem (Join Table)
ActiveRecord doesn’t support composite primary keys natively, but you can enforce uniqueness via a multi-column index:
Migration:
class CreateOrderItems < ActiveRecord::Migration[8.0]
def change
create_table :order_items, id: false do |t|
t.references :order, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.integer :quantity, null: false
t.timestamps
end
add_index :order_items, [:order_id, :product_id], unique: true
end
end
Model:
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product
validates :quantity, presence: true
validates :order_id, uniqueness: { scope: :product_id }
end
🎯 This simulates a composite key behavior: each product can only appear once per order.
➕ Extra: Use composite_primary_keys Gem (Optional)
If you really need true composite primary keys, use:
gem 'composite_primary_keys'
But it’s best to avoid unless your use case demands it — most Rails apps use a surrogate key (id) for simplicity.
to be continued.. 🚀
One thought on “Setup 🛠 Rails 8 App – Part 13: Composite keys & Candidate keys in Rails DB”