Learn SQL: Day 3 – JOINs (One of the Most Important SQL Topic)

If I had to choose one SQL topic that appears most frequently in Senior Developer interviews, it would be:

JOINs

Most Rails developers know:

User.joins(:orders)

But many cannot explain:

  • What SQL Rails generates
  • How PostgreSQL executes it
  • Why duplicates occur
  • When to use joins
  • When to use includes
  • When JOINs become slow

A senior engineer should be comfortable with all of these.

Today’s Goals

By the end of Day 3, you’ll understand:

  • INNER JOIN
  • LEFT JOIN
  • RIGHT JOIN
  • FULL OUTER JOIN
  • CROSS JOIN
  • Self JOIN
  • How Rails translates associations into JOINs
  • N+1 query problem
  • joins vs includes
  • Interview questions

Step 1: Create Fresh Tables

Let’s create a simple system.

Users

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100)
);

Orders

CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount NUMERIC(10,2),
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id)
REFERENCES users(id)
);

Insert Sample Data

Users:

INSERT INTO users(name)
VALUES
('John'),
('Mary'),
('Bob'),
('Alice');

Orders:

INSERT INTO orders(user_id, amount)
VALUES
(1,100),
(1,200),
(2,300),
(2,400),
(2,500);

Current data:

users

idname
1John
2Mary
3Bob
4Alice

orders

iduser_idamount
11100
21200
32300
42400
52500

Notice:

Bob has no orders
Alice has no orders

This becomes important.

What is a JOIN?

A JOIN combines rows from multiple tables.

Think:

users
+
orders
=
business information

The database uses a common column:

users.id
=
orders.user_id

1. INNER JOIN

Most common JOIN.

Returns only matching rows.

Query

SELECT
users.id,
users.name,
orders.amount
FROM users
INNER JOIN orders
ON users.id = orders.user_id;

Result:

nameamount
John100
John200
Mary300
Mary400
Mary500

Notice:

Bob disappeared
Alice disappeared

Why?

Because they have no matching order.

Visual

users orders
John <-> 100
John <-> 200
Mary <-> 300
Mary <-> 400
Mary <-> 500
Bob X
Alice X

Only matches survive.

Rails Equivalent

User.joins(:orders)

Generated SQL:

SELECT users.*
FROM users
INNER JOIN orders
ON orders.user_id = users.id;

Interview Question

What type of JOIN does Rails joins use?

Answer:

INNER JOIN

Many candidates miss this.

2. LEFT JOIN

Returns:

All rows from LEFT table
+
matching rows from RIGHT table

Query

SELECT
users.name,
orders.amount
FROM users
LEFT JOIN orders
ON users.id = orders.user_id;

Result:

nameamount
John100
John200
Mary300
Mary400
Mary500
BobNULL
AliceNULL

Notice:

Bob exists
Alice exists

Even without orders.

Visual

LEFT TABLE = users
Keep everything
John -> order
Mary -> order
Bob -> NULL
Alice -> NULL

Rails Equivalent

User.left_joins(:orders)

Generated SQL:

LEFT OUTER JOIN

Practical Example

Find users without orders.

SELECT users.*
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
WHERE orders.id IS NULL;

Result:

Bob
Alice

Rails:

User.left_joins(:orders)
.where(orders: { id: nil })

Common Interview Question

Find customers who never placed an order.

Expected answer:

LEFT JOIN
+
IS NULL

3. RIGHT JOIN

Opposite of LEFT JOIN.

Keep all rows from right table.

SELECT *
FROM users
RIGHT JOIN orders
ON users.id = orders.user_id;

In real-world Rails projects:

Rarely used

Most engineers rewrite it as LEFT JOIN.

4. FULL OUTER JOIN

Keep everything.

SELECT *
FROM users
FULL OUTER JOIN orders
ON users.id = orders.user_id;

Returns:

All users
+
All orders

matched where possible.

Used occasionally for:

  • reporting
  • analytics
  • reconciliation

Rare in Rails applications.

5. CROSS JOIN

Creates every possible combination.

Example:

CREATE TABLE colors (
color VARCHAR(20)
);
INSERT INTO colors
VALUES ('Red'),('Blue');

Sizes:

