TLDR: Sử dụng INNER JOIN để kết hợp các hàng liên quan từ hai bảng. LEFT JOIN bao gồm tất cả các hàng từ bảng đầu tiên ngay cả khi không có hàng nào khớp. Truy vấn con cho phép bạn lồng các truy vấn. Cửa sổ hàm thực hiện tính toán trên các hàng mà không cần nhóm. CTE làm cho các truy vấn phức tạp có thể đọc được.

Hầu hết các truy vấn thực tế đều cần dữ liệu từ nhiều bảng. Phép JOIN kết hợp các bảng dựa trên mối quan hệ.

Hiểu phép JOIN

Cho hai bảng:

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50)
);

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  user_id INTEGER REFERENCES users(id),
  title VARCHAR(200)
);

INSERT INTO users (id, username) VALUES (1, 'alice'), (2, 'bob'), (3, 'carol');
INSERT INTO posts (user_id, title) VALUES
  (1, 'Alice First Post'),
  (1, 'Alice Second Post'),
  (2, 'Bob Post');

Người dùng:

 id | username
----+----------
  1 | alice
  2 | bob
  3 | carol

Bài viết:

 id | user_id | title
----+---------+-------------------
  1 |       1 | Alice First Post
  2 |       1 | Alice Second Post
  3 |       2 | Bob Post

THAM GIA TRONG

Chỉ trả về các hàng phù hợp:

SELECT users.username, posts.title
FROM users
INNER JOIN posts ON users.id = posts.user_id;

Kết quả:

 username |       title
----------+-------------------
 alice    | Alice First Post
 alice    | Alice Second Post
 bob      | Bob Post

Carol không xuất hiện - cô ấy không có bài viết nào.

Bí danh bảng làm cho truy vấn ngắn hơn:

SELECT u.username, p.title
FROM users u
INNER JOIN posts p ON u.id = p.user_id;

THAM GIA TRÁI

Trả về tất cả các hàng từ bảng bên trái, với NULL cho những hàng không khớp:

SELECT u.username, p.title
FROM users u
LEFT JOIN posts p ON u.id = p.user_id;

Kết quả:

 username |       title
----------+-------------------
 alice    | Alice First Post
 alice    | Alice Second Post
 bob      | Bob Post
 carol    | NULL

Carol xuất hiện mặc dù cô ấy không có bài viết nào.

Tìm người dùng không có bài viết:

SELECT u.username
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE p.id IS NULL;

QUYỀN THAM GIA

Đối diện với LEFT JOIN - tất cả các hàng từ bảng bên phải:

SELECT u.username, p.title
FROM posts p
RIGHT JOIN users u ON u.id = p.user_id;

Kết quả tương tự như LEFT JOIN với các bảng được hoán đổi. Hầu hết mọi người thích LEFT JOIN vì tính nhất quán.

THAM GIA BÊN NGOÀI ĐẦY ĐỦ

Tất cả các hàng từ cả hai bảng:

SELECT u.username, p.title
FROM users u
FULL OUTER JOIN posts p ON u.id = p.user_id;

Hiếm khi cần thiết nhưng hữu ích khi tìm các hàng không khớp trong cả hai bảng.

Nhiều phép JOIN

Phép JOIN nhiều hơn hai bảng:

CREATE TABLE comments (
  id SERIAL PRIMARY KEY,
  post_id INTEGER REFERENCES posts(id),
  user_id INTEGER REFERENCES users(id),
  content TEXT
);

INSERT INTO comments (post_id, user_id, content) VALUES
  (1, 2, 'Great post!'),
  (1, 3, 'Thanks for sharing');

SELECT
  p.title AS post_title,
  u.username AS author,
  c.content AS comment,
  cu.username AS commenter
FROM posts p
JOIN users u ON p.user_id = u.id
JOIN comments c ON c.post_id = p.id
JOIN users cu ON c.user_id = cu.id;

Kết quả:

    post_title    | author | comment           | commenter
------------------+--------+-------------------+-----------
 Alice First Post | alice  | Great post!       | bob
 Alice First Post | alice  | Thanks for sharing| carol

Tự phép JOIN

Phép JOIN một bảng với chính nó:

CREATE TABLE employees (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100),
  manager_id INTEGER REFERENCES employees(id)
);

INSERT INTO employees (name, manager_id) VALUES
  ('CEO', NULL),
  ('VP Sales', 1),
  ('VP Engineering', 1),
  ('Sales Rep', 2),
  ('Developer', 3);

-- Find employees and their managers
SELECT
  e.name AS employee,
  m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Kết quả:

    employee     | manager
-----------------+---------
 CEO             | NULL
 VP Sales        | CEO
 VP Engineering  | CEO
 Sales Rep       | VP Sales
 Developer       | VP Engineering

Truy vấn con

Truy vấn trong truy vấn.

Truy vấn phụ ở WHERE

