PostgreSQL — Schema, Index & Internals
1/17
Bài 1 / 17~13 phútCài đặt PostgreSQLMiễn phí… lượt xem

Cài PostgreSQL local — Docker, psql và schema TaskFlow

Cài PostgreSQL 16 bằng Docker Compose, kết nối psql qua port 5433, nạp schema TaskFlow 5 bảng và thuộc 8 lệnh psql đủ dùng cho cả phần internals.

TL;DR: Cách nhanh nhất để có PostgreSQL chạy local là Docker Compose: một file YAML khai image postgres:16-alpine, map host port 5433 vào container port 5432, một volume giữ dữ liệu, rồi docker compose up -d postgres. Kết nối bằng docker compose exec postgres psql -U olhub -d olhub, tạo database taskflow, paste 5 câu CREATE TABLE của schema TaskFlow và seed 10 user. Postgres.app hoặc brew/apt chỉ là đường phụ khi bạn không muốn Docker. Cuối bài bạn phân biệt được cluster, database và role, và thuộc 8 lệnh psql đủ dùng cho toàn khoá.

Khoá SQL & Database đã dạy relational model, ER diagram và tour schema TaskFlow trên giấy. Khoá này mở PostgreSQL ra xem bên trong, nên từ bài sau trở đi mọi thứ đều cần một cluster thật để chạy EXPLAIN, pageinspect, VACUUM. Bài này lo đúng việc đó. Cuối bài bạn có psql sẵn sàng và 10 user trong bảng users.

1. Ba cách cài — chọn cách nào?

CáchƯu điểmNhược điểmKhi nào dùng
Docker Compose (khuyến nghị)Reproducible, dọn sạch dễ, OLHub repo có sẵn configCần Docker Desktop đang chạyMọi dev mới, mọi OS
Postgres.app (macOS)Binary native, cài dễ, có GUI nhỏ quản lý serverChỉ macOS, không reproducible giữa máyDev macOS muốn nhanh, không cần Docker
brew / aptNative, tự chọn version, không cần DockerPhải tự tạo database, dễ đụng Postgres có sẵn của hệ thốngPower user quen CLI

Cả ba đường đều đổ về một tiến trình postgres lắng nghe trên máy bạn, có role olhub và database olhub. Làm theo một cách, bỏ qua hai cách còn lại.

Ba cách cài Docker Compose, Postgres.app và brew hoặc apt đều đổ về một tiến trình postgres lắng nghe localhost với role và database olhub; từ đó psql và GUI như DBeaver cùng kết nối, và schema TaskFlow 5 bảng chỉ nạp một lần

2. Cài bằng Docker Compose — đường chính

Yêu cầu: Docker Desktop đã cài và đang chạy.

Bước 1 — Chuẩn bị docker-compose.yml. Nếu bạn đã clone OLHub repo, file này có sẵn. Nếu chưa, tạo file với nội dung tối thiểu:

YAML
services:
  postgres:
    image: postgres:16-alpine
    ports:
      - "5433:5432"
    environment:
      POSTGRES_DB: olhub
      POSTGRES_USER: olhub
      POSTGRES_PASSWORD: olhub_secret_2026
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U olhub"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Bốn khối, mỗi khối một việc: ports map host 5433 vào container 5432, environment tạo role + database lúc init, volumes giữ dữ liệu qua các lần restart, healthcheck để Docker biết Postgres đã sẵn sàng nhận kết nối chứ không chỉ "process đang chạy".

Bước 2 — Khởi động và verify.

Bash
docker compose up -d postgres
docker compose ps postgres

Output mong đợi:

NAME             IMAGE                COMMAND                  SERVICE    CREATED         STATUS                   PORTS
olhub-postgres   postgres:16-alpine   "docker-entrypoint.s…"   postgres   5 seconds ago   Up 4 seconds (healthy)   0.0.0.0:5433->5432/tcp

Chữ (healthy) đến từ khối healthcheck: pg_isready đã trả lời được; 5–10 giây đầu còn thấy (health: starting) là bình thường.

Bước 3 — Kết nối bằng psql.

Bash
docker compose exec postgres psql -U olhub -d olhub
psql (16.x)
Type "help" for help.

olhub=#

Prompt olhub=# nghĩa là bạn đang ở database olhub với quyền superuser (dấu #; user thường thấy >). Gõ \q để thoát.