CREATE TABLE sizes (
size VARCHAR(20)
);
INSERT INTO sizes
VALUES ('S'),('M');

Query:

SELECT *
FROM colors
CROSS JOIN sizes;

Result:

Red S
Red M
Blue S
Blue M

Every row paired with every row.

Formula:

RowsA × RowsB

Interview Question:

10 rows × 100 rows

How many rows?

Answer:

1000

6. Self JOIN

A table joins itself.

Very common interview topic.

Create employees:

CREATE TABLE employees (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
manager_id BIGINT
);

Insert:

INSERT INTO employees
(name, manager_id)
VALUES
('CEO', NULL),
('Manager1',1),
('Manager2',1),
('Developer1',2),
('Developer2',2);

Query:

SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;

Result:

employeemanager
CEONULL
Manager1CEO
Developer1Manager1

Rails

class Employee < ApplicationRecord
belongs_to :manager,
class_name: "Employee",
optional: true
has_many :subordinates,
class_name: "Employee",
foreign_key: :manager_id
end

Why Duplicates Occur

Look at:

SELECT *
FROM users
INNER JOIN orders
ON users.id = orders.user_id;

Mary has:

3 orders

Therefore:

Mary appears 3 times

JOINs multiply rows.

This is one of the most misunderstood SQL concepts.

DISTINCT After JOIN

Sometimes we want unique users.

SELECT DISTINCT users.*
FROM users
JOIN orders
ON users.id = orders.user_id;

Rails

User.joins(:orders).distinct

The N+1 Query Problem

Every Rails interview asks this.

Suppose:

users = User.all
users.each do |user|
puts user.orders.count
end

Queries:

SELECT * FROM users;

Then:

SELECT * FROM orders WHERE user_id=1;
SELECT * FROM orders WHERE user_id=2;
SELECT * FROM orders WHERE user_id=3;
...

100 users:

101 queries

Called:

N+1 problem

Fix Using includes

User.includes(:orders)

Rails loads:

SELECT * FROM users;

and

SELECT * FROM orders
WHERE user_id IN (...);

Only 2 queries.

joins vs includes

This is a favorite interview question.

joins

Used for filtering.

User.joins(:orders)

SQL:

INNER JOIN

Purpose:

Filter data

includes

Used for eager loading.

User.includes(:orders)

Purpose:

Avoid N+1

Example

Find users with orders.

User.joins(:orders)

Display users and orders.

User.includes(:orders)

Interview Question

Which is better?

joins

or

includes

Answer:

Depends on the problem.

Different purposes.

Real Interview Queries

Find users with orders

SELECT DISTINCT users.*
FROM users
JOIN orders
ON users.id = orders.user_id;

Rails:

User.joins(:orders).distinct

Find users without orders

SELECT users.*
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
WHERE orders.id IS NULL;

Rails:

User.left_joins(:orders)
.where(orders: { id: nil })

Find total orders per user

SELECT
users.name,
COUNT(orders.id)
FROM users
LEFT JOIN orders
ON users.id = orders.user_id
GROUP BY users.name;

We’ll study GROUP BY in Day 4.

Senior-Level Insights

1. Most Rails JOINs are INNER JOINs

joins

means:

INNER JOIN

2. LEFT JOIN + IS NULL

is the standard pattern for:

Find missing records

Examples:

  • users without orders
  • customers without subscriptions
  • products without inventory

3. JOINs Multiply Rows

One user:

3 orders

becomes:

3 result rows

Always remember this.

4. Understand the Generated SQL

When writing:

User.joins(:orders)

you should mentally see:

INNER JOIN orders
ON orders.user_id = users.id

immediately.


Practical Exercises

Exercise 1

Find all users with orders.

Exercise 2

Find all users without orders.

Exercise 3

Add 3 more orders for John.

Run:

INNER JOIN

Observe duplicates.

Exercise 4

Use:

DISTINCT

to remove duplicates.

Exercise 5

Create:

categories
products

and practice:

INNER JOIN
LEFT JOIN

Homework

Create the following schema:

authors
books
publishers

Relationships:

Author has many Books
Publisher has many Books
Book belongs to Author
Book belongs to Publisher

Insert sample data.

Write queries:

  1. Books with author names
  2. Books with publisher names
  3. Authors without books
  4. Publishers without books
  5. Unique authors who have books