-- Posts by users with more than 5 posts
SELECT title
FROM posts
WHERE user_id IN (
  SELECT user_id
  FROM posts
  GROUP BY user_id
  HAVING COUNT(*) > 5
);

Truy vấn con trong CHỌN

SELECT
  u.username,
  (SELECT COUNT(*) FROM posts p WHERE p.user_id = u.id) AS post_count
FROM users u;

Kết quả:

 username | post_count
----------+------------
 alice    |          2
 bob      |          1
 carol    |          0

Truy vấn con trong TỪ

SELECT avg_posts.username, avg_posts.post_count
FROM (
  SELECT u.username, COUNT(p.id) AS post_count
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
  GROUP BY u.username
) AS avg_posts
WHERE avg_posts.post_count > 0;

Biểu thức bảng chung (CTE)

CTE làm cho các truy vấn phức tạp có thể đọc được. Sử dụng WITH:

WITH user_post_counts AS (
  SELECT u.id, u.username, COUNT(p.id) AS post_count
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
  GROUP BY u.id, u.username
)
SELECT * FROM user_post_counts WHERE post_count > 1;

Nhiều CTE:

WITH active_users AS (
  SELECT * FROM users WHERE created_at > CURRENT_DATE - INTERVAL '30 days'
),
recent_posts AS (
  SELECT * FROM posts WHERE created_at > CURRENT_DATE - INTERVAL '7 days'
)
SELECT u.username, COUNT(p.id) AS recent_post_count
FROM active_users u
LEFT JOIN recent_posts p ON u.id = p.user_id
GROUP BY u.username;

Cửa sổ hàm

Thực hiện tính toán trên các hàng liên quan mà không cần GROUP BY.

ROW_NUMBER

SELECT
  username,
  created_at,
  ROW_NUMBER() OVER (ORDER BY created_at) AS signup_order
FROM users;

Kết quả:

 username | created_at          | signup_order
----------+---------------------+--------------
 alice    | 2024-01-01 10:00:00 | 1
 bob      | 2024-01-02 11:00:00 | 2
 carol    | 2024-01-03 12:00:00 | 3

RANK và DENSE_RANK

SELECT
  username,
  post_count,
  RANK() OVER (ORDER BY post_count DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY post_count DESC) AS dense_rank
FROM (
  SELECT u.username, COUNT(p.id) AS post_count
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
  GROUP BY u.username
) user_posts;

PHẦN THAM GIA BỞI

Tính toán trong nhóm:

SELECT
  p.title,
  u.username,
  p.views,
  AVG(p.views) OVER (PARTITION BY u.id) AS user_avg_views
FROM posts p
JOIN users u ON p.user_id = u.id;

Hiển thị lượt xem của từng bài đăng và lượt xem trung bình của người dùng trên tất cả các bài đăng của họ.

LAG và DẪN

Truy cập các giá trị hàng trước/tiếp theo:

SELECT
  title,
  created_at,
  LAG(created_at) OVER (ORDER BY created_at) AS prev_post_date,
  LEAD(created_at) OVER (ORDER BY created_at) AS next_post_date
FROM posts;

Hữu ích cho việc tính toán thời gian giữa các sự kiện.

ĐOÀN và ĐOÀN TẤT CẢ

Kết hợp kết quả từ nhiều truy vấn:

SELECT username AS name, 'user' AS type FROM users
UNION ALL
SELECT title AS name, 'post' AS type FROM posts;

UNION loại bỏ các bản sao, UNION ALL giữ chúng (nhanh hơn).

Ví dụ thực tế

Tìm tác giả hàng đầu:

SELECT
  u.username,
  COUNT(p.id) AS post_count,
  SUM(p.views) AS total_views
FROM users u
JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.username
ORDER BY total_views DESC
LIMIT 10;

Bài viết có nhiều bình luận nhất:

SELECT
  p.title,
  u.username AS author,
  COUNT(c.id) AS comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id, p.title, u.username
ORDER BY comment_count DESC
LIMIT 5;

Tóm tắt hoạt động của người dùng:

WITH user_stats AS (
  SELECT
    u.id,
    u.username,
    COUNT(DISTINCT p.id) AS posts,
    COUNT(DISTINCT c.id) AS comments
  FROM users u
  LEFT JOIN posts p ON p.user_id = u.id
  LEFT JOIN comments c ON c.user_id = u.id
  GROUP BY u.id, u.username
)
SELECT
  username,
  posts,
  comments,
  posts + comments AS total_activity
FROM user_stats
ORDER BY total_activity DESC;

Hiểu các phép nối và các kỹ thuật truy vấn nâng cao này cho phép bạn trích xuất chính xác dữ liệu bạn cần. Tiếp theo, chúng ta sẽ khám phá các chỉ mục - cách thực hiện các truy vấn này nhanh chóng ngay cả với hàng triệu hàng.