Foreign key — a column that references the primary key of another table
Concept
Foreign key — a column (or set of columns) in one table that references the primary key of another table. Enforces referential integrity: you can't have an order for a non-existent user.
Referential integrity: The database guarantees that the referenced row always exists. If orders.user_id is a foreign key referencing users.id, the database prevents:
- Inserting an order with a
user_idthat doesn't exist inusers. - Deleting a user who has existing orders (unless a cascade rule applies).
Terms:
- Parent table (referenced):
users— has the primary key. - Child table (referencing):
orders— has the foreign key.
Cascade actions — what happens when the parent row changes:
ON DELETE CASCADE: Delete child rows automatically when parent is deleted.DELETE FROM users WHERE id = 1→ all orders for user 1 are also deleted.ON DELETE SET NULL: Set the FK column to NULL when parent is deleted. Good for optional relationships.ON DELETE RESTRICT(default): Prevent the parent row from being deleted if children exist. Throws an error.ON UPDATE CASCADE: Update the FK value in child rows when parent PK changes (rare with auto-increment IDs).
Performance: A foreign key constraint adds an index on the FK column automatically in most DBs (MySQL InnoDB). This is required to check existence efficiently.
To index or not: Some teams disable FK constraints for performance (high-volume inserts). The referential integrity is then enforced at the application level. Dangerous but sometimes necessary for bulk loads.
MySQL vs PostgreSQL: MySQL InnoDB supports FK constraints. MyISAM does NOT. PostgreSQL always supports them with deferred constraints.
Code Example
<?php
// CREATING FOREIGN KEYS in Laravel migrations
// Method 1: fluent shorthand
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id') // BIGINT UNSIGNED NOT NULL
->constrained() // references users(id) — table name auto-inferred
->onDelete('cascade'); // ON DELETE CASCADE
});
// Method 2: explicit
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('restrict') // prevent user deletion if orders exist
->onUpdate('cascade');
});
// Method 3: cascade soft deletes instead of DB cascade
// (common in Laravel — lets you use withTrashed() on soft-deleted parents)
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->nullOnDelete(); // SET NULL on delete
});
// Referential integrity in action
\DB::table('orders')->insert(['user_id' => 9999, 'total' => 100]); // FAILS!
// Illuminate\Database\QueryException: Cannot add or update a child row:
// a foreign key constraint fails (orders.user_id → users.id)
User::find(1)->delete(); // FAILS if orders exist with onDelete('restrict')
// Illuminate\Database\QueryException: Cannot delete or update a parent row
// DROPPING a foreign key
Schema::table('orders', function (Blueprint $table) {
$table->dropForeign(['user_id']); // drops foreign key constraint
$table->dropColumn('user_id');
});
// Disabling FK checks (for seeding or bulk loads)
\DB::statement('SET FOREIGN_KEY_CHECKS=0'); // MySQL
// ... bulk insert ...
\DB::statement('SET FOREIGN_KEY_CHECKS=1');
// Checking FK integrity after bulk load
\DB::statement("
SELECT o.id FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL
"); // finds orphan orders (broken referential integrity)