Write both:

  • SQL
  • ActiveRecord

Question:

I cannot see any difference in output between left join and full outer join with our example. can u create a new data to see me correct difference between them?

This is actually something that confuses many developers in interviews.

The reason you don’t see any difference is because our current data doesn’t have any “orphan” records in the orders table.

Let’s understand why.

Our Current Data

users

idname
1John
2Mary
3Bob
4Alice

orders

iduser_idamount
11100
21200
32300
42400
52500

Every order belongs to an existing user.

So there are:

  • Users without orders (Bob, Alice)
  • No orders without users

That’s why LEFT JOIN and FULL OUTER JOIN appear almost identical.

Let’s Create a Better Example

To see the difference, we need an order that doesn’t match any user.

However…

Our foreign key prevents that.

FOREIGN KEY (user_id)
REFERENCES users(id)

This is a good thing because it maintains data integrity.

So for learning purposes, we’ll create another table without a foreign key.

Step 1

DROP TABLE IF EXISTS orders_demo;
CREATE TABLE orders_demo (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
amount NUMERIC(10,2)
);

Notice:

❌ No foreign key.

Step 2

Insert data

INSERT INTO orders_demo(user_id, amount)
VALUES
(1,100),
(1,200),
(2,300),
(999,400);

Now we have:

users

idname
1John
2Mary
3Bob
4Alice

orders_demo

iduser_idamount
11100
21200
32300
4999400

Notice:

user_id = 999

There is no matching user.

This is our orphan order.

INNER JOIN

SELECT
u.id,
u.name,
o.amount
FROM users u
INNER JOIN orders_demo o
ON u.id = o.user_id;

Result

nameamount
John100
John200
Mary300

The orphan order disappears.

LEFT JOIN

SELECT
u.id,
u.name,
o.amount
FROM users u
LEFT JOIN orders_demo o
ON u.id = o.user_id;

Result

nameamount
John100
John200
Mary300
BobNULL
AliceNULL

Question:

Where is the orphan order?

It is gone!

Why?

Because LEFT JOIN keeps every row from the left table (users). Since there is no user with id = 999, there is nothing on the left to preserve.

FULL OUTER JOIN

SELECT
u.id,
u.name,
o.user_id,
o.amount
FROM users u
FULL OUTER JOIN orders_demo o
ON u.id = o.user_id;

Result

user idnameorder user_idamount
1John1100
1John1200
2Mary2300
3BobNULLNULL
4AliceNULLNULL
NULLNULL999400

Now you finally see the difference!

The last row exists only because of FULL OUTER JOIN.

When to use FULL OUTER JOIN?

Check the page: https://railsdrop.com/learn-sql-day-3-when-to-use-full-outer-join/


Quick Quiz

Given these tables:

A

id
1
2
3

B

id
2
3
4

Without running SQL, what rows do you expect from:

  1. INNER JOIN
  2. LEFT JOIN (A LEFT JOIN B)
  3. RIGHT JOIN (A RIGHT JOIN B)
  4. FULL OUTER JOIN

Try answering these on paper first. If you can do that confidently, you’ve truly understood how the different joins work.


Day 4 Preview

Tomorrow we’ll cover one of the highest-frequency SQL interview topics:

GROUP BY & Aggregations

Including:

  • COUNT
  • SUM
  • AVG
  • MIN
  • MAX
  • GROUP BY
  • HAVING
  • Aggregate queries in Rails
  • Real interview problems such as:
    • Top customers
    • Revenue calculations
    • Most purchased products
    • Reporting queries

This is where SQL starts becoming analytical rather than just relational.

Happy Learning! 

Learn SQL: Day 2 – SELECT Queries (The Foundation of SQL)

Today we start writing queries.

Today’s Goals

By the end of Day 2 you should understand:

  • SELECT
  • WHERE
  • ORDER BY
  • LIMIT
  • OFFSET
  • DISTINCT
  • IN
  • BETWEEN
  • LIKE
  • ILIKE
  • NULL handling
  • ActiveRecord equivalents
  • Common interview questions
  • Common mistakes

Step 1: Create Our Practice Database

Connect:

psql sql_day1

Let’s create a new table.

