Skip to content
Innovialabs

Primary Key vs Foreign Key: Relationships in Databases

Basics4 min

In short

A primary key uniquely identifies each row in its own table, while a foreign key is a column that points to a primary key in another table; together they build relationships between tables and let the database block links to rows that do not exist.

The short answer

  • A primary key (PK) is a column, or set of columns, whose value uniquely identifies a row in its table. It is unique, never NULL, and a table has only one.
  • A foreign key (FK) is a column in one table that references the primary key (or another unique column) of another table. It says "this order belongs to that customer".

The database enforces both. You cannot insert two customers with the same ID, and you cannot create an order for a customer that does not exist. This is called referential integrity.

CREATE TABLE customers (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL
);

CREATE TABLE orders (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers (id),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

Natural and surrogate keys

  • A natural key comes from the real world: a passport number, an email, a tax ID.
  • A surrogate key is an artificial ID generated by the database or application.

Surrogate keys are the usual choice for primary keys because real-world values change: people change emails, documents get reissued. Keep natural identifiers as ordinary columns with a UNIQUE constraint.

Relationship types

One-to-many

The most common type. One customer has many orders; each order has one customer. The FK goes on the "many" side: orders.customer_id.

One-to-one

One user has one profile. Put an FK in the profile table and make it unique, often by making it the primary key at the same time:

CREATE TABLE profiles (
  user_id BIGINT PRIMARY KEY REFERENCES users (id),
  bio     TEXT
);

One-to-one is useful for splitting rarely used or sensitive columns into a separate table. If the data is always read together, a single table is usually simpler.

Many-to-many

A student attends many courses, and a course has many students. Neither table can hold the FK alone, so you add a junction table (also called a link or bridge table):

CREATE TABLE enrollments (
  student_id  BIGINT REFERENCES students (id),
  course_id   BIGINT REFERENCES courses (id),
  enrolled_at DATE NOT NULL,
  PRIMARY KEY (student_id, course_id)
);

The composite primary key prevents the same student from enrolling in the same course twice. The junction table can also store facts about the link itself, such as the enrollment date.

What happens on delete

When you delete a customer who still has orders, the FK decides what to do:

OptionBehavior
RESTRICT / NO ACTIONDeletion fails while child rows exist (default)
CASCADEChild rows are deleted too
SET NULLThe FK in child rows is set to NULL
SET DEFAULTThe FK is set to its default value
customer_id BIGINT REFERENCES customers (id) ON DELETE CASCADE

Use CASCADE for data that has no meaning without its parent, such as order items without an order. Avoid it for valuable records: cascading from customers to orders to payments can quietly wipe out financial history. For such data, prefer RESTRICT or a soft delete (an is_deleted or deleted_at column).

Serial IDs vs UUIDs

Serial / identityUUID
Size4 or 8 bytes16 bytes
Readability1042 is easy to say and debugLong string
Generated whereDatabaseDatabase or application
GuessableYes, IDs are sequentialPractically no
Merging data, offline creationRisk of conflictsUnique across systems
Index friendlinessSequential, good localityRandom UUIDv4 scatters inserts

Practical guidance:

  • For most internal tables, identity/serial keys are simple and efficient.
  • Choose UUIDs when IDs are created on clients or in several services, when you merge data from different sources, or when you do not want to expose record counts.
  • If you use UUIDs as a primary key, consider time-ordered variants such as UUIDv7, which keep inserts closer together in the index.
  • Never rely on unguessable IDs as security; always check access rights.

Common mistakes

  • Forgetting to index FK columns. PostgreSQL does not create an index on the referencing column automatically, while MySQL InnoDB does. Joins and cascading deletes on large tables then become slow.
  • Mismatched types, such as INT in one table and BIGINT in the other.
  • Skipping FKs "for speed" and ending up with orphan rows that point nowhere.
  • Using a mutable value, like an email, as the primary key.

FAQ

Yes. An order can reference a customer, a delivery address and a manager at the same time. A table can also reference itself, for example an employees.manager_id pointing to another employee.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.