Skip to content

บทที่ 8 — NoSQL Intro (MongoDB + Redis)

← บทที่ 7 | สารบัญ

หลังจบบท คุณจะ:

  • เข้าใจ 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) ที่ใช้บ่อยสุด:

ประเภทตัวอย่างใช้เมื่อ
DocumentMongoDB (มอง-โก-ดี-บี), CouchDBflexible schema, nested data
Key-ValueRedis (เร-ดิส), DynamoDB (ไดนา-โม-ดี-บี), Memcachedcache, session, counter
Wide-ColumnCassandra (แคส-แซน-ดรา), ScyllaDB (สกิล-ลา-ดี-บี), HBasewrite-heavy, time-series, IoT
GraphNeo4j (นี-โอ-โฟร์-เจ), Dgraph (ดี-กราฟ)relationship-heavy (social, recommend)
Time-SeriesTimescaleDB, InfluxDB v3, QuestDBmetrics, logs, IoT sensor
Vectorpgvector, Qdrant, Pinecone, Weaviate, Milvus, Turbopuffer, LanceDBembedding 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 nestedrow + dynamic columns ต่อ partitionคู่ key→valuenode + edge
จุดเด่นschema ยืดหยุ่น, query ลึกได้write throughput สูง, scale แนวนอนlatency ต่ำมาก (in-memory)ค้นความสัมพันธ์ลึก
ใช้กับcatalog, user profile, contentlog, time-series, IoTcache, session, countersocial, 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:

SQLMongoDBหมายเหตุ
DatabaseDatabaseเหมือนกัน
TableCollectionกลุ่มของ document
RowDocument1 record = 1 JSON object
ColumnFieldแต่ field ซ้อนกันลึก ๆ ได้ (nested object/array) — SQL column เป็น scalar
Primary Key_idMongoDB สร้าง _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 secret

GUI 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อยู่ใน / ไม่อยู่ใน listIN / NOT IN
Logical$or / $and / $notต่อเงื่อนไขOR / AND / NOT
Element$existsfield มีอยู่หรือไม่IS NULL / IS NOT NULL
Array$allarray มีครบทุกค่า(ไม่มีตรง ๆ)
Array$elemMatchelement ใน array ตรงเงื่อนไข(ต้อง subquery)
Update$setตั้งค่า fieldSET col = ?
Update$incเพิ่ม/ลดตัวเลข atomicSET col = col + ?
Update$push / $pullเพิ่ม/ลบ element ใน array(ไม่มีตรง ๆ)
Search$regexmatch ด้วย regexLIKE / 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ทำอะไร
$matchfilter (like WHERE)
$groupgroup + aggregate
$sortsort
$limit / $skippagination
$projectselect column + transform
$lookupLEFT JOIN
$unwindarray → multiple docs
$addFieldsadd computed field
$countcount 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) ส่วน readConcern default ยังเป็น 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
ไม่ใส่ indexindex จำเป็น (เหมือน SQL)
Transaction ทุกอย่างatomic op ใน single doc ก่อน
$lookup ทุก querydenormalize ถ้า 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-cli

13. 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 age

List (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:2

Sorted 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"                         # rank

Stream (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 expiration

Eviction 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:news
java
// 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/SubStreams (XADD/XREAD)
persistence❌ ไม่เก็บ — fire-and-forget✅ เก็บใน log (replay ได้)
deliveryat-most-onceat-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 DBpersistence + replication required
KEYS * ใน productionblocking — ใช้ SCAN
Big key (1 GB hash)split

22. Decision: SQL vs MongoDB vs Redis

Scenarioเลือก
User + Order + Product (CRUD + relation)PostgreSQL
Catalog ที่ spec ต่างกัน + nestedMongoDB (หรือ Postgres + JSONB)
User session, rate limit, cacheRedis
Real-time chat, social feedMongoDB
Time-series log/metricTimescaleDB / InfluxDB v3 / ClickHouse
Full-text searchElasticsearch หรือ 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 logCassandra 5.0 / ScyllaDB
Distributed SQLCockroachDB 24.x / TiDB 8.x / Spanner
Serverless key-valueDynamoDB / 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-Remaining header

🛠️ 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
TTLTime To Live = อายุ key ก่อนถูกลบอัตโนมัติใช้ใน Redis + MongoDB TTL index
LRULeast Recently Usedนโยบายลบตัวที่ไม่ถูกใช้นานสุด
LFULeast 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
LWTLightweight TransactionPaxos-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) + เลือกถูก

หัวข้อต่อยอด

ทำอะไร
TimescaleDBtime-series database (extension Postgres)
ClickHouseOLAP / data warehouse
CockroachDBdistributed SQL
Kafka + CDCevent-driven + change data capture
Shardinghorizontal scaling
Read replica + load balancingscale read
Database design patternCQRS, event sourcing

← บทที่ 7 | ← สารบัญ Database | ← สารบัญหลัก