DROP TABLE IF EXISTS users;
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255),
age INTEGER,
city VARCHAR(100),
salary NUMERIC(10,2),
active BOOLEAN,
created_at TIMESTAMP DEFAULT NOW()
);

Insert Sample Data

INSERT INTO users
(name, email, age, city, salary, active)
VALUES
('John', 'john@test.com', 30, 'New York', 70000, true),
('Mary', 'mary@test.com', 25, 'Chicago', 60000, true),
('Bob', 'bob@test.com', 35, 'Chicago', 90000, false),
('Alice', 'alice@test.com', 28, 'Boston', 75000, true),
('Tom', 'tom@test.com', 40, 'New York', 120000, false),
('Sara', 'sara@test.com', 32, 'Boston', 85000, true),
('Mike', 'mike@test.com', NULL, 'Chicago', NULL, true);

View data:

SELECT * FROM users;

1. SELECT

The most basic query.

SELECT * FROM users;

Meaning:

Give me all columns from users

Output:

id | name | email | age | city | salary

Select Specific Columns

Instead of everything:

SELECT name, email FROM users;

Output:

name | email
--------+---------------
John | john@test.com
Mary | mary@test.com

Rails Equivalent

User.select(:name, :email)

Senior Insight

Avoid:

SELECT *

in production systems unless needed.

Why?

Because fetching unnecessary columns:

  • uses more memory
  • transfers more data
  • slows queries

Good:

SELECT id, name FROM users;

2. WHERE Clause

Filters rows.

Example

Only active users.

SELECT * FROM users
WHERE active = true;

Rails:

User.where(active: true)

Age Greater Than 30

SELECT * FROM users
WHERE age > 30;

Rails:

User.where("age > ?", 30)

Multiple Conditions

SELECT * FROM users
WHERE city = 'Chicago'
AND active = true;

Rails:

User.where(city: "Chicago", active: true)

OR

SELECT * FROM users
WHERE city = 'Chicago'
OR city = 'Boston';

Rails:

User.where(city: ["Chicago", "Boston"])

Interview Question

Which runs first?

WHERE A OR B AND C

Answer:

AND

before

OR

Use parentheses.

WHERE (A OR B)
AND C

3. ORDER BY

Sort results.

Ascending

SELECT * FROM users
ORDER BY age ASC;

Smallest age first.

Descending

SELECT * FROM users
ORDER BY salary DESC;

Highest salary first.

Rails:

User.order(salary: :desc)

Multiple Columns

SELECT * FROM users
ORDER BY city ASC, salary DESC;

Meaning:

Sort by city first
Inside each city
sort by salary

Common Interview Question

What happens if you omit ASC/DESC?

ORDER BY age

Default:

ASC

4. LIMIT

Return only N rows.

SELECT * FROM users
LIMIT 3;

Rails:

User.limit(3)

Why LIMIT Matters

Imagine:

10 million rows

Fetching all:

slow
memory-heavy
unnecessary

LIMIT reduces work.

5. OFFSET

Skip rows.

SELECT * FROM users
LIMIT 3
OFFSET 3;

Meaning:

Skip first 3
Return next 3

Rails

User.limit(3).offset(3)

Pagination Example

Page 1

LIMIT 10 OFFSET 0

Page 2

LIMIT 10 OFFSET 10

Page 3

LIMIT 10 OFFSET 20

Senior Insight

Large OFFSET values become expensive.

Example:

OFFSET 500000

PostgreSQL still scans through those rows.

Later we’ll learn:

Keyset Pagination

which is much faster.

6. DISTINCT

Remove duplicates.

Example:

SELECT city FROM users;

Result:

Chicago
Chicago
Chicago
Boston
Boston
New York

Distinct:

SELECT DISTINCT city FROM users;

Result:

Chicago
Boston
New York

Rails

User.select(:city).distinct

Multiple Columns

SELECT DISTINCT city, active FROM users;

Distinct applies to the combination.

7. IN

Cleaner alternative to multiple OR conditions.

Instead of:

WHERE city='Boston'
OR city='Chicago'
OR city='New York'

Use:

WHERE city IN
('Boston','Chicago','New York');

Rails:

User.where(city: ["Boston", "Chicago", "New York"])

8. BETWEEN

Range filtering.

