နားလည်ထားရမယ့် အချက်
ဒီပရောဂျက်ဟာ ယခင်သင်ခန်းစာသုံးခုကို တစ်ပြိုင်နက်တည်း အသုံးချစေတဲ့ design exercise တစ်ခုဖြစ်ပါတယ် - entity တစ်ခုချင်းစီ ဘယ်လိုချိတ်ဆက်မလဲဆိုတာ ဆုံးဖြတ်ဖို့ tables၊ keys နှင့် relationships; data ပွားများခြင်းမရှိအောင် ထိန်းသိမ်းဖို့ constraints နှင့် normalization; design က query လိုအပ်ချက်တွေကို တကယ်ဖြေရှင်းနိုင်ရဲ့လား စစ်ဆေးဖို့ schema diagram နှင့် access pattern တို့ ပါဝင်ပါတယ်။
Thuta Learning ကဲ့သို့ သင်ယူရေးပလက်ဖောင်းတစ်ခုက ဒီ tool အားလုံးကို လိုအပ်လောက်အောင် data model ကြွယ်ဝပေမယ့် တစ်ပြိုင်နက်တည်း စဉ်းစားနိုင်လောက်လောက် ငယ်တာကြောင့် ရွေးချယ်ထားခြင်းဖြစ်ပါတယ်။
- Users - platform အသုံးပြုသူများ
- Courses - သင်တန်းများ
- Lessons - သင်ခန်းစာများ
- Enrollments - user ↔ course ချိတ်ဆက်ချက်
- Progress - user ↔ lesson ပြီးစီးမှု
- Bookmarks - user သိမ်းထားသော lesson
Lesson တစ်ခုက Course တစ်ခုကို foreign key နဲ့ ပိုင်ဆိုင်တာကြောင့် lesson row တိုင်းမှာ course_id တစ်ခု ပါဝင်ပြီး title ကို ထပ်ခါထပ်ခါ မရေးဘဲ ဖြစ်စေပါတယ် - ဒါက normalization သင်ခန်းစာက သတိပေးထားတဲ့ ပွားများမှုအမျိုးအစားပါပဲ။
Enrollments နှင့် Progress တို့က Users ကို Courses နှင့် Users ကို Lessons နှင့် ချိတ်ဆက်ပေးတဲ့ join-style table တွေဖြစ်ပြီး၊ ယခင် relationships သင်ခန်းစာမှာ မိတ်ဆက်ခဲ့တဲ့ many-to-many pattern အတိုင်းပါပဲ။
Unique constraint က ဘာလုပ်ပေးလဲ
Enrollments ထဲက (user_id, course_id) အပေါ် unique constraint တစ်ခုဟာ constraints သင်ခန်းစာကနေ တိုက်ရိုက်ယူထားတာဖြစ်ပါတယ် - user တစ်ဦးကို course တစ်ခုထဲကို နှစ်ကြိမ် enroll မလုပ်နိုင်အောင် database ကိုယ်တိုင်က တားဆီးပေးပါတယ်။
Bookmarks ကလည်း အလားတူ shape ကို ပြန်သုံးထားပြီး user တစ်ဦးနှင့် သူပြန်ကြည့်ချင်တဲ့ lesson တစ်ခုကို ချိတ်ဆက်ပေးပါတယ်။
Schema တစ်ခုကို ပြီးပြည့်စုံပြီလို့ မဆိုမီ real access pattern တွေနှင့် တိုက်စစ်ရပါမယ် - စက္ကူပေါ်မှာ သန့်ရှင်းနေပေမယ့် app က တကယ်မေးမယ့် မေးခွန်းတွေကို ဖြေမပေးနိုင်ရင် ဒီ schema က မပြီးသေးပါဘူး။
LEARNING PLATFORM SCHEMA
------------------------
LEARNING PLATFORM SCHEMA
-------------------------
USERS
user_id PK
name
email
COURSES
course_id PK
title
LESSONS
lesson_id PK
course_id FK -> COURSES.course_id
title
ENROLLMENTS
enrollment_id PK
user_id FK -> USERS.user_id
course_id FK -> COURSES.course_id
UNIQUE(user_id, course_id)
PROGRESS
progress_id PK
user_id FK -> USERS.user_id
lesson_id FK -> LESSONS.lesson_id
completed
BOOKMARKS
bookmark_id PK
user_id FK -> USERS.user_id
lesson_id FK -> LESSONS.lesson_id
RELATIONSHIPS
COURSES 1---* LESSONS
USERS 1---* ENROLLMENTS *---1 COURSES
USERS 1---* PROGRESS *---1 LESSONS
USERS 1---* BOOKMARKS *---1 LESSONSလက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
SQL syntax ကို တန်းရိုက်မယ့်အစား ဒီအစီအစဉ်အတိုင်း သင်ယူရေးပလက်ဖောင်း schema ကို တည်ဆောက်ကြည့်ပါ -
Entity များကို စာရင်းလုပ်ပါ
App ပြောနေတဲ့ noun ခြောက်ခု - User, Course, Lesson, Enrollment, Progress, Bookmark - ကို ရေးချပါ။ တစ်ခုစီက table တစ်ခုစီ ဖြစ်လာမည်။
Primary key ရွေးပါ
Entity တစ်ခုစီကို ရိုးရှင်းသော auto-incrementing id ပေးပါ - user_id, course_id, lesson_id အစရှိသည်ဖြင့်။
Foreign key များ ချိတ်ဆက်ပါ
Lessons မှာ course_id FK ရှိမည်။ Enrollments မှာ user_id နှင့် course_id နှစ်ခုလုံး ရှိမည်။ Progress နှင့် Bookmarks တို့တွင် user_id နှင့် lesson_id နှစ်ခုလုံး ရှိမည်။
အရေးကြီးသော constraint ထည့်ပါ
Enrollments (user_id, course_id) အပေါ် unique index တစ်ခုထည့်ပြီး enrollment တစ်ခုတည်းကို ထပ်ခါထပ်ခါ insert မလုပ်နိုင်အောင် လုပ်ပါ။
Access pattern များနှင့် တိုက်စစ်ပါ
Access pattern တစ်ခုစီအတွက် ဘယ် table၊ ဘယ် key က ဖြေနိုင်လဲ နာမည်တပ်ပါ။ key path မရှိရင် schema မှားနေခြင်းဖြစ်သည်။
Draft အများစု ဒီနေရာမှာ မှားတတ်သည်
Noun တွေကို မှန်ကန်စွာ model လုပ်ပြီးသားဖြစ်ပေမယ့် homepage က load တိုင်း အမှန်တကယ် ဖြေဆိုပေးရမယ့် မေးခွန်းကို မေ့နေတတ်ပါတယ်။
အတူတူ စမ်းရေးကြည့်မယ်
const schema = {
Users: { pk: "user_id", fields: ["user_id", "name", "email"] },
Courses: { pk: "course_id", fields: ["course_id", "title"] },
Lessons: {
pk: "lesson_id",
fields: ["lesson_id", "course_id", "title"],
fk: [{ field: "course_id", references: "Courses.course_id" }]
},
Enrollments: {
pk: "enrollment_id",
fields: ["enrollment_id", "user_id", "course_id"],
fk: [
{ field: "user_id", references: "Users.user_id" },
{ field: "course_id", references: "Courses.course_id" }
],
unique: [["user_id", "course_id"]]
},
Progress: {
pk: "progress_id",
fields: ["progress_id", "user_id", "lesson_id", "completed"],
fk: [
{ field: "user_id", references: "Users.user_id" },
{ field: "lesson_id", references: "Lessons.lesson_id" }
]
},
Bookmarks: {
pk: "bookmark_id",
fields: ["bookmark_id", "user_id", "lesson_id"],
fk: [
{ field: "user_id", references: "Users.user_id" },
{ field: "lesson_id", references: "Lessons.lesson_id" }
]
}
};
const accessPatterns = [
{ name: "Get all lessons for a course", table: "Lessons", via: "course_id" },
{ name: "Get a user's progress across all courses", table: "Progress", via: "user_id" },
{ name: "Get all courses a user is enrolled in", table: "Enrollments", via: "user_id" },
{ name: "Get all bookmarks for a user", table: "Bookmarks", via: "user_id" },
{ name: "Check if a user is already enrolled in a course", table: "Enrollments", via: "user_id+course_id" }
];
function checkAccessPattern(pattern) {
const table = schema[pattern.table];
if (!table) return { pattern: pattern.name, supported: false, reason: "table not found" };
if (pattern.via === "user_id+course_id") {
const hasUnique = table.unique && table.unique.some(
(u) => u.includes("user_id") && u.includes("course_id")
);
return {
pattern: pattern.name,
supported: !!hasUnique,
reason: hasUnique
? "supported via unique constraint on (user_id, course_id)"
: "no supporting constraint"
};
}
const fkFields = (table.fk || []).map((f) => f.field);
const supported = fkFields.includes(pattern.via) || table.fields.includes(pattern.via);
return {
pattern: pattern.name,
supported,
reason: supported
? `supported via foreign key "${pattern.via}" on ${pattern.table}`
: "no supporting key"
};
}
accessPatterns.forEach((p) => {
const result = checkAccessPattern(p);
console.log(`${result.supported ? "OK" : "MISSING"} - ${result.pattern}: ${result.reason}`);
});
OK - Get all lessons for a course: supported via foreign key "course_id" on Lessons
OK - Get a user's progress across all courses: supported via foreign key "user_id" on Progress
OK - Get all courses a user is enrolled in: supported via foreign key "user_id" on Enrollments
OK - Get all bookmarks for a user: supported via foreign key "user_id" on Bookmarks
OK - Check if a user is already enrolled in a course: supported via unique constraint on (user_id, course_id)၅ မိနစ် စမ်းကြည့်
Certificates table တစ်ခု ထပ်ထည့်ပါ - user တစ်ဦးသည် course တစ်ခုရှိ lesson အားလုံးကို completed ဖြစ်သွားလျှင် certificate ရရှိသည်ဟု ယူဆပါ။ ၎င်းရဲ့ primary key၊ foreign key များကို ဆုံးဖြတ်ပြီး access-pattern list ထဲသို့ ထည့်ကာ 'user တစ်ဦး၏ certificate အားလုံး ရယူခြင်း' ဆိုတဲ့ pattern အသစ်ကို validation function နှင့် ပြန်စစ်ကြည့်ပါ။
သတိလေးတစ်ချက်
App က တကယ်ဘယ်လို query တွေ လုပ်မယ်ဆိုတာ မစစ်ဆေးမီ table တွေကို ဒီဇိုင်းဆွဲလိုက်ပြီး၊ နောက်မှ report အရေးကြီးတစ်ခုက unindexed join သုံးခု လိုအပ်နေကြောင်း တွေ့ရသည်
Enrollments ပေါ်ရှိ unique constraint ကို ကျော်ပြီး duplicate enrollment ကို app code ကသာ ကာကွယ်ဖို့ မှီခိုထားခြင်း - request နှစ်ခု race ဖြစ်တာနှင့် ချက်ချင်း ပျက်စီးသွားနိုင်သည်
PostgreSQL Documentation: Constraints — How Databases Work