CHAPTER 10 / DATABASE
資料庫:
把東西存起來
使用者註冊了、買了、留言了——這些資料都要存。存哪?檔案不夠用、變數一重整就沒。 這章教你 SQL(PostgreSQL/Supabase)、NoSQL(MongoDB)、ORM(Prisma),上完能撐住一個 SaaS。
- 會設計資料表、寫 SQL 查詢
- 會用 Supabase 做出有後台的 SaaS
- 會用 MongoDB 處理彈性結構資料
- 會用 Prisma ORM 操作資料庫
- 分得清何時用 SQL、何時用 NoSQL
LESSON 10.1
SQL vs NoSQL
| SQL(PostgreSQL) | NoSQL(MongoDB) | |
|---|---|---|
| 結構 | 表格、欄位固定 | 文件、欄位彈性 |
| 關聯 | 強大(JOIN) | 較弱 |
| 一致性 | 強 | 最終一致 |
| 學習 | 較難(要會 SQL) | 較易(像 JSON) |
| 適合 | 電商、訂單、財務 | 內容、社群、日誌 |
不確定?選 PostgreSQL。它支援 JSON 欄位(兼具 NoSQL 彈性)、能勝任 99% 場景。Supabase、Neon 都是 PostgreSQL 雲端服務。
LESSON 10.2
SQL 入門
建表
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
body TEXT,
published BOOLEAN DEFAULT FALSE
);
CRUD
-- 新增
INSERT INTO users (email, name) VALUES ('a@b.com', '小明');
-- 查詢
SELECT * FROM users WHERE id = 1;
SELECT name, email FROM users ORDER BY created_at DESC LIMIT 10;
-- JOIN(最強功能)
SELECT p.title, u.name
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.published = TRUE;
-- 統計
SELECT user_id, COUNT(*) AS post_count
FROM posts
GROUP BY user_id
HAVING COUNT(*) > 5;
-- 更新
UPDATE users SET name = '阿明' WHERE id = 1;
-- 刪除
DELETE FROM posts WHERE id = 42;
索引:查詢加速
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
沒索引的大表查詢慢到要哭。常用 WHERE / ORDER BY 的欄位都該有索引。
LESSON 10.3
Supabase:PostgreSQL 雲端神器
Supabase = 託管 PostgreSQL + 即時訂閱 + 認證 + 檔案儲存 + Auto-API。個人專案、SaaS MVP 神器。免費方案夠用。
三步驟上線
- 到 supabase.com 建專案,挑離你近的區域(東京)
- SQL Editor 建表、設 RLS(Row Level Security)
- 前端裝
@supabase/supabase-js直連
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// 查
const { data, error } = await supabase
.from('posts')
.select('*, user:users(name)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10);
// 寫
await supabase.from('posts').insert({ title: '你好', user_id: 1 });
// 即時訂閱
supabase.channel('posts')
.on('postgres_changes', { event: 'INSERT', table: 'posts' }, payload => {
console.log('有新貼文!', payload.new);
})
.subscribe();
RLS(Row Level Security):超重要
-- 開 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 大家可以看 published 文章
CREATE POLICY "public read" ON posts
FOR SELECT USING (published = true);
-- 只能改自己的
CREATE POLICY "own update" ON posts
FOR UPDATE USING (auth.uid() = user_id);
用 Supabase 一定要設 RLS。Anon key 是公開的,不設 RLS 等於資料庫對全世界開放。新手最常踩的雷。
LESSON 10.4
Prisma ORM
ORM = 用程式語法操作資料庫,不用直接寫 SQL。Prisma 是 Node.js 最強。
npm install prisma @prisma/client
npx prisma init
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
user User @relation(fields: [userId], references: [id])
userId Int
}
npx prisma migrate dev --name init
npx prisma studio # 視覺化資料庫操作
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 查(含關聯)
const posts = await prisma.post.findMany({
where: { published: true },
include: { user: true },
orderBy: { createdAt: 'desc' },
take: 10
});
// 建
await prisma.post.create({
data: { title: '你好', user: { connect: { id: 1 } } }
});
LESSON 10.5
MongoDB
NoSQL 王者。資料像 JSON,schema 彈性。Insight 後台、即時通訊、IoT 資料常用。
// 用 Mongoose
import mongoose from 'mongoose';
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
body: String,
tags: [String],
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
meta: { type: mongoose.Schema.Types.Mixed } // 任意 JSON
}, { timestamps: true });
const Post = mongoose.model('Post', postSchema);
// 用
await Post.create({ title: '你好', tags: ['tech'] });
const posts = await Post.find({ tags: 'tech' }).populate('author');
MongoDB Atlas(雲端託管,免費)
到 mongodb.com/atlas 建免費 cluster,把連線字串放 .env 就能用。Insight 早期就跑在 Atlas 免費版。
LESSON 10.6
資料表設計三原則
- Normalization(正規化):每樣東西只存一次。使用者資料只在 users 表,posts 表存 user_id。
- 關聯:1對多用外鍵、多對多用中間表。
- 命名一致:表名複數(
users)、外鍵{單數}_id。團隊一致就好。
多對多範例
-- 文章可以有多個標籤、標籤可以在多篇
CREATE TABLE posts ( id SERIAL PRIMARY KEY, ... );
CREATE TABLE tags ( id SERIAL PRIMARY KEY, name VARCHAR );
CREATE TABLE post_tags (
post_id INT REFERENCES posts(id),
tag_id INT REFERENCES tags(id),
PRIMARY KEY (post_id, tag_id)
);
練習:多人部落格資料庫
- 用 Supabase 建 users、posts、comments 三張表
- 用 Prisma 寫 schema,跑 migrate
- RLS:使用者只能編輯自己的文章 / 留言
- API:列文章(含作者名、留言數)
- 用 Supabase realtime,新留言即時推播給文章作者
常見卡關 FAQ
// DB 學員最常問的問題
Supabase Free 夠用嗎?
個人專案、MVP 完全夠。500MB 資料、50000 月活、2GB 頻寬。超過再升級 25 美/月。比自己架便宜太多。
Prisma 還是直接寫 SQL?
中小專案 Prisma 香爆——type-safe、自動 migration、有 Studio。只在做極致效能(高併發 query)才直接寫 SQL。
N+1 query 問題是什麼?
查 10 篇文章,再為每篇查作者 = 11 次 query。應該用 JOIN(
include 在 Prisma、populate 在 Mongoose),一次撈完。Slack 變慢通常是這個。怎麼避免 SQL 注入?
永遠用參數化查詢,不要字串拼接。Prisma、Supabase、Mongoose 都自動處理。手寫
raw SQL 才要小心。詳見第 13 章。資料庫怎麼備份?
Supabase / Atlas 自動每日備份。自架 PostgreSQL 用
pg_dump 寫 cron 排程。關鍵:要驗證能還原,沒測過的備份等同沒備份。