Age between 25 and 35.

SELECT * FROM users
WHERE age BETWEEN 25 AND 35;

Equivalent:

age >= 25
AND
age <= 35

Rails

User.where(age: 25..35)

Salary Range

SELECT * FROM users
WHERE salary BETWEEN 60000 AND 90000;

Interview Question

Is BETWEEN inclusive?

Answer:

YES

Both boundaries included.

9. LIKE

Pattern matching.

Find names beginning with M.

SELECT * FROM users
WHERE name LIKE 'M%';

Result:

Mary
Mike

Ends With

WHERE email LIKE '%test.com'

Contains

WHERE name LIKE '%ar%'

Matches:

Mary
Sara

Wildcards

SymbolMeaning
%Any number of chars
_Exactly one char

Example

WHERE name LIKE '_o%'

Matches:

Bob
Tom

10. ILIKE

PostgreSQL-specific.

Case-insensitive LIKE.

SELECT * FROM users
WHERE name ILIKE 'john';

Matches:

John
JOHN
john
JoHn

Rails

User.where("name ILIKE ?", "john")

Senior Interview Insight

In PostgreSQL:

LIKE

is case-sensitive.

ILIKE

is case-insensitive.

Many developers don’t know this.

11. NULL Handling

This is a favorite interview topic.

Let’s inspect:

SELECT * FROM users;

Mike has:

age = NULL
salary = NULL

Wrong

WHERE age = NULL

Returns:

Nothing

Correct

WHERE age IS NULL

Find users with no salary:

SELECT * FROM users
WHERE salary IS NULL;

Rails

User.where(age: nil)

Generates:

IS NULL

NOT NULL

SELECT * FROM users
WHERE salary IS NOT NULL;

Why NULL Is Special

SQL uses:

TRUE
FALSE
UNKNOWN

not just:

TRUE
FALSE

This is called:

Three-Valued Logic

Interviewers love asking this.

Practical Exercises

Exercise 1

Find all active users.

Exercise 2

Find users older than 30.

Exercise 3

Find users from Boston.

Exercise 4

Find top 3 highest-paid users.

Exercise 5

Find unique cities.

Exercise 6

Find users aged between 25 and 35.

Exercise 7

Find names starting with S.

Exercise 8

Find users whose salary is NULL.

Combining Everything

Example:

SELECT name, city, salary
FROM users
WHERE active = true
AND city IN ('Chicago', 'Boston')
AND salary IS NOT NULL
ORDER BY salary DESC
LIMIT 3;

Can you explain what this query does before running it?

That’s exactly the kind of reasoning expected in senior interviews.

ActiveRecord Translation Challenge

Convert this SQL:

SELECT *
FROM users
WHERE city = 'Chicago'
AND active = true
ORDER BY salary DESC
LIMIT 2;

into ActiveRecord.

Common Mistakes

Mistake 1

WHERE age = NULL

Wrong.

Use:

WHERE age IS NULL

Mistake 2

Using:

SELECT *

everywhere.

Mistake 3

Forgetting ORDER BY when using LIMIT.

LIMIT 5

without ordering can return arbitrary rows.

Mistake 4

Using huge OFFSET values.

Senior-Level Knowledge

Understand that SQL logically executes in this order:

FROM
WHERE
SELECT
DISTINCT
ORDER BY
LIMIT

Even though we write:

SELECT ...
FROM ...
WHERE ...

PostgreSQL conceptually processes the clauses in the above order.

This understanding becomes extremely important when we move to:

  • JOINs
  • GROUP BY
  • HAVING
  • Query Optimization
  • EXPLAIN ANALYZE

Homework

Create a new table:

CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(100),
price NUMERIC(10,2),
stock_quantity INTEGER
);

Insert at least 10 records.

Practice:

  1. SELECT specific columns
  2. WHERE with multiple conditions
  3. ORDER BY price DESC
  4. LIMIT 5
  5. DISTINCT categories
  6. BETWEEN on price
  7. LIKE searches
  8. Products with stock_quantity IS NULL

Day 3 Preview

Next we’ll cover one of the most important interview topics:

JOINs