Connection string từ host (dùng cho GUI tool hoặc app chạy ngoài Docker):

postgresql://olhub:olhub_secret_2026@localhost:5433/olhub

Số 5433 chỉ có ở phía host; app chạy cùng compose network vẫn nối container qua postgres:5432.

Dọn sạch khi không cần:

Bash
docker compose down          # stop container, giu data
docker compose down -v       # stop container + xoa volume (mat data)

3. Đường phụ — Postgres.app và brew/apt

Chỉ đọc mục này nếu bạn không dùng Docker.

Postgres.app (macOS)brew (macOS)apt (Ubuntu/Debian)
CàiTải .dmg ở postgresapp.com, kéo vào Applications, bấm Initialize rồi Startbrew install postgresql@16 && brew services start postgresql@16sudo apt install -y postgresql-16 postgresql-contrib && sudo systemctl enable --now postgresql
Mặc địnhPort 5432, trust mode, username = user macOSPort 5432, trust mode, username = user macOSPort 5432, peer auth, user hệ thống postgres
Kết nối/Applications/Postgres.app/Contents/Versions/latest/bin/psql -d postgrescreatedb taskflow && psql -d taskflowsudo -u postgres psql
Pitfall — hai lỗi đầu tiên của đường phụ

command not found: psql sau khi cài Postgres.app: app không tự thêm binary vào PATH, thêm export PATH="/Applications/Postgres.app/Contents/Versions/latest/bin:$PATH" vào ~/.zshrc rồi source lại. database "alice" does not exist sau brew services start: brew tạo cluster nhưng không tạo database trùng tên user, chạy createdb taskflow trước rồi mới psql -d taskflow.

Thử ngẫmmáy bạn đã cài Postgres qua brew cho một dự án cũ, giờ thêm OLHub Docker. Hai bản Postgres này có tranh port hay tranh dữ liệu với nhau không?

4. Cluster, database và role khác nhau thế nào?

Hình dung một toà nhà văn phòng: toà nhà là cluster, mỗi tầng là một database, thẻ ra vào là role. Thẻ do ban quản lý toà nhà cấp nên lên được mọi tầng, nhưng mở được phòng nào trên tầng đó là chuyện của từng tầng.

Toà nhàPostgreSQL
Toà nhà, một ban quản lýCluster: một tiến trình postgres, một thư mục dữ liệu, một port
TầngDatabase: không gian tên riêng cho schema và bảng
Thẻ ra vào cấp cho cả toàRole: tài khoản đăng nhập, dùng chung cho mọi database

Container olhub-postgres là một cluster; bản brew trên máy bạn là một cluster khác. Bên trong có nhiều database (olhub, taskflow, postgres); một kết nối psql luôn gắn với đúng một database, muốn sang database khác phải \c. Role vừa là tài khoản đăng nhập vừa là chủ sở hữu object, và thuộc về cluster: olhub đăng nhập được vào cả olhub lẫn taskflow. POSTGRES_USER trong compose tạo role, POSTGRES_DB tạo database, cả hai chỉ chạy lần init đầu khi volume còn trống.

5. psql — 8 lệnh phải nhớ

LệnhMô tả
\lLiệt kê database trong cluster
\c <db>Connect sang database khác
\dtLiệt kê bảng trong schema hiện tại (public)
\d <table>Mô tả bảng: column, type, index, constraint
\duLiệt kê role trong cluster
\timing onHiện thời gian thực thi mỗi query
\xToggle expanded display, mỗi column một dòng
\qThoát psql

Trước khi gõ: database olhub vừa được Docker tạo lúc init, bạn đoán \dt in ra gì?

olhub=# \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |
-----------+----------+----------+-------------+-------------+
 olhub     | olhub    | UTF8     | en_US.utf8  | en_US.utf8  |
 postgres  | postgres | UTF8     | en_US.utf8  | en_US.utf8  |

olhub=# \dt
Did not find any relations.

\dt báo không có relation nào vì olhub là database trống, còn \l cho thấy cluster có hai database và hai owner khác nhau. Bước tiếp theo tạo database thứ ba và nạp bảng vào đó.

6. Nạp schema TaskFlow

Schema TaskFlow gồm 5 bảng; DDL đầy đủ và giải thích từng constraint ở Tour TaskFlow schema của khoá SQL & Database. Ở đây chỉ cần copy-paste để DB sẵn sàng cho module index.

