โหมดมืด
บทที่ 8 — NoSQL Intro (MongoDB + Redis)
หลังจบบท คุณจะ:
- เข้าใจ NoSQL 4 ประเภท + เลือกถูก
- ใช้ MongoDB พื้นฐาน (query, index, transaction)
- ใช้ Redis สำหรับ cache + queue + session
- รู้ว่าเมื่อไหร่ใช้ SQL vs NoSQL vs ผสมกัน
1. NoSQL 4+ ประเภท (recap)
NoSQL (อ่าน "โน-เอส-คิว-แอล") ไม่ใช่ฐานข้อมูลแบบเดียว แต่เป็นกลุ่มที่แบ่งได้หลายประเภทตามรูปแบบข้อมูลและงานที่เหมาะ — document (schema ยืดหยุ่น), key-value (cache/session), wide-column (write-heavy), graph (ความสัมพันธ์เยอะ) บวกกับ 2 หมวดใหม่ที่โตเร็วในยุค 2026: time-series และ vector บทนี้จะเจาะ MongoDB (อ่าน "มอง-โก-ดี-บี" — document) และ Redis (อ่าน "เร-ดิส" — key-value) ที่ใช้บ่อยสุด:
| ประเภท | ตัวอย่าง | ใช้เมื่อ |
|---|---|---|
| Document | MongoDB (มอง-โก-ดี-บี), CouchDB | flexible schema, nested data |
| Key-Value | Redis (เร-ดิส), DynamoDB (ไดนา-โม-ดี-บี), Memcached | cache, session, counter |
| Wide-Column | Cassandra (แคส-แซน-ดรา), ScyllaDB (สกิล-ลา-ดี-บี), HBase | write-heavy, time-series, IoT |
| Graph | Neo4j (นี-โอ-โฟร์-เจ), Dgraph (ดี-กราฟ) | relationship-heavy (social, recommend) |
| Time-Series | TimescaleDB, InfluxDB v3, QuestDB | metrics, logs, IoT sensor |
| Vector | pgvector, Qdrant, Pinecone, Weaviate, Milvus, Turbopuffer, LanceDB | embedding search (RAG, AI app) |
💡 หมวดเดิมมี 4 ประเภท แต่ปี 2026 Time-Series และ Vector (สำหรับ AI embedding) โตจนกลายเป็นหมวดมาตรฐาน — DynamoDB ในงานจริงนิยมใช้แบบ "single-table design" (key-value + wide-column hybrid)
เปรียบเทียบ 4 หมวดหลักแบบเร็ว
| ด้าน | Document (MongoDB) | Wide-Column (Cassandra) | Key-Value (Redis) | Graph (Neo4j) |
|---|---|---|---|---|
| โครงสร้างข้อมูล | JSON/BSON nested | row + dynamic columns ต่อ partition | คู่ key→value | node + edge |
| จุดเด่น | schema ยืดหยุ่น, query ลึกได้ | write throughput สูง, scale แนวนอน | latency ต่ำมาก (in-memory) | ค้นความสัมพันธ์ลึก |
| ใช้กับ | catalog, user profile, content | log, time-series, IoT | cache, session, counter | social, fraud, recommend |
| ความสม่ำเสมอ | tunable (default majority) | tunable (default CL=ONE → AP) | single-node ตรง | ACID (Neo4j) |
💡 Cassandra/ScyllaDB consistency knob: Cassandra ปี 2026 อยู่ที่ 5.0 มีจุดเด่นคือ "tunable consistency" — ปรับ Consistency Level (CL) ต่อ query ได้
CL=ONE(default) → fast + AP (พร้อมใช้แม้ node ล่ม แต่อาจอ่านได้ค่าเก่า)CL=QUORUM→ near-strong (majority ต้อง ack)CL=QUORUM+ LWT (lightweight transaction, Paxos) → CP-ish (เกือบ strong consistent)ScyllaDB = Cassandra-compatible แต่เขียนใหม่ด้วย C++/shard-per-core ได้ throughput ~10x — ใช้ CQL เดียวกัน
Part 1: MongoDB
2. แนวคิด
MongoDB (อ่าน "มอง-โก-ดี-บี") = "เก็บ JSON document" — เก็บข้อมูลเป็นเอกสาร JSON (จริง ๆ คือ BSON = binary JSON) ที่ซ้อนกันได้ลึก ๆ ไม่ต้องประกาศ schema ล่วงหน้า
เทียบศัพท์ SQL ↔ MongoDB:
| SQL | MongoDB | หมายเหตุ |
|---|---|---|
| Database | Database | เหมือนกัน |
| Table | Collection | กลุ่มของ document |
| Row | Document | 1 record = 1 JSON object |
| Column | Field | แต่ field ซ้อนกันลึก ๆ ได้ (nested object/array) — SQL column เป็น scalar |
| Primary Key | _id | MongoDB สร้าง _id ให้อัตโนมัติเป็น ObjectId ถ้าไม่ระบุ |
| JOIN | $lookup (aggregation) | ทำได้แต่ช้ากว่า SQL JOIN — จึงนิยม embed แทน |
💡 mapping ด้านบนเป็นแบบหยาบ ๆ — จุดต่างสำคัญคือ MongoDB document nested ได้ลึก (object ใน object, array ของ object) ส่วน SQL column เป็นค่าเดี่ยว (scalar) เสมอ ดังนั้นโครงสร้างที่ใน SQL ต้องแยกหลายตาราง MongoDB อาจเก็บใน document เดียวก็พอ
javascript
// 1 document ใน collection "users"
{
"_id": ObjectId("..."),
"email": "anna@example.com",
"name": "Anna",
"age": 25,
"addresses": [ // array nested
{ "label": "home", "city": "Bangkok" },
{ "label": "work", "city": "Bangkok" }
],
"preferences": {
"theme": "dark",
"lang": "th"
}
}ไม่มี schema → document ใน collection เดียวกัน อาจมี field ต่างกันได้
3. Setup
เริ่มเล่น MongoDB ด้วย Docker — รัน 1 container ก็ใช้ได้เลย พร้อม mongosh (CLI) และ Compass (GUI) สำหรับสำรวจข้อมูล ทุกตัวอย่างในส่วนนี้รันตามได้หลัง setup เสร็จ:
yaml
# docker-compose.yml
services:
mongo:
image: mongo:8
command: ["--replSet", "rs0", "--bind_ip_all"]
ports: ["27017:27017"]
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secret
volumes:
- mongodata:/data/db
volumes:
mongodata:📌 ทำไมต้อง
--replSet? ใช้ mongo:8 (MongoDB 8.0 รุ่น current ปี 2026 — มี performance + sharding ดีขึ้นจาก 7.0) และเปิดเป็น replica set 1 node ตั้งแต่แรก เพราะ MongoDB transaction (ข้อ 8) ต้องการ replica set/sharded cluster — standalone จะใช้ไม่ได้ ตั้งแต่ MongoDB 5.0 ค่า default writeConcern =majorityแล้ว (เมื่อก่อนเป็นw:1) ส่วน readConcern default ยังเป็นlocal
bash
docker compose up -d
# init replica set ครั้งแรก (รันครั้งเดียว)
docker compose exec mongo mongosh -u admin -p secret --eval "rs.initiate()"
docker compose exec mongo mongosh -u admin -p secretGUI tool: MongoDB Compass (official)
4. CRUD
CRUD ใน MongoDB ใช้ method บน collection (insertOne, find, updateOne, deleteOne) แทน SQL — query เป็น JSON object ที่ระบุเงื่อนไข ส่วนนี้แสดงการเพิ่ม/ค้น/แก้/ลบ document พร้อม operator พื้นฐาน เทียบกับ SQL ที่เรียนมาจะเห็นว่าคิดคนละแบบแต่ทำงานเดียวกัน:
📌 คำสั่งต่อไปนี้พิมพ์ใน mongosh (Mongo shell) ที่เปิดในข้อ 3 — ไม่ใช่ใน Bash/PowerShell
javascript
// เลือก database (รันใน mongosh)
use mydb
// สร้าง collection อัตโนมัติเมื่อ insert
db.users.insertOne({
email: "anna@example.com",
name: "Anna",
age: 25,
tags: ["admin", "premium"]
});
db.users.insertMany([
{ email: "ben@example.com", name: "Ben", age: 30 },
{ email: "carol@example.com", name: "Carol", age: 28 }
]);
// --- ค้นหาแบบพื้นฐาน ---
db.users.find(); // ทั้งหมด
db.users.findOne({ email: "anna@example.com" });
// --- Comparison operators ($gt, $gte, $lt, $lte, $ne) ---
db.users.find({ age: { $gt: 25 } }); // age > 25
db.users.find({ age: { $gte: 18, $lte: 30 } }); // 18 ≤ age ≤ 30
db.users.find({ age: { $ne: 25 } }); // age ≠ 25
// --- Array operators ($in, $all) ---
db.users.find({ tags: "admin" }); // array contain
db.users.find({ tags: { $all: ["admin", "premium"] } }); // มีครบทุกค่า
db.users.find({ age: { $in: [25, 30, 35] } }); // age ใน list
// --- Logical operators ($or, $and, $not) ---
db.users.find({ $or: [{ age: 25 }, { name: "Ben" }] });
// --- Regex (เลือกแบบใดแบบหนึ่ง ไม่ใช้คู่กัน) ---
db.users.find({ name: { $regex: /^A/i } }); // literal regex + flag
// หรือ: db.users.find({ name: { $regex: "^A", $options: "i" } });
// ⚠️ อย่าใส่ `/^A/` (literal มี flag ในตัว) คู่กับ `$options:"i"` — บางเวอร์ชันเตือน
// Project (เลือก field)
db.users.find({}, { name: 1, email: 1 });
db.users.find({}, { age: 0 }); // exclude
// Sort + Limit + Skip
db.users.find().sort({ age: -1 }).limit(10).skip(20);
// Update
db.users.updateOne(
{ email: "anna@example.com" },
{ $set: { age: 26 } }
);
db.users.updateMany(
{ age: { $lt: 18 } },
{ $set: { status: "minor" } }
);
// $inc, $push, $pull
db.users.updateOne(
{ _id: ObjectId("...") },
{
$inc: { age: 1 },
$push: { tags: "vip" },
$pull: { tags: "trial" }
}
);
// Upsert
db.users.updateOne(
{ email: "new@x.com" },
{ $set: { name: "New", age: 20 } },
{ upsert: true }
);
// Delete
db.users.deleteOne({ email: "anna@example.com" });
db.users.deleteMany({ status: "inactive" });สรุป Operator MongoDB ที่ใช้บ่อย
| กลุ่ม | Operator | ความหมาย | เทียบ SQL |
|---|---|---|---|
| Comparison | $gt / $gte | มากกว่า / มากกว่าหรือเท่ากับ | > / >= |
| Comparison | $lt / $lte | น้อยกว่า / น้อยกว่าหรือเท่ากับ | < / <= |
| Comparison | $ne | ไม่เท่ากับ | <> / != |
| Comparison | $in / $nin | อยู่ใน / ไม่อยู่ใน list | IN / NOT IN |
| Logical | $or / $and / $not | ต่อเงื่อนไข | OR / AND / NOT |
| Element | $exists | field มีอยู่หรือไม่ | IS NULL / IS NOT NULL |
| Array | $all | array มีครบทุกค่า | (ไม่มีตรง ๆ) |
| Array | $elemMatch | element ใน array ตรงเงื่อนไข | (ต้อง subquery) |
| Update | $set | ตั้งค่า field | SET col = ? |
| Update | $inc | เพิ่ม/ลดตัวเลข atomic | SET col = col + ? |
| Update | $push / $pull | เพิ่ม/ลบ element ใน array | (ไม่มีตรง ๆ) |
| Search | $regex | match ด้วย regex | LIKE / REGEXP |
💡 จำเทคนิค: operator ขึ้นต้นด้วย
$ทั้งหมด — และเป็น key ของ object ที่ใส่ใน query/update
5. Aggregation Pipeline
aggregation pipeline คือเครื่องมือวิเคราะห์ข้อมูลของ MongoDB ที่เทียบเท่า GROUP BY + JOIN + window ของ SQL — ข้อมูลไหลผ่าน "stage" ทีละขั้น ($match → $group → $sort → ...) แต่ละ stage แปลงผลส่งให้ stage ถัดไป ทรงพลังมากแต่ต้องคิดแบบ pipeline:
แทน GROUP BY ใน SQL — pipeline ของ stage:
เริ่มจากตัวอย่างง่าย ๆ ก่อน ($match → $group):
javascript
// นับ order paid ของแต่ละ user
db.orders.aggregate([
{ $match: { status: "PAID" } },
{ $group: { _id: "$userId", count: { $sum: 1 }, revenue: { $sum: "$total" } } }
]);💡 อ่านเหมือนสายพาน: data →
$match(กรอง) →$group(รวม) → output
ตัวอย่างเต็ม (advanced) — เห็นพลังของ pipeline เมื่อใช้หลาย stage ซ้อนกัน ไม่ต้องเข้าใจทุกบรรทัดในครั้งแรก:
javascript
db.orders.aggregate([
// Stage 1: filter
{ $match: { status: "PAID", createdAt: { $gte: ISODate("2026-01-01") } } },
// Stage 2: group
{ $group: {
_id: "$userId",
orderCount: { $sum: 1 },
revenue: { $sum: "$total" },
avgOrder: { $avg: "$total" }
}},
// Stage 3: filter again
{ $match: { revenue: { $gt: 1000 } } },
// Stage 4: sort
{ $sort: { revenue: -1 } },
// Stage 5: limit
{ $limit: 10 },
// Stage 6: join with users (lookup)
{ $lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user"
}},
// Stage 7: unwind array
{ $unwind: "$user" },
// Stage 8: project (reshape)
{ $project: {
userId: "$_id",
userName: "$user.name",
revenue: 1,
orderCount: 1
}}
]);Stages ที่ใช้บ่อย
| Stage | ทำอะไร |
|---|---|
$match | filter (like WHERE) |
$group | group + aggregate |
$sort | sort |
$limit / $skip | pagination |
$project | select column + transform |
$lookup | LEFT JOIN |
$unwind | array → multiple docs |
$addFields | add computed field |
$count | count documents |
6. Index ใน MongoDB
เหมือน SQL, MongoDB ก็ต้องมี index เพื่อให้ query เร็ว — รองรับ single field, compound, unique, partial และพิเศษคือ index บน field ใน array/nested object และ text index สำหรับ search หลักการเลือก index เหมือนกับ SQL: index ตาม query ที่ใช้บ่อย:
javascript
// Single field
db.users.createIndex({ email: 1 }); // ascending
db.users.createIndex({ createdAt: -1 }); // descending
// Compound
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
// Unique
db.users.createIndex({ email: 1 }, { unique: true });
// Text index
db.articles.createIndex({ title: "text", body: "text" });
db.articles.find({ $text: { $search: "database performance" } });
// ⚠️ Text index: 1 collection มีได้ **แค่ 1 text index** (รวมหลาย field ใน index เดียวได้)
// และห้าม compound กับ field ประเภทอื่น (เช่น { title: "text", category: 1 })
// Geospatial
db.places.createIndex({ location: "2dsphere" });
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [100.5, 13.7] },
$maxDistance: 10000
}
}
});
// TTL (auto-delete after time)
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// 💡 expireAfterSeconds: 0 = ลบทันทีหลังเวลาที่ระบุใน field `expiresAt`
// ใส่ N (เช่น 3600) = ลบ N วินาทีหลังเวลาใน field นั้น
// ⚠️ TTL background task รันประมาณ 1 ครั้ง/นาที — doc ที่หมดอายุอาจค้างได้ถึง ~60s
// Partial
db.users.createIndex(
{ email: 1 },
{ partialFilterExpression: { status: "active" } }
);
// EXPLAIN
db.users.find({ email: "a@b.com" }).explain("executionStats");7. Schema Design ใน MongoDB
แม้ schemaless — design ยังสำคัญ
Embedded (1-1, 1-few)
javascript
// user มี addresses (น้อย ๆ)
{
_id: ObjectId(),
name: "Anna",
addresses: [
{ label: "home", line1: "...", city: "Bangkok" },
{ label: "work", line1: "...", city: "Bangkok" }
]
}✅ ดี: อ่านทีเดียว, atomic update
❌ ไม่ดี: ถ้า array ใหญ่ (>1000) — query ช้า + 16MB limit
Referenced (1-many, M-M)
javascript
// user document
{ _id: ObjectId("u1"), name: "Anna" }
// orders document (refer)
{ _id: ObjectId("o1"), userId: ObjectId("u1"), total: 100 }ใช้ $lookup join — ช้ากว่า embedded แต่ flexible กว่า
Hybrid
javascript
// user + last 10 orders embedded + reference ทั้งหมด
{
_id: ObjectId("u1"),
name: "Anna",
recentOrders: [
{ id: "o100", total: 50, date: ISODate(...) }
]
}
// + orders collection สำหรับ history เต็มกฎ
- 1-1 → embed
- 1-few (< 100, ไม่ใหญ่ขึ้นเรื่อย ๆ) → embed
- 1-many — depend on access pattern
- M-M → reference (junction-like)
8. Transaction (MongoDB 4.0+)
⚠️ ต้องเป็น replica set หรือ sharded cluster เท่านั้น — standalone
mongodไม่รองรับ multi-document transaction และจะ error: "Transaction numbers are only allowed on a replica set member..." — ดังนั้นใน Docker compose ข้อ 3 จึงเปิดด้วย--replSet rs0ตั้งแต่แรก
javascript
const session = db.getMongo().startSession();
// 🔑 ระบุ readConcern + writeConcern ให้ชัดเจน (production best practice)
session.startTransaction({
readConcern: { level: "snapshot" }, // snapshot isolation
writeConcern: { w: "majority" } // default ตั้งแต่ 5.0 แต่ระบุไว้ชัด ๆ
});
try {
db.accounts.updateOne(
{ _id: "A" },
{ $inc: { balance: -100 } },
{ session }
);
db.accounts.updateOne(
{ _id: "B" },
{ $inc: { balance: 100 } },
{ session }
);
session.commitTransaction();
} catch (e) {
session.abortTransaction();
throw e;
} finally {
session.endSession();
}💡 MongoDB 5.0+ ตั้ง
writeConcern = majorityให้เป็น default แล้ว (เมื่อก่อนเป็นw:1) ส่วนreadConcerndefault ยังเป็นlocal— สำหรับ transaction ใน production แนะนำให้ระบุsnapshotเพื่อกัน read anomaly⚠️ Transaction มี overhead — ออกแบบให้ atomic op ใน single document ดีกว่าถ้าทำได้
9. Spring Boot + MongoDB
📌 ส่วนนี้เป็นโค้ด Java/Spring Boot ถ้ายังไม่ได้อ่าน Spring Boot ข้ามได้ — ไม่กระทบความเข้าใจ MongoDB เอง
ใช้ MongoDB ใน Spring Boot ได้เหมือน JPA — @Document map class กับ collection, MongoRepository ให้ query method สำเร็จรูป (findByEmail) ที่ Spring แปลงเป็น MongoDB query ให้ ทำให้สลับจาก SQL มา MongoDB ใช้ pattern เดิมได้:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>java
@Document(collection = "users")
public class User {
@Id private String id;
@Indexed(unique = true) private String email;
private String name;
private List<Address> addresses;
}
@Repository
public interface UserRepository extends MongoRepository<User, String> {
Optional<User> findByEmail(String email);
List<User> findByAgeBetween(int min, int max);
}ใช้เหมือน JPA — แต่ behind จะ map เป็น MongoDB query
10. ⚠️ MongoDB Pitfalls
| Pitfall | แก้ |
|---|---|
| ใช้ MongoDB เพราะ "ใหม่" | ใช้เมื่อ schema flexible จริง |
| Embed ทุกอย่าง | embed แค่ 1-1, 1-few |
| ไม่ใส่ index | index จำเป็น (เหมือน SQL) |
| Transaction ทุกอย่าง | atomic op ใน single doc ก่อน |
$lookup ทุก query | denormalize ถ้า query บ่อย |
| Document ใหญ่ (>16MB) | reference / split |
| ไม่มี schema validation | ใช้ JSON Schema validation |
Part 2: Redis
11. Redis คืออะไร
Redis (อ่าน "เร-ดิส") = in-memory key-value store — เร็วมาก (sub-millisecond)
แยกคำให้เข้าใจง่าย:
- in-memory = เก็บข้อมูลไว้ใน RAM (ไม่ใช่ disk) → อ่าน/เขียนเร็วระดับไมโครวินาที
- key-value = จับคู่ "กุญแจ" (key) กับ "ค่า" (value) เหมือน
HashMapใน Java - store = ที่จัดเก็บข้อมูล
💡 ปี 2026 ต้องรู้: หลัง Redis เปลี่ยน license เป็น SSPL/AGPL (มี.ค. 2024) Linux Foundation จึง fork ออกมาเป็น Valkey (BSD license) — code base ยังคล้ายกัน CLI/protocol ใช้ได้แทบจะตรง ๆ Redis ปัจจุบัน (2026) อยู่ที่เวอร์ชัน 8.0 ส่วน Valkey เป็นทางเลือก open-source สำหรับองค์กรที่ห่วง license
ใช้สำหรับ:
- ✅ Cache
- ✅ Session storage
- ✅ Counter / Rate limiter
- ✅ Pub/Sub
- ✅ Queue
- ✅ Real-time leaderboard
- ❌ Primary database (memory limit + persistence ไม่ใช่หลัก)
12. Setup
เริ่ม Redis ด้วย Docker เหมือนกัน — 1 container + redis-cli ก็พร้อมใช้ สังเกต option --appendonly yes ที่เปิด persistence (เขียนลง disk) เพราะ Redis เก็บข้อมูลใน memory เป็นหลัก ถ้าไม่เปิดข้อมูลหายเมื่อ restart:
yaml
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
volumes:
- redisdata:/data
command: redis-server --appendonly yes
volumes:
redisdata:bash
docker compose up -d
docker compose exec redis redis-cli13. Data Types
Redis ไม่ใช่แค่ key-value ธรรมดา — value เป็นได้หลาย "data type" ที่มี operation เฉพาะ: String (+INCR atomic), Hash (object), List (queue), Set, Sorted Set (leaderboard) การเลือก type ให้ตรงงานคือกุญแจของการใช้ Redis อย่างมีประสิทธิภาพ:
String
SET user:1:name "Anna"
GET user:1:name
SETEX session:abc 3600 "userId:42" # expire 3600s
INCR page:home:views # atomic increment
DECR stock:product:1
APPEND log "new event\n"Hash
HSET user:1 name "Anna" email "a@b.com" age 25
HGET user:1 name
HGETALL user:1
HINCRBY user:1 age 1
HDEL user:1 ageList (queue / stack)
💡 อ่านชื่อ command:
L= Left (หัวลิสต์),R= Right (ท้ายลิสต์),B= Blocking (รอจนกว่าจะมีข้อมูล)
LPUSH= ดันเข้าหัวซ้าย,RPUSH= ดันเข้าท้ายขวาLPOP/RPOP= ดึงออกจากหัว/ท้าย- FIFO (queue):
LPUSH+RPOP(เข้าซ้าย-ออกขวา) — ตัวแรกที่ใส่ออกก่อน- LIFO (stack):
LPUSH+LPOP(เข้าซ้าย-ออกซ้าย) — ตัวล่าสุดที่ใส่ออกก่อน
LPUSH queue:jobs "job1" # ดัน job1 เข้าหัว → [job1]
LPUSH queue:jobs "job2" # ดัน job2 เข้าหัว → [job2, job1]
RPOP queue:jobs # ดึงท้าย → "job1" (FIFO)
LRANGE queue:jobs 0 -1 # all elements
BLPOP queue:jobs 5 # blocking pop (รอสูงสุด 5s)Set
SADD tags:post:1 "javascript" "react"
SMEMBERS tags:post:1
SISMEMBER tags:post:1 "react"
SINTER tags:post:1 tags:post:2 # intersection
SUNION tags:post:1 tags:post:2Sorted Set (ordered)
ZADD leaderboard 100 "alice"
ZADD leaderboard 250 "bob"
ZADD leaderboard 175 "carol"
ZRANGE leaderboard 0 -1 WITHSCORES # ascending
ZREVRANGE leaderboard 0 9 # top 10
ZRANGEBYSCORE leaderboard 100 200 # 100-200 score
ZINCRBY leaderboard 10 "alice" # alice +10
ZRANK leaderboard "alice" # rankStream (queue ที่ persistent)
💡 อ่านสัญลักษณ์พิเศษ:
*= ให้ Redis gen ID อัตโนมัติ (timestamp-based)$= "เริ่มฟังจากตอนนี้เป็นต้นไป" (ข้าม message เก่าทั้งหมด)>= "อ่าน message ที่ยังไม่มีใครใน consumer group นี้รับ"0= "อ่านตั้งแต่ message แรกสุด"
XADD events * type click userId 42 # add — * = auto-id
XREAD COUNT 10 STREAMS events 0 # read — 0 = ตั้งแต่ต้น
XGROUP CREATE events consumer-group $ MKSTREAM # สร้าง group — $ = นับจาก now
XREADGROUP GROUP consumer-group worker1 COUNT 5 STREAMS events > # > = unseen ของ groupใช้แทน Kafka สำหรับ small/medium queue (มี persistence + consumer group + ACK = at-least-once delivery)
14. Expiration + Eviction
จุดเด่นของ Redis คือ key หมดอายุเองได้ (TTL) เหมาะกับ cache/session — และเมื่อ memory เต็ม Redis มี eviction policy ที่เลือกว่าจะลบ key ไหนทิ้ง (LRU/LFU) การตั้ง policy ให้ถูกสำคัญมากตอนใช้เป็น cache:
SET key value EX 60 # expire 60s
SET key value PX 60000 # expire 60000ms
EXPIRE key 60 # set expiration
TTL key # ดูเวลาคงเหลือ
PERSIST key # remove expirationEviction Policy (เมื่อ memory เต็ม)
eviction policy (นโยบายการไล่ข้อมูลออก) = กฎที่ Redis ใช้ตัดสินว่าจะ "ลบ key ไหนทิ้ง" เมื่อ memory เต็ม เช่น LRU (Least Recently Used = ลบตัวที่ไม่ถูกใช้นานสุด), LFU (Least Frequently Used = ลบตัวที่ถูกใช้น้อยสุด)
maxmemory-policy:
noeviction # ห้ามลบ — เมื่อ memory เต็มจะ return error
allkeys-lru # ลบ LRU จากทุก key
allkeys-lfu # ลบ LFU จากทุก key (least frequently used)
allkeys-random # ลบสุ่มจากทุก key
volatile-lru # ลบ LRU เฉพาะ key ที่มี TTL
volatile-lfu # ลบ LFU เฉพาะ key ที่มี TTL
volatile-random # ลบสุ่มเฉพาะ key ที่มี TTL
volatile-ttl # ลบ key ที่ TTL ใกล้หมดอายุที่สุดก่อน💡 หลักง่าย ๆ:
allkeys-*= ลบจากทุก key,volatile-*= ลบเฉพาะ key ที่ตั้ง TTL ไว้แล้วเท่านั้น
15. Pattern: Cache-Aside
📌 หัวข้อ Pattern ถัด ๆ ไป (15-20) มีโค้ด Java/Spring Boot ประกอบ ถ้ายังไม่ได้อ่าน Spring Boot โฟกัสที่คำอธิบาย pattern แล้วข้ามโค้ด Java ได้ — แนวคิด pattern ใช้ได้กับทุกภาษา
cache-aside เป็น pattern caching ที่ใช้บ่อยสุด — อ่านจาก cache ก่อน ถ้า miss ค่อยไป DB แล้ว set กลับ และ invalidate cache เมื่อ update DB จุดที่ต้องระวังคือ "เมื่อไหร่ invalidate" เพราะ cache ที่ไม่ลบจะค้างค่าเก่า:
java
// แนวทาง 1: ใช้ RedisTemplate (Spring Data Redis) — serialize ให้อัตโนมัติ ไม่ต้องจัด JSON เอง
public User getUser(Long id) {
String key = "user:" + id;
// 1. Check cache
User cached = (User) redisTemplate.opsForValue().get(key);
if (cached != null) return cached;
// 2. Cache miss → DB
User user = userRepo.findById(id).orElseThrow();
// 3. Set cache (TTL 5 นาที)
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(5));
return user;
}
public void updateUser(User user) {
userRepo.save(user);
redisTemplate.delete("user:" + user.getId()); // ⭐ invalidate
}java
// แนวทาง 2: ถ้าจัด JSON เองด้วย ObjectMapper — ต้อง handle JsonProcessingException (checked)
public User getUserManualJson(Long id) throws JsonProcessingException {
String key = "user:" + id;
String cached = redis.get(key);
if (cached != null) return objectMapper.readValue(cached, User.class);
User user = userRepo.findById(id).orElseThrow();
// setex/writeValueAsString = throws JsonProcessingException → ต้อง throws หรือ try/catch
redis.setex(key, 300, objectMapper.writeValueAsString(user));
return user;
}16. Pattern: Rate Limiter
INCR ของ Redis เป็น atomic จึงเหมาะทำ rate limiter ที่แชร์ข้าม instance — นับ request ต่อ user ต่อหน้าต่างเวลา แล้วปฏิเสธเมื่อเกิน เป็น pattern ที่ทั้งง่ายและถูกต้องแม้มีหลาย server:
java
public boolean tryAcquire(String userId) {
String key = "rate:" + userId + ":" + (System.currentTimeMillis() / 60000);
Long count = redis.incr(key);
if (count == 1) redis.expire(key, 60);
return count <= 100; // 100 req/min
}17. Pattern: Distributed Lock
เมื่อมีหลาย instance แต่อยากให้แค่ตัวเดียวทำงานบางอย่าง (เช่น cron job) — distributed lock ผ่าน Redis (SET NX EX) ช่วยได้ จุดสำคัญคือ release lock ต้องเช็คว่า "เราเป็นเจ้าของจริง" ด้วย Lua script (atomic) เพื่อกันลบ lock ของคนอื่น production ควรใช้ Redisson:
java
// ใช้ Spring Data Redis (RedisTemplate) — pattern ที่ใช้บ่อยใน Spring Boot
String lockKey = "lock:job:42";
String lockValue = UUID.randomUUID().toString();
// Try acquire (atomic SET NX EX) — return true ถ้าได้ lock, false ถ้าไม่ได้
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
if (Boolean.TRUE.equals(acquired)) {
try {
// ทำ critical section
} finally {
// Release lock แบบ atomic ผ่าน Lua script
// KEYS[1] = key อันแรกที่ส่งให้ script (lockKey)
// ARGV[1] = arg อันแรก (lockValue ที่เราเป็นเจ้าของ)
// Lua script รันใน Redis แบบ atomic — เช็คกับลบเป็น operation เดียว กัน race
String script =
"if redis.call('get',KEYS[1])==ARGV[1] " +
"then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
List.of(lockKey), lockValue
);
}
}💡 ทำไมต้อง Lua script ตอน release? ถ้าใช้
getแล้วdelแยกกัน — ระหว่างนั้น lock อาจ expire และคนอื่นได้ lock ใหม่ → เราจะลบ lock ของเขาผิด ๆ Lua script ทำให้ "เช็ค + ลบ" เป็น atomic operation เดียว🏗️ Production — แนะนำใช้ Redisson library — handle edge cases (lock renewal/watchdog, reentrant, fair lock) ให้หมด ไม่ต้องเขียน Lua เอง
18. Pattern: Session Store
ตอน scale แอปหลาย instance HTTP session ที่เก็บใน memory ของแต่ละ server ทำให้ user ต้อง login ใหม่เมื่อโดน route ไปคนละ instance — เก็บ session ใน Redis แทน ทำให้ทุก instance แชร์ session กันได้ Spring Session ตั้งค่าแค่ไม่กี่บรรทัด:
xml
<!-- ต้องเพิ่ม dependency นี้ก่อน — ไม่งั้น property ด้านล่างจะถูกเพิกเฉย -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>properties
# application.properties
spring.session.store-type=redis
spring.data.redis.host=localhost→ Session ของ user เก็บใน Redis แทน HTTP session
→ multi-instance app share session ได้
19. Pub/Sub
Redis ทำ pub/sub แบบเบา ๆ ได้ — publisher ส่งข้อความเข้า channel แล้ว subscriber ที่ฟังอยู่รับทันที เหมาะกับ real-time notification ข้าม instance ⚠️ แต่ไม่ persistent (subscriber ที่ไม่ออนไลน์ตอน publish จะพลาด) ถ้าต้องการความทนทานใช้ Redis Streams หรือ Kafka:
# Publisher
PUBLISH channel:news "Breaking news!"
# Subscriber
SUBSCRIBE channel:newsjava
// Spring Data Redis
@Component
public class NewsListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("Got: " + new String(message.getBody()));
}
}💡 Pub/Sub vs Streams (สำคัญ!):
ด้าน Pub/Sub Streams (XADD/XREAD) persistence ❌ ไม่เก็บ — fire-and-forget ✅ เก็บใน log (replay ได้) delivery at-most-once at-least-once (มี ACK) consumer group ❌ ไม่มี ✅ มี (Kafka-like) ใช้กับ real-time broadcast ที่หายได้ (chat presence) งานต้อง guarantee (order, payment, job) สรุป: Pub/Sub = ไฟ-แล้ว-ลืม, Streams = log + consumer group + ACK — งานที่ต้อง guarantee ให้ใช้ Streams
20. Spring Boot + Redis
ปิดท้ายด้วยการใช้ Redis ใน Spring Boot — starter data-redis ให้ RedisTemplate สำหรับสั่งงานทุก data type และ @Cacheable/@CacheEvict ที่ทำ caching แบบ declarative (แค่แปะ annotation ไม่ต้องเขียน cache logic เอง) ตามที่ใช้ใน pattern ก่อนหน้า:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository repo;
private final RedisTemplate<String, Object> redis;
@Cacheable(value = "users", key = "#id")
public User getById(Long id) {
return repo.findById(id).orElseThrow();
}
@CacheEvict(value = "users", key = "#user.id")
public User update(User user) {
return repo.save(user);
}
}@Cacheable + @CacheEvict = magic — Spring จัดการ cache-aside ให้
21. ⚠️ Redis Pitfalls
| Pitfall | แก้ |
|---|---|
| Cache stale ตอน data เปลี่ยน | invalidate ตอน write |
| Cache stampede (สแตม-พีด = "ฝูงวิ่งพรวด") — พอ cache key หมดอายุพร้อมกัน request จำนวนมากพรวดเข้า DB จน DB ล้ม | jitter TTL: ttl = 300 + random(0..60) วินาที → key หมดอายุไม่พร้อมกัน ลด stampede ได้มาก หรือใช้ lock + single-flight |
| Memory เต็ม | maxmemory + LRU |
| Key ที่ไม่ expire → memory leak | ใส่ TTL เสมอ |
| Use Redis as primary DB | persistence + replication required |
KEYS * ใน production | blocking — ใช้ SCAN |
| Big key (1 GB hash) | split |
22. Decision: SQL vs MongoDB vs Redis
| Scenario | เลือก |
|---|---|
| User + Order + Product (CRUD + relation) | PostgreSQL |
| Catalog ที่ spec ต่างกัน + nested | MongoDB (หรือ Postgres + JSONB) |
| User session, rate limit, cache | Redis |
| Real-time chat, social feed | MongoDB |
| Time-series log/metric | TimescaleDB / InfluxDB v3 / ClickHouse |
| Full-text search | Elasticsearch หรือ Postgres FTS |
| Graph (friend of friend) | Neo4j หรือ Postgres recursive CTE |
| Vector / embedding (RAG, AI app) | pgvector / Qdrant / Pinecone / Turbopuffer / Weaviate / Milvus / LanceDB |
| Write-heavy / IoT log | Cassandra 5.0 / ScyllaDB |
| Distributed SQL | CockroachDB 24.x / TiDB 8.x / Spanner |
| Serverless key-value | DynamoDB / FoundationDB |
กฎทอง: เริ่ม PostgreSQL เสมอ → เพิ่ม Redis เป็น cache → เพิ่มอื่นเมื่อมี use case ชัด
23. Polyglot Persistence
App ใหญ่ ๆ ใช้ DB หลายตัวพร้อมกัน:
แต่ละตัวทำสิ่งที่ตัวเองดีที่สุด — แต่ระวัง consistency
24. Checkpoint
🛠️ Checkpoint 8.1 — MongoDB CRUD
Setup MongoDB → สร้าง collection posts (title, body, tags, author, comments embedded)
- Insert 20 posts
- Query: posts ที่มี tag "tech"
- Update: เพิ่ม comment ใน post
- Aggregation: count post per tag
🛠️ Checkpoint 8.2 — Redis Cache
Spring Boot + Redis:
getUser(id)→ cache 5 นาทีupdateUser(user)→ invalidate- ทดสอบ EXPLAIN ก่อน/หลัง cache
🛠️ Checkpoint 8.3 — Rate Limiter
ทำ rate limiter middleware ที่ใช้ Redis:
- 100 req / minute / IP
- ตอบ 429 +
X-RateLimit-Remainingheader
🛠️ Checkpoint 8.4 — Compare
จากบทที่ 7 — ทำ "Product Catalog with flexible attributes":
- Implementation 1: PostgreSQL + JSONB
- Implementation 2: MongoDB
- Compare: ease of code, performance, schema evolution
25. Glossary (ศัพท์ท้ายบท)
| ศัพท์ | คำอ่าน/คำแปล | ความหมายสั้น |
|---|---|---|
| NoSQL | โน-เอส-คิว-แอล | กลุ่ม DB ที่ไม่ใช่ relational — แบ่งเป็น document/key-value/wide-column/graph/time-series/vector |
| MongoDB | มอง-โก-ดี-บี | document database — เก็บ JSON/BSON |
| Cassandra | แคส-แซน-ดรา | wide-column DB, tunable CL, AP-ish default |
| ScyllaDB | สกิล-ลา-ดี-บี | Cassandra-compatible เขียนใหม่ด้วย C++ |
| Redis | เร-ดิส | in-memory key-value store |
| Valkey | แวล-คีย์ | fork ของ Redis (BSD license) หลัง relicense 2024 |
| DynamoDB | ไดนา-โม-ดี-บี | managed key-value/wide-column ของ AWS |
| BSON | บี-สัน | binary JSON format ของ MongoDB |
| TTL | Time To Live = อายุ key ก่อนถูกลบอัตโนมัติ | ใช้ใน Redis + MongoDB TTL index |
| LRU | Least Recently Used | นโยบายลบตัวที่ไม่ถูกใช้นานสุด |
| LFU | Least Frequently Used | นโยบายลบตัวที่ถูกใช้น้อยสุด (นับความถี่) |
| Cache stampede | สแตม-พีด ("ฝูงวิ่งพรวด") | request แห่เข้า DB ตอน cache หมดอายุพร้อมกัน |
| Sharding | ชาร์ด-ดิง | แบ่งข้อมูลออกหลาย node ตาม key |
| Replica set | เร-พลิ-คา-เซ็ต | กลุ่ม node ที่ copy ข้อมูลให้กัน (MongoDB) |
| writeConcern | คอน-เซิร์น (ความสนใจ) | ระดับการยืนยันว่าเขียนสำเร็จ (majority = default ตั้งแต่ MongoDB 5.0) |
| readConcern | คอน-เซิร์น | ระดับ isolation ของการอ่าน (local, majority, snapshot) |
| CL (Consistency Level) | ระดับความสม่ำเสมอ | knob ของ Cassandra: ONE/QUORUM/ALL |
| LWT | Lightweight Transaction | Paxos-based compare-and-set ใน Cassandra |
| Pub/Sub | ผับ-สับ | publish/subscribe — fire-and-forget messaging |
| at-most-once / at-least-once | การันตีการส่งครั้งมากสุด/น้อยสุด | Pub/Sub = at-most, Streams = at-least |
26. สรุปบท
✅ MongoDB = document store — flexible schema, embed nested data
✅ Aggregation pipeline = MongoDB's GROUP BY
✅ Schema design: embed (1-few) vs reference (M-M)
✅ Redis = in-memory key-value — cache, session, queue, rate limit, lock
✅ Data types: String, Hash, List, Set, Sorted Set, Stream
✅ Cache-aside pattern + invalidation strategy
✅ Distributed lock + rate limiter common pattern
✅ PostgreSQL ก่อน — เพิ่ม Redis cache → เพิ่มอื่นเมื่อจำเป็น
✅ Polyglot persistence: ใช้แต่ละ DB ตามจุดเด่น
🎉 จบ Database Book
หลังจากบทที่ 0-8 คุณมี:
- SQL พื้นฐาน + JOIN + Aggregation + Subquery + CTE + Window
- Index + Performance tuning + EXPLAIN
- Transaction + ACID + Isolation + Lock + MVCC
- Schema Design + Normalization + Migration
- PostgreSQL features ลึก (JSONB, FTS, Partition, Extensions)
- NoSQL (MongoDB, Redis) + เลือกถูก
หัวข้อต่อยอด
| ทำอะไร | |
|---|---|
| TimescaleDB | time-series database (extension Postgres) |
| ClickHouse | OLAP / data warehouse |
| CockroachDB | distributed SQL |
| Kafka + CDC | event-driven + change data capture |
| Sharding | horizontal scaling |
| Read replica + load balancing | scale read |
| Database design pattern | CQRS, event sourcing |