Including:

  • INNER JOIN
  • LEFT JOIN
  • RIGHT JOIN
  • FULL JOIN
  • CROSS JOIN
  • Self Join
  • ActiveRecord joins
  • includes vs joins vs preload vs eager_load
  • Real Rails interview questions

Day 3 is where SQL starts becoming truly powerful.

Happy Learning! 🚀

Learn SQL: Day 1 – Relational Database Fundamentals

We’re going to learn these topics at three levels simultaneously:

  1. Database Level (PostgreSQL)
  2. SQL Level
  3. Rails / ActiveRecord Level

For every topic, ask yourself:

“How does PostgreSQL store this?”

“How do I query this with SQL?”

“How does Rails represent this?”

This will help you to prepare for interviews. We use PostgreSQL here.

Today’s Goal

By the end of Day 1, you should fully understand:

  • Database
  • Table
  • Row
  • Column
  • Primary Key
  • Foreign Key
  • Constraints
  • One-to-One
  • One-to-Many
  • Many-to-Many
  • Rails associations behind them

Part 1: What is a Database?

Imagine you are building an e-commerce application.

You need to store:

  • Users
  • Products
  • Orders
  • Payments

A database is simply a structured place to store and retrieve that information.

In PostgreSQL:

CREATE DATABASE shop_app;

Connect to it:

psql postgres

Inside psql:

CREATE DATABASE shop_app;

Connect:

\c shop_app

Verify:

SELECT current_database();

Output:

 shop_app



Part 2: What is a Table?

A table is similar to an Excel sheet.

Example:

Users table

idnameemail
1Johnjohn@test.com
2Marymary@test.com

Create it:

CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
);

Verify:

\d users

Interview Question:

Why not store everything in a single giant table?

Answer:

Because:

  • duplication increases
  • maintenance becomes difficult
  • relationships become unclear
  • updates become expensive

This concept is called normalization (we’ll study later).


Part 3: Rows

A row represents one record.

Insert data:

INSERT INTO users (name, email)
VALUES
('John', 'john@test.com'),
('Mary', 'mary@test.com');

View:

SELECT * FROM users;

Output:

 id | name | email
----+------+----------------
 1  | John | john@test.com
 2  | Mary | mary@test.com


Each row = one user.


Part 4: Columns

Columns describe attributes.

In users table:

id
name
email

View columns:

\d users

Senior Insight:

A database table models an entity.

Examples:

EntityTable
Userusers
Productproducts
Orderorders

Columns represent attributes of that entity.


Part 5: Primary Keys

Every row needs a unique identifier.

Example:

id BIGSERIAL PRIMARY KEY

Meaning:

1
2
3
4
...

No duplicates.

No NULLs.

Try:

INSERT INTO users (id, name)
VALUES (1, 'Bob');

You should get:

duplicate key value violates unique constraint

Why Primary Keys Exist

Without a primary key:

John
John
John

Which John?

Nobody knows.

Primary key solves identity.

Rails Equivalent

Migration:

create_table :users do |t|
t.string :name
t.string :email
end

Rails automatically adds:

id

as the primary key.


Part 6: Constraints

Constraint = database rule.

Interviewers love this topic.

NOT NULL

Create:

CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

Try:

INSERT INTO products(name)
VALUES(NULL);

Fails.

UNIQUE

CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE
);

Duplicate email:

INSERT INTO customers(email)
VALUES('test@test.com');
INSERT INTO customers(email)
VALUES('test@test.com');

Fails.

CHECK Constraint

Age must be positive.

CREATE TABLE employees (
id BIGSERIAL PRIMARY KEY,
age INTEGER CHECK(age > 0)
);

Fails:

INSERT INTO employees(age)
VALUES(-5);

Why Constraints Matter

Junior developer:

validates :email, uniqueness: true

Senior developer:

validates :email, uniqueness: true
+
UNIQUE(email)

Because application validations can be bypassed.

Database constraints cannot.


Part 7: Foreign Keys

Now let’s model:

User has many orders.

Create users:

CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100)
);

Create orders:

CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
total NUMERIC(10,2)
);

Foreign key:

ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id)
REFERENCES users(id);

Insert user:

INSERT INTO users(name)
VALUES('John');

Insert order:

INSERT INTO orders(user_id,total)
VALUES(1,100);

Works.

Try:

INSERT INTO orders(user_id,total)
VALUES(999,100);