Bước 1 — Tạo database taskflow:

Bash
docker compose exec postgres psql -U olhub -d olhub -c "CREATE DATABASE taskflow;"

Bước 2 — Apply DDL bằng heredoc (hoặc paste 5 CREATE TABLE vào psql):

Bash
docker compose exec -T postgres psql -U olhub -d taskflow <<'EOF'
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE projects (
  id BIGSERIAL PRIMARY KEY,
  owner_id BIGINT NOT NULL REFERENCES users(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE project_members (
  user_id BIGINT NOT NULL REFERENCES users(id),
  project_id BIGINT NOT NULL REFERENCES projects(id),
  role TEXT NOT NULL CHECK (role IN ('owner', 'member', 'viewer')),
  joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (user_id, project_id)
);

CREATE TABLE tasks (
  id BIGSERIAL PRIMARY KEY,
  project_id BIGINT NOT NULL REFERENCES projects(id),
  assignee_id BIGINT REFERENCES users(id),
  title TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('todo', 'doing', 'done', 'archived')) DEFAULT 'todo',
  due_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE comments (
  id BIGSERIAL PRIMARY KEY,
  task_id BIGINT NOT NULL REFERENCES tasks(id),
  user_id BIGINT NOT NULL REFERENCES users(id),
  body TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
EOF

Bước 3 — Seed nhỏ (10 user + 5 project):

Bash
docker compose exec -T postgres psql -U olhub -d taskflow <<'EOF'
INSERT INTO users (email, name) VALUES
  ('[email protected]', 'Nguyen Van An'),
  ('[email protected]', 'Tran Thi Binh'),
  ('[email protected]', 'Le Hoang Cuong'),
  ('[email protected]', 'Pham Tien Dung'),
  ('[email protected]', 'Hoang Thi Em'),
  ('[email protected]', 'Vo Minh Fan'),
  ('[email protected]', 'Bui Quoc Giang'),
  ('[email protected]', 'Do Thi Huong'),
  ('[email protected]', 'Mai Van Inh'),
  ('[email protected]', 'Ngo Hoang Khoa');

INSERT INTO projects (owner_id, name) VALUES
  (1, 'OLHub Platform'),
  (1, 'Marketing Site'),
  (2, 'Mobile App'),
  (3, 'Internal Dashboard'),
  (4, 'Data Pipeline');
EOF

Verify:

Bash
docker compose exec postgres psql -U olhub -d taskflow -c "SELECT count(*) FROM users;"
 count
-------
    10
(1 row)
Seed chỉ có users + projects

Các bảng project_members, tasks, comments còn rỗng: 1 triệu task sẽ được sinh bằng script ở mini-challenge dashboard tuning khi cần dataset đủ lớn để đo. SELECT count(*) FROM tasks trả 0 lúc này là điều đã lường trước.

Thử ngẫmseed chỉ có users và projects. Nếu bài sau JOIN tasks với users để đếm task mỗi người, kết quả lúc này là gì, và INNER JOIN với LEFT JOIN có cho cùng đáp án không?

7. GUI tool — chọn cái nào

Khuyến nghị cho khoá này: DBeaver Community (miễn phí, cross-platform, vẽ ER diagram từ FK). Kết nối với OLHub Docker: New Database Connection → PostgreSQL, Host localhost, Port 5433, Database olhub, Username olhub, Password olhub_secret_2026, bấm Test Connection. pgAdmin 4 đáng mở khi cần xem query plan dạng đồ hoạ; còn lại psql đủ dùng cho cả khoá.

8. Pitfall — 3 lỗi setup phổ biến

Lỗi 1 — Connection refused

psql: error: connection to server at "localhost" (127.0.0.1), port 5433 failed: Connection refused

Container chưa start hoặc đang khởi động. docker compose ps postgres chưa thấy Up (healthy) thì docker compose up -d postgres rồi đợi thêm 5–10 giây.

Lỗi 2 — password authentication failed

psql: error: connection to server failed: FATAL: password authentication failed for user "alice"

Sai role hoặc sai password. Postgres tạo sẵn superuser postgres khi init cluster, nhưng container OLHub không bật trust mode cho nó; role có password là olhub, khai trong docker-compose.yml. Luôn dùng -U olhub và kiểm tra lại connection string ở phần trên.

Lỗi 3 — port is already allocated

Error response from daemon: driver failed programming external connectivity:
bind for 0.0.0.0:5433 failed: port is already allocated

Process khác đang giữ 5433. lsof -i :5433 để xem là ai: nếu là container cũ thì docker compose down trước; nếu là process khác thì đổi host port trong docker-compose.yml sang 5434 hoặc port trống bất kỳ.

9. Workflow dev hàng ngày

Buổi sáng docker compose up -d postgres, rồi vào psql bằng docker compose exec postgres psql -U olhub -d taskflow. Gõ hai lệnh này mỗi ngày thì nên có alias:

Bash
alias psqltf='docker compose exec postgres psql -U olhub -d taskflow'

Trước khi nghịch ALTER TABLE hay DELETE hàng loạt, chụp snapshot để restore trong vài giây thay vì setup lại từ đầu:

Bash
# Snapshot
docker compose exec postgres pg_dump -U olhub -d taskflow > taskflow-snapshot.sql

# Restore
docker compose exec postgres psql -U olhub -d taskflow -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
docker compose exec -T postgres psql -U olhub -d taskflow < taskflow-snapshot.sql

Thử ngẫmbạn vừa restore bằng DROP SCHEMA CASCADE nhưng quên mình đang connect vào database olhub thay vì taskflow. Hậu quả là gì, và vì sao Postgres không cảnh báo?

10. Deep Dive — Postgres setup

📚 Deep Dive — Postgres setup

11. Liên hệ các bài khác

  • Tour TaskFlow schema — DDL 5 bảng bạn vừa paste được giải thích từng constraint ở đó; đọc lại nếu chưa rõ vì sao project_members có primary key ghép.
  • ER modeling — sơ đồ ER mà 5 bảng này map về, cần khi bài index nói "cardinality của assignee_id".
  • B-tree internals — bài đầu tiên dùng cluster này để chạy pageinspect; không có Postgres thật thì bài đó chỉ đọc được, không thử được.
  • Mini-challenge dashboard tuning — nơi bảng tasks được seed 1 triệu row; mọi con số EXPLAIN của module index đo trên dataset đó.
  • Publish port — DNAT và Kết nối container bằng veth — dòng "5433:5432" là một luật DNAT của Docker; đọc khi muốn biết vì sao localhost:5433 tới được container.

12. Tóm tắt

  • Checklist sau setup: docker compose ps thấy (healthy), SELECT count(*) FROM users trên taskflow trả 10.
  • Đọc lỗi kết nối theo chữ: Connection refused là container chưa lên, password authentication failed là sai role (olhub, không phải postgres), port is already allocated thì lsof -i :5433.
  • Gặp ECONNREFUSED từ app trong compose network thì kiểm tra app có đang trỏ nhầm vào localhost:5433 thay vì postgres:5432 không.
  • Snapshot bằng pg_dump trước khi nghịch, restore bằng DROP SCHEMA public CASCADE + psql < file; cách này cho môi trường học, production dùng migration tool.
  • Ba lệnh psql bạn sẽ gõ nhiều nhất ở module index: \d <table> xem index và constraint, \timing on trước khi đo, \x khi output nhiều cột.

Bài tiếp theo mở B-tree ra xem page, page split và fill factor trên chính cluster bạn vừa dựng.

13. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao OLHub map host port 5433 thay vì dùng default 5432? Nếu một dev khác trên cùng team cũng đang chạy Postgres local thì sao?
  2. Q2
    Phân biệt khi nào dùng psql CLI và khi nào dùng GUI tool. Nêu hai việc ví dụ cho mỗi loại.
  3. Q3
    Bạn chạy CREATE ROLE reporter LOGIN PASSWORD 'x' khi đang connect vào database olhub. Role đó có đăng nhập được vào database taskflow không? Vì sao?
  4. Q4
    Bạn chạy \dt trong psql và thấy 'Did not find any relations' dù đã CREATE TABLE trước đó. Nguyên nhân có thể là gì?
  5. Q5
    Bạn muốn rollback database về trạng thái trước khi thử một loạt ALTER TABLE và DELETE. Dùng lệnh nào? Tradeoff so với dùng migration tool như Flyway/Liquibase?

Bài tiếp theo: B-tree internals — page split và fill factor. Chưa quen schema TaskFlow? Đọc lại Tour TaskFlow schema ở khoá SQL & Database trước.

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

B-tree internals — page split, fill factor và height