Ачааллж байна...
Өмнөх хичээлд бид GROUP BY-ээр гишүүн бүрийн зээлийн тоог гаргасан: Бат 3, Номин 2, Сараа 2, Тэмүүлэн 1, Ану 0. Одоо эрхлэгч дараагийн асуултаа тавьлаа: "Хоёр ба түүнээс олон ном зээлсэн гишүүд хэн бэ?" Энэ нь энгийн шүүлт мэт сонсогдоно — Бүлэг 2-оос хойш бид WHERE-ээр шүүсээр ирсэн. Гэвч WHERE COUNT(*) >= 2 гэж бичвэл SQLite алдаа өгнө. Энд шинэ хэрэгсэл хэрэгтэй: HAVING.
WHERE болон HAVING хоёр хоёулаа шүүдэг бөгөөд бичлэг нь бараг адилхан харагддаг тул SQL суралцагчдын дунд хамгийн удаан үргэлжилдэг төөрөгдлийн нэг болдог. Ялгаа нь нэг өгүүлбэрт багтана: WHERE нь мөрүүдийг сагслахаас өмнө шүүдэг, HAVING нь сагснуудыг үүссэний дараа шүүдэг. Энэ өгүүлбэрийг цээжлэх амархан, ойлгох нь хэцүү. Тиймээс энэ хичээлд бид нэг жишээг хоёр шүүлтээр хоёуланг нь дамжуулж, мөр бүрийн хувь заяаг шат бүрт мөшгинө.
Бүлэг 1-ийн SELECT-ийн анатоми хичээлд "Бүлэг 8-д WHERE болон HAVING хоёрын ялгааг судлахдаа 'аль нь түрүүлж ажилладаг вэ' гэдэг асуулт бүх зүйлийг тайлбарлах түлхүүр болно" гэж хэлсэн. Тэр түлхүүрийг одоо хэрэглэнэ. library2.db-г нээгээрэй — бид зөвхөн уншина.
Өмнөх хичээлийн номын санч ба түүний сагснуудыг санаарай. Тэр зээлийн хуудас бүрийг уншиж, гишүүний сагсанд хийдэг байсан. Одоо түүний ажилд хоёр хяналтын цэг нэмэгдлээ.
Эхний хяналт сагснуудын өмнө байна: хуудас бүрийг сагсанд хийхээсээ өмнө шалгана. "Зөвхөн буцаагдсан зээлүүдийг сагслана" гэсэн дүрэмтэй бол буцаагаагүй зээлийн хуудас сагсанд огт орохгүй — хажуу тийш тавигдана. Энэ шалгалт хуудас бүрийг дангаар нь хардаг; тэр мөчид сагс хараахан байхгүй тул "энэ сагсанд хэдэн хуудас байгаа" гэдгийг мэдэх боломжгүй. Энэ бол WHERE.
Хоёр дахь хяналт сагснуудын дараа байна: бүх хуудас ангилагдаж, сагс бүр тоологдсоны дараа сагс бүрийг шалгана. "Хоёроос цөөн хуудастай сагсыг тайланд бүү оруул" гэсэн дүрэмтэй бол нэг хуудастай сагс бүхэлдээ хажуу тийш тавигдана. Энэ шалгалт сагсыг бүхэлд нь хардаг — тоо, нийлбэр, дундажийг. Энэ бол HAVING. Хоёр хяналт хоёулаа "хасдаг" ч нэг нь хуудас хасдаг, нөгөө нь сагс хасдаг.
Эрхлэгчийн асуулт: хоёр ба түүнээс олон ном зээлсэн гишүүд. Эхлээд өмнөх хичээлийн мэдлэгээр WHERE-ээр оролдъё:
sql
SELECT member_id, COUNT(*) AS loan_count
FROM loans
WHERE COUNT(*) >= 2
GROUP BY member_id;Үр дүн:
misuse of aggregate: COUNT()Энэ мессеж юу гэж хэлж байна вэ гэвэл: “COUNT() aggregate функцийг буруу газар ашиглалаа.” Энэ алдааг Хичээл 1-ийн Түгээмэл алдаанууд хэсэгт AVG-тэй хамт харсан. Зүйрлэлээр шалтгаан тодорхой: WHERE нь хуудас бүрийг сагсанд хийхээс өмнө шалгадаг — тэр мөчид сагс байхгүй, тоолох юм байхгүй. "Энэ хуудасны сагсанд хоёроос олон хуудас бий юу" гэж асуух нь утгагүй, учир нь сагс хараахан үүсээгүй.
Зөв хэлбэр нь HAVING:
sql
SELECT member_id, COUNT(*) AS loan_count
FROM loans
GROUP BY member_id
HAVING COUNT(*) >= 2
ORDER BY member_id;Үр дүн:
member_id | loan_count
----------+-----------
1 | 3
2 | 2
4 | 2Энэ query юу гэж хэлж байна вэ гэвэл: “loans-ийн мөрүүдийг гишүүнээр сагслаад, мөрийн тоо нь хоёр ба түүнээс их сагснуудыг л үлдээж, тэдгээрийн member_id ба тоог харуул.” Шатаар мөшгье. GROUP BY дөрвөн сагс үүсгэнэ: гишүүн 1 — гурван мөр, гишүүн 2 — хоёр, гишүүн 3 — нэг, гишүүн 4 — хоёр. Дараа нь HAVING сагс бүрийг шалгана: 3 ≥ 2 — үлдэнэ; 2 ≥ 2 — үлдэнэ; 1 ≥ 2 — хасагдана (Тэмүүлэн); 2 ≥ 2 — үлдэнэ. Гурван сагс гурван мөр болно.
HAVING нь Бүлэг 1-ийн араг ясанд GROUP BY-ийн яг араас, ORDER BY-ийн өмнө байрладаг — тэр араг ясны таван нүднээс энэ нь бидний хамгийн сүүлд бөглөж буй нь. HAVING-ийн нөхцөл WHERE-ийнхтэй адил хэлбэртэй: харьцуулалт, AND, OR, NOT — гэхдээ тэдгээр нь сагсны тухай утгуудыг (aggregate функцууд болон сагсны шошго) хардаг.
Одоо энэ хичээлийн гол жишээ. Эрхлэгчийн асуулт нарийсав: "Номоо хоёр ба түүнээс олон удаа буцааж өгсөн гишүүд хэн бэ?" Энэ асуулт хоёр төрлийн шүүлт агуулж байна: "буцааж өгсөн" нь зээл бүрийн тухай (хуудас бүрийн шалгалт), "хоёр ба түүнээс олон" нь гишүүн бүрийн нийлбэрийн тухай (сагсны шалгалт). Query:
sql
SELECT member_id, COUNT(*) AS returned_count
FROM loans
WHERE returned_on IS NOT NULL
GROUP BY member_id
HAVING COUNT(*) >= 2
ORDER BY member_id;Үр дүн:
member_id | returned_count
----------+---------------
1 | 2
4 | 2Энэ query юу гэж хэлж байна вэ гэвэл: “loans-ийн мөрүүдээс буцаасан огноо нь бөглөгдсөнийг л аваад, гишүүнээр сагсалж, хоёр ба түүнээс олон мөртэй сагснуудын member_id ба тоог харуул.”
Мөр бүрийн аяллыг дөрвөн шатаар мөшгье.
Шат 1 — FROM. Найман зээл:
id | member_id | returned_on
---+-----------+-----------
1 | 1 | 2026-09-15
2 | 1 | 2026-09-30
3 | 2 | 2026-09-25
4 | 1 | NULL
5 | 4 | 2026-10-02
6 | 2 | NULL
7 | 3 | NULL
8 | 4 | 2026-10-12Шат 2 — WHERE (хуудас бүрийг шалгах). returned_on IS NOT NULL — зээл 4, 6, 7 буцаагаагүй тул сагсанд орохгүй, хажуу тийш тавигдана. Таван мөр үлдэнэ:
id | member_id | returned_on
---+-----------+-----------
1 | 1 | 2026-09-15
2 | 1 | 2026-09-30
3 | 2 | 2026-09-25
5 | 4 | 2026-10-02
8 | 4 | 2026-10-12Шат 3 — GROUP BY (сагслах). Үлдсэн таван мөр гурван сагсанд:
сагс member_id = 1: зээл 1, зээл 2 → 2 мөр
сагс member_id = 2: зээл 3 → 1 мөр
сагс member_id = 4: зээл 5, зээл 8 → 2 мөрТэмүүлэн (3) сагсгүй болсныг анзаараарай — түүний цорын ганц зээл (7) буцаагаагүй тул Шат 2-т хасагдсан, түүнд сагслах мөр үлдээгүй. Батын сагс гурав биш, хоёр мөртэй — зээл 4 Шат 2-т хасагдсан.
Шат 4 — HAVING (сагс бүрийг шалгах). COUNT(*) >= 2 — гишүүн 1-ийн сагс (2) үлдэнэ, гишүүн 2-ынх (1) хасагдана, гишүүн 4-ийнх (2) үлдэнэ.
Эцсийн үр дүн хоёр мөр: Бат ба Номин. Сараа нэг ном буцаасан тул Шат 4-т, Тэмүүлэн нэг ч ном буцаагаагүй тул Шат 2-т хасагдсан — хоёр өөр шатанд, хоёр өөр шалтгаанаар.
Энэ мөшгилтөөс WHERE ба HAVING-ийн бүх ялгааг уншиж болно. WHERE ажиллах үед сагс байхгүй тул түүний нөхцөл зөвхөн нэг мөрийн утгыг (returned_on) харж чадна — aggregate функц бичиж болохгүй. HAVING ажиллах үед хуудаснууд аль хэдийн сагсанд орсон тул түүний нөхцөл сагсны нийлбэрийг (COUNT(*)) харж чадна. Мөн WHERE-ийн шийдвэр HAVING-ийн хараад буй тоонд нөлөөлдөг: Батын тоо Шат 2-ын улмаас 3-аас 2 болсон. Тиймээс дараалал чухал — WHERE эхэлж, HAVING дараа нь.
Жишээ 1: ижил үг, өөр асуулт
WHERE ба HAVING-ийн ялгааг тод харуулах хамгийн сайн арга бол бараг ижил хоёр query-г харьцуулах. Жанр бүрийн мэдээллийг харъя. Эхний query:
sql
SELECT genre, COUNT(*) AS book_count
FROM books
WHERE copies >= 2
GROUP BY genre
ORDER BY genre;Үр дүн:
genre | book_count
------------+-----------
Програмчлал | 3
Роман | 1
Түүх | 1
Шүлэг | 1Энэ query юу гэж хэлж байна вэ гэвэл: “books-ээс хоёр ба түүнээс олон хувьтай номнуудыг л аваад, жанраар сагсалж, жанр бүрийн номын тоог харуул.” Энэ бол номын шүүлт: нэг хувьтай номнууд (Учиртай гурван толгой, Clean Architecture) сагсанд орохгүй. Жүжгийн жанр цорын ганц номоо алдаж бүхэлдээ алга болсон, програмчлалын жанр дөрвөн номоос гурав болсон.
Хоёр дахь query:
sql
SELECT genre, COUNT(*) AS book_count
FROM books
GROUP BY genre
HAVING SUM(copies) >= 2
ORDER BY genre;Үр дүн:
genre | book_count
------------+-----------
Програмчлал | 4
Роман | 1
Түүх | 1
Шүлэг | 1Энэ query юу гэж хэлж байна вэ гэвэл: “books-ийг жанраар сагслаад, нийт хувь нь хоёр ба түүнээс их жанруудын номын тоог харуул.” Энэ бол жанрын шүүлт: бүх ном сагсанд орсон, дараа нь нийт хувь нь хоёроос цөөн жанр (Жүжиг — 1 хувь) хасагдсан. Програмчлалын жанр дөрвөн номтойгоо бүрэн үлдсэн.
Хоёр үр дүн гаднаасаа бараг адилхан — нэг жанр алга, бусад нь байна. Гэвч програмчлалын мөрийг хараарай: 3 ба 4. Эхний query "хоёроос олон хувьтай хэдэн програмчлалын ном байна вэ" гэж, хоёр дахь нь "хувийн нийлбэр хоёроос олон жанрт хэдэн ном байна вэ" гэж хариулсан. Тоо хоёулаа үнэн, гэвч өөр өөр асуултын хариулт. Иймэрхүү зөрүү бодит тайланд мэдэгдэхгүй өнгөрөх аюултай тул query бичихээсээ өмнө асуултаа "мөр бүрийн тухай уу, бүлэг бүрийн тухай уу" гэж задлах дадал гаргаарай.
Жишээ 2: нэрээр — хамгийн их зээлэгдсэн номнууд
HAVING нь Бүлэг 7-ийн JOIN болон өмнөх хичээлийн “PRIMARY KEY-ээр сагсал” дүрэмтэй бүрэн нийцдэг. Хоёр ба түүнээс олон удаа зээлэгдсэн номнуудыг нэрээр нь харъя. Query:
sql
SELECT b.title, COUNT(l.id) AS times_loaned
FROM books AS b
INNER JOIN loans AS l ON l.book_id = b.id
GROUP BY b.id, b.title
HAVING COUNT(l.id) >= 2
ORDER BY times_loaned DESC, b.title;Үр дүн:
title | times_loaned
---------------+-------------
Clean Code | 3
Тунгалаг Тамир | 2Энэ query юу гэж хэлж байна вэ гэвэл: “books-ийг loans-тай нийлүүлж, номоор сагслаад, хоёр ба түүнээс олон зээлтэй номнуудын нэр ба зээлийн тоог тоогоор ихээс бага руу эрэмбэлж харуул.” Шатууд: JOIN найман мөр, GROUP BY таван сагс (Clean Code 3, Тунгалаг Тамир 2, Python Crash Course 1, The Pragmatic Programmer 1, Миний нутаг 1), HAVING гурван нэг зээлтэй сагсыг хасна. Энэ бол номын сангийн "алдартай номнууд"-ын жагсаалт — хэрэв дахин хувь худалдаж авах бол эндээс эхлэх нь зүйтэй.
Жишээ 3: HAVING-д alias ашиглах тухай шударга тэмдэглэл
Жишээ 2-т бид SELECT-д times_loaned гэсэн alias өгсөн ч HAVING-д COUNT(l.id)-г дахин бичсэн. Яагаад HAVING times_loaned >= 2 гэж бичсэнгүй вэ? SQLite үүнийг зөвшөөрдөг — query алдаагүй ажиллаж, ижил үр дүн өгнө. Гэвч PostgreSQL зөвшөөрдөггүй, учир нь гүйцэтгэх дарааллаар HAVING нь SELECT-ээс өмнө ажилладаг тул SELECT-д өгсөн нэр тэр мөчид хараахан байхгүй. ORDER BY нь SELECT-ийн дараа ажилладаг тул alias-ийг хоёр өгөгдлийн сан хоёулаа хүлээн авдаг. Өмнөх хичээлийн GROUP BY-ийн дүрмийн нэгэн адил бид PostgreSQL-д ч ажиллах хэлбэрийг сонгоно: HAVING-д aggregate илэрхийллийг бүтнээр нь бичнэ.
Бүлэг 1-ээс хойш бид араг ясны нүд бүрийг нэг нэгээр нь бөглөсөөр ирсэн. Одоо бүгдийг нэг дор, өгөгдлийн сангийн гүйцэтгэх дарааллаар харъя:
1. FROM ба JOIN — хүснэгтүүдийг авч нийлүүлэх
2. WHERE — мөр бүрийг шүүх (сагсгүй)
3. GROUP BY — үлдсэн мөрүүдийг сагслах
4. HAVING — сагс бүрийг шүүх
5. SELECT — үр дүнгийн баганыг тооцоолох (alias энд төрнө)
6. ORDER BY — эрэмбэлэх (alias ашиглаж болно)
7. LIMIT — тасдахЭнэ хүснэгт бүлгийн эхнээс хойш бидний тулгарсан бүх "яагаад" асуултад хариулна. Яагаад WHERE-д aggregate бичиж болохгүй вэ? — Шат 2-т сагс байхгүй. Яагаад HAVING-д SELECT-ийн alias PostgreSQL-д ажилладаггүй вэ? — Шат 4 нь Шат 5-аас өмнө. Яагаад ORDER BY-д alias ажилладаг вэ? — Шат 6 нь Шат 5-ын дараа. Яагаад LIMIT нь ORDER BY-гүй утгагүй вэ? — Шат 7 нь Шат 6-гийн эрэмбэлсэн үр дүнг тасдана. Энэ хүснэгтийг цээжлэх шаардлагагүй, гэхдээ query алдаа өгөх эсвэл хачин үр дүн гаргах бүрт түүн рүү буцаж хараарай.
Эсрэг чиглэлийн алдаа ч бий. Хэрэв HAVING сагснуудыг шүүдэг бол сагсны шошгоор шүүж болох уу? Болно: GROUP BY genre HAVING genre <> 'Програмчлал' нь ажиллана. Гэвч энэ нь WHERE genre <> 'Програмчлал' GROUP BY genre-тэй ижил үр дүн өгдөг бөгөөд WHERE хувилбар нь илүү зөв. Шалтгаан нь хоёр: WHERE програмчлалын мөрүүдийг сагслахаас өмнө хасдаг тул өгөгдлийн сан илүү бага ажил хийнэ; мөн query-г уншиж буй хүн "энэ бол мөрийн шүүлт" гэдгийг шууд таньдаг. Дүрэм: нөхцөл нь aggregate функц агуулж байвал HAVING, үгүй бол WHERE. Энгийн бөгөөд бараг хэзээ ч алддаггүй дүрэм.
Хэрэв сонирхож байвал “9-р сард хоёр ба түүнээс олон ном зээлсэн гишүүд” гэсэн асуултад хариулж үзээрэй. Эхлээд асуултаа задлаарай: аль хэсэг нь зээл бүрийн тухай (WHERE-д), аль хэсэг нь гишүүний нийлбэрийн тухай (HAVING-д)? Дараа нь гол жишээний дөрвөн шатыг цаасан дээр мөшгиж, хариултаа таамаглаад query-ээр шалгаарай. Мөн "Програмчлалын жанраас бусад, нийт хувь нь хоёроос олон жанрууд" гэсэн асуултыг WHERE ба HAVING хоёуланг нь агуулсан нэг query-ээр бичиж болно. Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээлүүд тэдгээрийг хийсэн гэж тооцохгүй.
WHERE-д aggregate бичих. Анхны жишээнд харсанчлан misuse of aggregate: COUNT() гарна. Энэ мессеж нь "энэ нөхцлийг HAVING руу шилжүүл" гэсэн шууд зөвлөмж гэж уншиж болно.
HAVING-ийг GROUP BY-ийн өмнө бичих. ... HAVING COUNT(*) >= 2 GROUP BY member_id гэж бичвэл near "GROUP": syntax error гарна. Энэ мессеж юу гэж хэлж байна вэ гэвэл: “GROUP гэдэг үгийн ойролцоо SQL-ийн дүрэм зөрчигдлөө.” Араг ясны дараалал: GROUP BY, дараа нь HAVING — сагс үүсэхээс өмнө сагсыг шүүх боломжгүй.
Мөрийн шүүлтийг HAVING-д бичих. Дээр тайлбарласанчлан энэ нь ихэвчлэн ажиллах ч удаан, төөрөгдүүлэгч. Мөн HAVING-д GROUP BY-д байхгүй энгийн баганаар шүүвэл (HAVING returned_on IS NOT NULL) өмнөх хичээлийн "сагсанд олон утга" асуудал давтагдана: SQLite сагснаас дурын нэг мөрийн утгыг шалгаж, утгагүй шийдвэр гаргана, PostgreSQL алдаа өгнө.
WHERE-ийн нөлөөг мартах. Хоёр шатны мөшгилтөд Батын тоо WHERE-ийн улмаас 3-аас 2 болсон. Хэрэв таны HAVING-ийн хүлээлт (жишээ нь "Бат гурван зээлтэй") WHERE-ийн өмнөх тоон дээр тулгуурласан бол үр дүн таныг гайхшруулна. HAVING-ийн хардаг тоо бол WHERE-ийн дараах тоо.
LEFT JOIN, HAVING ба тэг. Хэрэв "нэг ч ном зээлээгүй гишүүд"-ийг LEFT JOIN ... GROUP BY ... HAVING COUNT(*) = 0 гэж олох гэвэл хоосон үр дүн гарна — өмнөх хичээлд сурсанчлан LEFT JOIN-ийн дараа Ану нэг мөртэй тул COUNT(*) нь 1. HAVING COUNT(l.id) = 0 нь зөв ажиллана. Гэвч энэ асуултын хамгийн энгийн хариулт Бүлэг 7-ийн LEFT JOIN ... WHERE l.id IS NULL хэвээр.
Энэ хичээлд та WHERE ба HAVING-ийн ялгааг бүрмөсөн шийдлээ. WHERE нь мөрүүдийг сагслахаас өмнө, мөр бүрийг дангаар нь шүүдэг тул aggregate функц агуулж чадахгүй. HAVING нь GROUP BY сагснуудыг үүсгэсний дараа сагс бүрийг бүхэлд нь шүүдэг тул COUNT, SUM гэх мэт нийлбэрийг шалгаж чадна. Нэг query-д хоёулаа байж болох бөгөөд WHERE эхэлж ажилладаг тул HAVING-ийн хардаг тоонд нөлөөлдөг. Бүрэн гүйцэтгэх дараалал — FROM/JOIN, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT — нь энэ бүлгийн бүх "яагаад" асуултад хариулдаг. Нөхцөл нь aggregate агуулж байвал HAVING, үгүй бол WHERE; HAVING-д SELECT-ийн alias биш, aggregate илэрхийллийг бүтнээр нь бичнэ.
Дараагийн хичээлээр Бүлэг 8 дуусна. Бид JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT-ийг бүгдийг нэг query-д хослуулж, номын сангийн жинхэнэ тайлангууд бүтээнэ: хамгийн их зээлэгдсэн номнууд зохиогчтойгоо, гишүүн бүрийн зээл ба хоцролт нэрээр нь, жанр бүрийн ашиглалт. Эдгээр нь энэ сургалтын одоог хүртэлх хамгийн сэтгэл ханамжтай query-нүүд байх болно — Түвшин 1, 2, 3-ын мэдлэг бүгд нэг дор ажиллана.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.