Fails.

Because user doesn’t exist.

Why Foreign Keys Exist

Without them:

Order belongs to user 999

But user 999 doesn’t exist.

Database becomes corrupted.

Rails Equivalent

class User < ApplicationRecord
has_many :orders
end
class Order < ApplicationRecord
belongs_to :user
end

Migration:

t.references :user,
null: false,
foreign_key: true

Rails creates:

user_id
FOREIGN KEY

behind the scenes.


Part 8: One-to-Many Relationship

Most common relationship.

Example:

User -> Orders

One user:

John

Many orders:

Order 1
Order 2
Order 3

Diagram:

users
-----
id
orders
------
id
user_id

Rails:

User has_many :orders
Order belongs_to :user

Practical Exercise

Insert:

INSERT INTO users(name)
VALUES('Mary');

Orders:

INSERT INTO orders(user_id,total)
VALUES
(2,50),
(2,75),
(2,120);

Query:

SELECT *
FROM orders
WHERE user_id = 2;

Part 9: One-to-One Relationship

Less common.

Example:

User
Profile

Each user has exactly one profile.

Create profile table:

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

Notice:

UNIQUE(user_id)

This forces:

One user
One profile

Rails:

class User < ApplicationRecord
has_one :profile
end
class Profile < ApplicationRecord
belongs_to :user
end

Interview Question:

How does a database enforce one-to-one?

Answer:

FOREIGN KEY
+
UNIQUE

on the foreign key column.


Part 10: Many-to-Many Relationship

Classic interview topic.

Example:

Students
Courses

Student can enroll in many courses.

Course can have many students.

Create students:

CREATE TABLE students (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100)
);

Create courses:

CREATE TABLE courses (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(100)
);

Need a join table:

CREATE TABLE enrollments (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY(student_id, course_id),
FOREIGN KEY(student_id)
REFERENCES students(id),
FOREIGN KEY(course_id)
REFERENCES courses(id)
);

Diagram:

students
|
|
enrollments
|
|
courses

Rails

class Student < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments
end
class Course < ApplicationRecord
has_many :enrollments
has_many :students, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
end

Senior-Level Insight

Most Rails developers stop at:

has_many
belongs_to

Strong backend engineers understand:

Association
Foreign Key
Constraint
Index
Storage

That understanding helps you:

  • debug production issues
  • optimize queries
  • design schemas
  • answer interview questions confidently

Interview Questions

Try answering without looking.

Q1

Difference between:

PRIMARY KEY

and

UNIQUE

Q2

Can a table have multiple UNIQUE constraints?

Q3

Can a table have multiple PRIMARY KEYS?

Q4

How is a one-to-one relationship implemented in PostgreSQL?

Q5

Why should foreign keys exist even when Rails validations exist?

Q6

What problem does a join table solve?

Practical Lab (Run Everything)

Create a fresh database:

CREATE DATABASE interview_sql_day1;

Connect:

\c interview_sql_day1

Create:

users
profiles
orders
students
courses
enrollments

Insert sample data.

Then practice:

SELECT * FROM users;
SELECT * FROM orders;
SELECT * FROM profiles;
SELECT * FROM enrollments;

Try intentionally violating:

  • PRIMARY KEY
  • UNIQUE
  • NOT NULL
  • FOREIGN KEY

and observe PostgreSQL’s error messages.

A senior engineer learns a lot from database errors.


Homework

Exercise 1

Create:

authors
books

One author → many books

Add proper foreign keys.

Exercise 2

Create:

employees
employee_details

One-to-one relationship.

Exercise 3

Create:

movies
actors
movie_actors

Many-to-many relationship.

Insert:

  • 3 movies
  • 5 actors

Create relationships.

Exercise 4

For every relationship above, write the equivalent Rails models and associations.

Day 2 Preview

Next we’ll cover the foundation of everything in SQL:

SELECT Queries

Including:

  • SELECT
  • WHERE
  • ORDER BY
  • LIMIT
  • OFFSET
  • DISTINCT
  • IN
  • BETWEEN
  • LIKE
  • ILIKE
  • NULL handling

plus PostgreSQL execution behavior and ActiveRecord equivalents.

This is where real querying begins.

Happy Learning! 🚀