Ачааллж байна...
Өмнөх хичээлд номын санч Дулмаагийн "бүгдийг нэг дор" хүснэгт засах, нэмэх, устгах гэсэн өдөр тутмын гурван үйлдэлд гурван өөр аргаар эвдэрсэн. Бид шалтгааныг нь олсон: өөр өөр амьдралтай "зүйл"-үүд — гишүүд, номнууд, зээлүүд — нэг мөрөнд холилдсон. Одоо асуулт нь: хүснэгт зохиохдоо ийм алдаанаас яаж урьдчилан сэргийлэх вэ? Хүснэгт бүрийг зохиосныхоо дараа эвдэрч байгааг нь хүлээх биш, анхнаас нь зөв зохиох арга хэрэгтэй.
Энэ аргачлалыг normalization гэдэг. Их сургуулийн сурах бичгүүд үүнийг "нэгдүгээр хэвийн хэлбэр", "хоёрдугаар хэвийн хэлбэр", "гуравдугаар хэвийн хэлбэр" гэх мэт дугаарласан, албан ёсны тодорхойлолтоор заадаг. Тэдгээр тодорхойлолт үнэн зөв боловч эхлэгчдэд хийсвэр санагддаг. Энэ хичээлд бид тэдгээрийн практик утгыг гурван энгийн өгүүлбэр болгон сурна. Дүрэм бүрийг Дулмаагийн хүснэгт дээр хэрэглэж, хүснэгт алхам алхмаар засагдахыг харна.
Хичээлийн төгсгөлд шударга нэг сэдвийг хөндөнө: normalization-ийг хэзээ зогсоох вэ? Мэргэжлийн системүүд заримдаа давхардлыг санаатайгаар хадгалдаг — таны ашиглаж буй энэ платформ ч гэсэн. Ингэх нь хэзээ зөв, хэзээ аюултай болохыг ялгаж сурна.
Гэрийн эд зүйлсээ хадгалахыг бодъё. Хамгийн муу арга нь бүх юмыг нэг том хайрцагт хийх — оймс, хайч, баримт бичиг холилдоно, нэгийг хайхад бүгдийг ухна. Эмх цэгцтэй хүн гурван дүрэм баримталдаг. Нэгдүгээрт, нэг тасалгаанд нэг л зүйл — хоёр оймсыг нэг уутанд хийгээд "оймс" гэж шошголохгүй. Хоёрдугаарт, ижил төрлийн зүйлс нэг шургуулганд — оймсны шургуулганд хайч байхгүй. Гуравдугаарт, хэрэгтэй үедээ тоолж болох зүйлийн тоог тусад нь бичиж хадгалахгүй — "шургуулганд 12 хос оймс бий" гэсэн цаас наавал нэг хос алга болох өдөр тэр цаас худал болно.
Эдгээр гурван дүрэм өгөгдлийн сангийн гурван дүрэмтэй бараг үг үсгээрээ таарна. Normalization бол өгөгдлийн хувьд эмх цэгцтэй хүний зуршил юм.
Өмнөх хичээлийн loan_log хүснэгтийг дахин бүтээгээрэй: тэр хичээлийн CREATE TABLE болон INSERT скриптийг library.db дээр ажиллуулна (энэ хичээлийн турш Write Changes дарахгүй). Хүснэгт өмнөх хичээлийн эхэн дэх зургаан мөртэйгөө байх ёстой. Харин Дулмаагийн өмнөх хувилбарыг төсөөлөөд эхэлье — тэр эхлээд бүр ч муу хүснэгттэй байсан.
Дулмаа анх хүснэгтээ гишүүн бүрт нэг мөр байхаар зохиосон бөгөөд зээлсэн номнуудыг нь нэг нүдэнд жагсаасан:
member_name | member_email | borrowed_books
------------+-------------------+------------------------------------------------
Бат | bat@example.com | Clean Code, Python Crash Course, Тунгалаг Тамир
Сараа | saraa@example.com | Тунгалаг Тамир, Clean Code
Номин | nomin@example.com | Clean CodeЭнэ бол Бүлэг 5-ийн нэг-олон харилцааны хичээлийн "Буруу хувилбар 2" — нэг нүдэнд жагсаалт. Тэнд бид энэ хэлбэрийн гурван асуудлыг харсан: FOREIGN KEY шалгах боломжгүй, LIKE '%Code%' гэх мэт хайлт буруу мөр барьдаг, нэг ном нэмэх эсвэл хасах нь текстийг задалж буцааж бичих болдог. Түүнээс гадна “Clean Code хэдэн удаа зээлэгдсэн бэ” гэдэг энгийн асуултад SQL-ийн хэрэгслүүд бараг хариулж чадахгүй.
Зарим хүн энэ асуудлаас "зугтах" гэж баганыг давтдаг: book_1, book_2, book_3. Энэ нь жагсаалтыг гурван нүдэнд хуваасан мэт харагддаг ч мөн чанар нь ижил — дөрөв дэх ном ирэхэд шинэ багана нэмэх хэрэгтэй болно, “Clean Code-ийг хэн зээлсэн бэ” гэж асуухад гурван баганыг бүгдийг шалгах ёстой. Тиймээс дүрмийн бүрэн хэлбэр нь: нүд бүрт ганц утга, мөн ижил утгын давталт баганаар биш, мөрөөр илэрхийлэгдэнэ.
Засвар нь зээл бүрийг тусдаа мөр болгох юм — яг Дулмаагийн дараагийн хувилбар болох loan_log. Тэр хүснэгт Дүрэм 1-ийг бүрэн хангадаг: нүд бүрт ганц утга. Гэвч өмнөх хичээлд харсанчлан энэ нь хангалтгүй байсан. Сурах бичгийн хэлээр Дүрэм 1 бол нэгдүгээр хэвийн хэлбэр (1NF) юм.
loan_log-ийн мөр бүр зээлийг илэрхийлдэг — loan_id түүний түлхүүр. Одоо багана бүрээс асууя: "чи энэ зээлийн тухай юу?" Огноо — тийм, энэ зээл хэзээ болсныг хэлж байна. Гэвч member_city? Энэ нь зээлийн биш, гишүүний тухай мэдээлэл: Бат хаана ч ном зээлсэн, тэр Улаанбаатарт амьдардаг. book_author? Номын тухай: хэн зээлсэн ч Clean Code-ийг Robert C. Martin бичсэн. Мөрийн түлхүүрийн тухай биш, өөр "зүйл"-ийн тухай багана бүр давхардлын эх үүсвэр — учир нь тэр "зүйл" хэдэн мөрөнд гарч ирнэ, төдий удаа хуулагдана.
Тиймээс дүрэм нь: мөр бүр ганц "зүйл"-ийн тухай; багана бүр тэр зүйлийн өөрийнх нь шинж чанар байх ёстой. Өөр зүйлд хамаарах баганууд өөрийн хүснэгтэд шилжиж, хуучин хүснэгтэд зөвхөн тэр хүснэгт рүү заасан FOREIGN KEY үлдэнэ. Сурах бичгийн хоёрдугаар ба гуравдугаар хэвийн хэлбэрийн (2NF, 3NF) гол санаа яг энэ бөгөөд мэргэжилтнүүд үүнийг "багана бүр түлхүүрийн тухай, бүхэл түлхүүрийн тухай, зөвхөн түлхүүрийн тухай байх ёстой" гэж томъёолдог.
Одоо loan_log-ийг гурван "зүйл" болгон хуваая. Эхлээд гишүүд — давхардалгүй гишүүн бүр нэг мөр:
sql
CREATE TABLE members_n (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
email TEXT
);
INSERT INTO members_n (name, city, email)
SELECT DISTINCT member_name, member_city, member_email
FROM loan_log
ORDER BY member_name;Энэ тушаал юу гэж хэлж байна вэ гэвэл: “members_n хүснэгт үүсгэ; дараа нь loan_log-оос гишүүний нэр, хот, имэйлийн давхардалгүй хослолуудыг нэрээр эрэмбэлэн авч, members_n руу хуул.” Бүлэг 2-ын DISTINCT энд гол ажлыг хийлээ: зургаан мөрөөс Батын гурван, Сараагийн хоёр хуулбарыг нэг болгон нэгтгэв. Үр дүн:
id | name | city | email
---+-------+-------------+------------------
1 | Бат | Улаанбаатар | bat@example.com
2 | Номин | Эрдэнэт | nomin@example.com
3 | Сараа | Дархан | saraa@example.comДараа нь номнууд:
sql
CREATE TABLE books_n (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT
);
INSERT INTO books_n (title, author)
SELECT DISTINCT book_title, book_author
FROM loan_log
ORDER BY book_title;Энэ тушаал юу гэж хэлж байна вэ гэвэл: “books_n хүснэгт үүсгэж, loan_log-оос номын нэр ба зохиогчийн давхардалгүй хослолуудыг хуул.” Үр дүн:
id | title | author
---+---------------------+-----------------
1 | Clean Code | Robert C. Martin
2 | Python Crash Course | Eric Matthes
3 | Тунгалаг Тамир | Ч.ЛодойдамбаЭцэст нь зээлүүд — зөвхөн хоёр заалт ба огноо:
sql
CREATE TABLE loans_n (
id INTEGER PRIMARY KEY,
member_id INTEGER NOT NULL,
book_id INTEGER NOT NULL,
loaned_on TEXT NOT NULL,
FOREIGN KEY (member_id) REFERENCES members_n (id),
FOREIGN KEY (book_id) REFERENCES books_n (id)
);
INSERT INTO loans_n (member_id, book_id, loaned_on)
VALUES
(1, 1, '2026-09-03'),
(1, 2, '2026-09-10'),
(3, 3, '2026-09-12'),
(1, 3, '2026-09-20'),
(2, 1, '2026-09-25'),
(3, 1, '2026-10-01');Энэ тушаал юу гэж хэлж байна вэ гэвэл: “loans_n хүснэгт үүсгэж, зургаан зээлийг гишүүний болон номын дугаараар нь нэм.” Энд дугааруудыг бид гараар тааруулсан — loan_log-ийн мөр бүрийг харж, "Бат бол 1, Clean Code бол 1" гэх мэтээр. Үүнийг автоматаар хийхэд JOIN хэрэгтэй бөгөөд Бүлэг 7-д тэр аргыг сурна. Үр дүнгийн loans_n:
id | member_id | book_id | loaned_on
---+-----------+---------+-----------
1 | 1 | 1 | 2026-09-03
2 | 1 | 2 | 2026-09-10
3 | 3 | 3 | 2026-09-12
4 | 1 | 3 | 2026-09-20
5 | 2 | 1 | 2026-09-25
6 | 3 | 1 | 2026-10-01Одоо өмнөх хичээлийн гурван эвдрэлийг шинэ бүтэц дээр давтаж, тус бүр хэрхэн алга болсныг шалгая.
Засах: Батын имэйл солигдлоо. UPDATE members_n SET email = 'bat.new@example.com' WHERE id = 1; — мессеж 1 rows affected. Батын имэйл зөвхөн нэг газар хадгалагддаг тул зөрөх хуулбар байхгүй. Ижил нэртэй өөр Бат гишүүн байсан ч тэр өөрийн id-тай тул хөндөгдөхгүй.
Нэмэх: Ану элслээ. INSERT INTO members_n (name, city, email) VALUES ('Ану', 'Улаанбаатар', 'anu@example.com'); — ямар ч хуурамч зээлгүйгээр. SELECT COUNT(*) FROM loans_n; нь 6 гэж хариулна — зээлийн тоо үнэн хэвээр. Шинэ ном “Clean Architecture”-ыг ч зээлгүйгээр books_n-д нэмж болно.
Устгах: Номины алдаатай зээл. DELETE FROM loans_n WHERE id = 5; — мессеж 1 rows affected. SELECT * FROM members_n WHERE id = 2; нь Номин, Эрдэнэт, nomin@example.com гэж хариулна — зээл устсан, гишүүн үлдсэн.
Гурван эвдрэл гурвуулаа алга боллоо. Бид ямар ч шинэ хэрэгсэл ашиглаагүй — зөвхөн хүснэгтүүдийг "зүйл" бүрээр нь хуваасан.
Сар өнгөрч, Дулмаа гишүүн бүрийн хэдэн ном зээлснийг харахыг хүслээ. Түүнд хамгийн энгийн санагдсан шийдэл нь гишүүдийн хүснэгтэд тоолуур нэмэх байв. Хичээлийн энэ хэсгийг Номины зээлийг устгахаас өмнөх байдлаар төсөөлөхийн тулд эхлээд тэр зээлийг буцааж нэмье: INSERT INTO loans_n (id, member_id, book_id, loaned_on) VALUES (5, 2, 1, '2026-09-25');. Одоо тоолуурыг нэмье:
sql
ALTER TABLE members_n ADD COLUMN loan_count INTEGER;
UPDATE members_n SET loan_count = 3 WHERE id = 1;
UPDATE members_n SET loan_count = 1 WHERE id = 2;
UPDATE members_n SET loan_count = 2 WHERE id = 3;
UPDATE members_n SET loan_count = 0 WHERE id = 4;Энэ тушаалууд юу гэж хэлж байна вэ гэвэл: “members_n-д loan_count багана нэмж, гишүүн бүрийн одоогийн зээлийн тоог бич.” Одоохондоо тоонууд үнэн: Бат 3, Номин 1, Сараа 2, Ану 0. Одоо Номины алдаатай зээлийг дахин устгая — Дулмаа зээлийн хүснэгтийг зассан, гэвч тоолуурыг засахаа мартжээ:
sql
DELETE FROM loans_n WHERE id = 5;Одоо хоёр газраас Номины зээлийн тоог асууя:
sql
SELECT loan_count FROM members_n WHERE id = 2;Үр дүн: 1.
sql
SELECT * FROM loans_n WHERE member_id = 2;Үр дүн: (хоосон — 0 мөр).
Эдгээр query юу гэж хэлж байна вэ гэвэл: "Номины тоолуурыг харуул" ба "Номины бүх зээлийг харуул." Тоолуур 1 гэж, бодит зээлүүд 0 гэж хэлж байна. Өгөгдлийн сан өөртэйгөө дахин зөрчилдлөө — гэхдээ энэ удаад гишүүний мэдээлэл давхардаагүй, харин өөр хүснэгтээс тооцоолж болох баримтыг тусад нь хадгалсан. loan_count бол loans_n-ийн хуулбар, зүгээр л тоо хэлбэртэй. Хуулбар бүрийн нэгэн адил энэ нь эх сурвалжаасаа зөрөх эрсдэлтэй.
Тиймээс гурав дахь дүрэм: бусад өгөгдлөөс тооцоолж гаргаж болох зүйлийг тусад нь бүү хадгал — хэрэгтэй үедээ тооцоол. Зээлийн тоо, захиалгын нийт дүн, төрсөн оноос тооцоолох нас, хувийн тоо ба зээлсэн тооноос тооцоолох "үлдсэн хувь" — эдгээр бүгд хэрэгтэй үед query-ээр тооцоологдох ёстой. Бүлэг 8-д COUNT-ыг GROUP BY-тэй хослуулан "гишүүн бүрийн зээлийн тоо"-г нэг query-ээр, үргэлж үнэн зөвөөр гаргахыг сурна. Тэр хүртэл тоолуураа устгая: ALTER TABLE members_n DROP COLUMN loan_count;.
Одоо шударга яриа. Гурван дүрмийг сохроор мөрдөх нь бодит системд заримдаа буруу байдаг бөгөөд мэргэжилтнүүд давхардлыг санаатайгаар хадгалдаг тохиолдол бий. Үүнийг denormalization гэдэг. Ялгаа нь санамсаргүй давхардал ба санаатай давхардлын хооронд оршдог.
Нэгдүгээр тохиолдол: тухайн мөчийн агшин зураг. Номын сан хоцорсон зээлд торгууль ногдуулдаг бөгөөд торгуулийн хэмжээ жил бүр өөрчлөгддөг гэж бодъё. Хэрэв зээлийн мөрөнд тухайн үеийн торгуулийн хэмжээг хуулж хадгалбал энэ нь Дүрэм 3-ыг зөрчсөн мэт харагдана. Гэвч үнэндээ энэ нь өөр баримт: "өнөөдрийн торгууль хэд вэ" биш, "энэ зээлд ямар хэмжээ хэрэглэгдсэн бэ." Ирэх жил торгууль өсөхөд өнгөрсөн зээлүүдийн түүх өөрчлөгдөх ёсгүй. Худалдааны захиалгын хүргэлтийн хаяг, баримт дээрх үнэ ч ижил зарчимтай — эдгээр нь давхардал биш, түүх юм.
Хоёрдугаар тохиолдол: хурдны төлөөх хуулбар. Таны ашиглаж буй энэ сургалтын платформ сургалт бүрийн хүснэгтэд дундаж үнэлгээ болон үнэлгээний тоог тусад нь хадгалдаг — өөрөөр хэлбэл сэтгэгдлийн хүснэгтээс тооцоолж болох утгуудыг. Энэ нь Дүрэм 3-ын шууд зөрчил. Шалтгаан нь: сургалтын жагсаалт хуудас бүр олон удаа ачаалагддаг бөгөөд жагсаалтын сургалт бүрт сэтгэгдлүүдийг тоолж дундажлах нь илүүдэл ажил болно. Тиймээс хуулбарыг хадгалж, шинэ сэтгэгдэл нэмэгдэх бүрт түүнийг шинэчлэх үүргийг програмын код хүлээдэг. Энэ нь Дулмаагийн loan_count-той яг адилхан эрсдэлтэй — шинэчлэхээ мартвал тоо худал болно. Ялгаа нь энэ нь санаатай, хэмжиж тогтоосон хэрэгцээнд суурилсан, синхрончлох хариуцлагыг тодорхой газарт оноосон шийдвэр юм.
Гуравдугаар тохиолдол: зөвхөн уншигддаг тайлан, экспорт. Сарын тайланд зориулж нэгтгэсэн хүснэгт, Excel руу гаргасан файл зэрэг нь хэзээ ч засагдахгүй, зөвхөн уншигддаг тул давхардлын гажуудал тэдэнд хамаарахгүй.
Эндээс гарах практик зарчим: анхдагчаар normalization хий, denormalization-ийг зөвхөн санаатайгаар, шалтгаантай, синхрончлох төлөвлөгөөтэйгөөр хий. Хэрэв "энэ хуулбарыг хэн, хэзээ шинэчлэх вэ" гэсэн асуултад тодорхой хариулт байхгүй бол хуулбар хэрэггүй. Мөн хурдны асуудал бодитоор хэмжигдэхээс өмнө хурдны төлөө denormalization хийх нь бараг үргэлж эрт. Бүлэг 9-д index-ийн тухай сурахдаа ихэнх хурдны асуудлыг давхардалгүйгээр шийдэх аргыг харна.
Бид Write Changes дараагүй тул Revert Changes дарахад loan_log, members_n, books_n, loans_n хүснэгтүүд алга болно. Database Structure табд зөвхөн books, members үлдсэн эсэхийг шалгаарай.
Хэрэв сонирхож байвал өмнөх хичээлийн "Туршаад үзээрэй" хэсэгт олсон өөрийн амьдралын "бүгдийг нэг дор" хүснэгтэд гурван дүрмийг хэрэглэж үзээрэй. Эхлээд нэг нүдэнд олон утга байгаа эсэхийг шалга. Дараа нь багана бүрээс "чи энэ мөрийн тухай юу, өөр зүйлийн тухай юу?" гэж асуугаад, өөр "зүйл"-ийн багануудыг шинэ хүснэгтэд шилжүүл. Эцэст нь тооцоолж болох багана байгаа эсэхийг шалга. Цаасан дээр хүснэгтүүдээ зураад, тус бүрийн PRIMARY KEY болон FOREIGN KEY-г тэмдэглэхэд хангалттай. Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээлүүд тэдгээрийг хийсэн гэж тооцохгүй.
Хэт хуваах. Normalization-ийг дөнгөж сурсан хүмүүс заримдаа багана бүрийг тусдаа хүснэгт болгох гэж оролддог: гишүүний хотыг cities хүснэгтэд, имэйлийг emails хүснэгтэд. Дүрэм 2-ын асуулт бол "энэ багана өөр зүйлийн тухай юу" — гишүүний хот гишүүний шинж чанар тул members-д үлдэнэ. Хотын хүснэгт зөвхөн хотын тухай нэмэлт мэдээлэл (аймаг, хүн ам, номын сангийн салбар) хадгалах шаардлагатай үед л утга учиртай болно.
DISTINCT-ээр хувааж байгаад зөрүүтэй бичлэгийг олохгүй байх. Хэрэв loan_log-д Бат нэг мөрөнд “bat@example.com”, нөгөөд “Bat@example.com” гэж бичигдсэн бол SELECT DISTINCT тэднийг хоёр өөр гишүүн гэж үзэж, members_n-д хоёр Бат үүснэ. Хуучин давхардалтай өгөгдлийг хуваахаас өмнө SELECT DISTINCT-ийн үр дүнг нүдээр сайн шалгаж, зөрүүтэй бичлэгүүдийг эхлээд UPDATE-ээр нэгтгэх хэрэгтэй.
Тооцоолсон утгыг хадгалаад шинэчлэхээ мартах. Дүрэм 3-ын жишээнд харсанчлан энэ нь алдааны мессеж өгдөггүй. Хэрэв санаатай denormalization хийх бол хуулбарыг шинэчлэх бүх газрыг (INSERT, UPDATE, DELETE бүрийн дараа) тодорхой жагсааж, тэдгээрийг нэг газарт төвлөрүүлэх хэрэгтэй.
Түүхийг давхардал гэж андуурах. Эсрэг алдаа нь Нэгдүгээр тохиолдлын агшин зургийг "давхардал" гэж үзээд устгах явдал юм. Хэрэв захиалгын хүснэгтээс үнийг хасаад бүтээгдэхүүний хүснэгтийн одоогийн үнэ рүү заавал, үнэ өөрчлөгдөх бүрт бүх хуучин захиалгын дүн өөрчлөгдөнө. "Энэ утга цаг хугацааны явцад өөрчлөгдөхөд хуучин мөрүүд өөрчлөгдөх ёстой юу?" — хариулт нь "үгүй" бол энэ нь давхардал биш, түүх.
Normalization-ийг хүснэгт үүсгэсний дараа бодох. Бүлэг 4-ийн ALTER хичээлд SQLite-д хүснэгтийн бүтцийг өөрчлөх нь хэр ажилтай болохыг харсан. Энэ хичээлд бид зургаахан мөртэй хүснэгтийг хуваахад ч гурван CREATE, гурван INSERT, гараар дугаар тааруулах шаардлагатай болсон. Мянган мөртэй, амьд програм ашигладаг хүснэгтэд энэ нь олон өдрийн ажил болно. Гурван дүрмийг хүснэгт зохиох үед, цаасан дээр шалгах нь хамгийн хямд.
Энэ хичээлд та normalization-ийг гурван энгийн дүрмээр сурлаа. Нэгдүгээрт, нүд бүрт ганц утга — жагсаалт эсвэл давтагдсан баганын оронд мөр. Хоёрдугаарт, мөр бүр ганц "зүйл"-ийн тухай — өөр зүйлийн шинж чанарыг өөрийн хүснэгтэд шилжүүлж, FOREIGN KEY-ээр холбох. Гуравдугаарт, тооцоолж болох зүйлийг хадгалахгүй — хэрэгтэй үед тооцоолох. Дулмаагийн хүснэгтийг эдгээр дүрмээр members_n, books_n, loans_n болгон хувааж, өмнөх хичээлийн гурван гажуудал бүгд алга болсныг шалгалаа. Мөн denormalization — агшин зургийн түүх, хурдны төлөөх хуулбар, зөвхөн уншигддаг тайлан — нь санаатай, шалтгаантай, синхрончлох төлөвлөгөөтэй үед зөв байж болохыг шударгаар харлаа.
Дараагийн хичээл бол Түвшин 2-ын төгсгөлийн удирдамжтай дасгал юм. FastAPI сургалтын Номын сангийн API-ийн өгөгдлийн санг бид энэ түвшинд сурсан бүх зүйлээр — төрөл, constraint, PRIMARY KEY, FOREIGN KEY, ON DELETE, normalization — эхнээс нь зөв зохионо. Эхлээд цаасан дээр харилцааг ярьж, дараа нь CREATE TABLE тушаалуудыг бичиж, эцэст нь .sql файл болгон хадгална. Тэр бүтэц Түвшин 3-ын бүх JOIN ба тайлангийн query-ний суурь болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.