IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis2/65-dars22 daqiqa
Mundarija (29)

PostgreSQL massivlar, range, DOMAIN va maxsus turlar: boy tur tizimi

Qisqacha: Postgres'da int va text dan tashqari ko'p tur bor: massiv (text[]), vaqt oralig'i (tstzrange), o'z qoidasi bilan tur (DOMAIN), sanoqli ro'yxat (ENUM), bir nechta maydonli tur (composite) va registrga befarq matn (citext). Ular ilova kodidagi tekshiruv va hisobning bir qismini bazaga oladi: masalan, kesishadigan bronni baza o'zi rad etadi, noto'g'ri email esa hech qaysi jadvalga kirmaydi.

Bu darsda

  • Massiv bilan ishlaysiz: indeks 1 dan, ANY, @>, &&, unnest, array_agg va cardinality tuzog'i.
  • Vaqt oraliqlarini (tstzrange, daterange) saqlaysiz, [) chegarasini va && kesishishni tushuntirasiz.
  • Multirange bilan "qaysi vaqtlar bo'sh?" savoliga bitta so'rov bilan javob berasiz.
  • DOMAIN bilan qayta ishlatiladigan tur yaratasiz va uning NOT NULL tuzog'ini bilasiz.
  • ENUM, composite tur va citext qachon kerakligini aytasiz.

Oldin bilishingiz kerak: JSONB, PostgreSQL ma'lumot turlari, Modellash naqshlari, Indeks turlari: GIN, GiST, BRIN.

1. Nega bu kerak?

O'tgan darsda JSONB'ni ko'rdik — ustun ichida butun hujjat. Bugun boshqa tomon: Postgres'ning tur tizimi. Uch vaziyatni olaylik.

Birinchi: Toshkentdagi o'quv markazi xonalarni bron qiladi. Ikki o'qituvchi bir xonaga 09:30 dan bron qilmasin. Ilovada "avval tekshir, keyin yoz" desangiz — ikki so'rov bir vaqtda kelganda ikkalasi ham "bo'sh" deb ko'radi (poyga holati). Bazada esa vaqt oralig'i turi va istisno cheklovi bor — u ikkinchisini o'zi rad etadi.

Ikkinchi: email foydalanuvchilar, izohlar, buyurtmalar jadvallarida bor. Har birida bir xil CHECK (email ~ …) yozasizmi? Bitta joyda unutildi — noto'g'ri email kirdi. DOMAIN — qoidasi bilan birga tur: bir marta yozasiz, hamma joyda ishlatasiz.

Uchinchi: "Aziz@Example.COM va aziz@example.com — bitta odam" degan qoida. citext turi buni o'zi biladi.

O'xshatish: ombordagi qutilar. Oddiy karton quti — text: ichiga nima solsangiz ketaveradi. Tuxum qutisi esa faqat tuxumga mo'ljallangan, har tuxumning o'z uyasi bor — noto'g'ri narsa sig'maydi. Maxsus turlar — tuxum qutisi: shakli ma'lumotning o'zini himoya qiladi.

Bu dars "faqat tajriba" darsi: misollar alohida jadvallarda, vazifalar sxemasi o'zgarmaydi. Nega — «Loyiha qadami» bo'limida.

2. Massivlar

2.1 Massiv ustuni

Ma'lumot turlari va Modellash naqshlari darslarida massivni ko'rdik: text[] — matnlar ro'yxati, teglar uchun. Kanon qarori: teglar — alohida jadval. Bu yerda muqobilni chuqurroq o'rganamiz. Bu darsdagi so'rovlarni o'z PostgreSQL 17.6 klasterimizda ishga tushirdik — holat 01 bazasida, lekin faqat yangi tajriba jadvallari bilan (kanon tajribasi (IlmHamroh, 2026-10, PostgreSQL 18.6) bilan mos; farq bo'lsa — aytamiz):

sql
\pset null '(null)'
CREATE TABLE m_vazifa (
  id int PRIMARY KEY,
  matn text,
  teglar text[] NOT NULL DEFAULT '{}'
);
INSERT INTO m_vazifa VALUES
  (1, 'Non olish', '{#bozor,#uy}'),
  (2, 'Hisobot', '{#ish}'),
  (3, 'Yugurish', '{}'),
  (4, 'To''lov', ARRAY['#uy', '#to''lov']);
SELECT id, teglar, teglar[1] AS birinchi, teglar[5] AS yoq,
       cardinality(teglar) AS soni,
       array_length(teglar, 1) AS uzunlik
FROM m_vazifa ORDER BY id;
text
Null display is "(null)".
CREATE TABLE
INSERT 0 4
 id |    teglar     | birinchi |  yoq   | soni | uzunlik
----+---------------+----------+--------+------+---------
  1 | {#bozor,#uy}  | #bozor   | (null) |    2 |       2
  2 | {#ish}        | #ish     | (null) |    1 |       1
  3 | {}            | (null)   | (null) |    0 |  (null)
  4 | {#uy,#to'lov} | #uy      | (null) |    2 |       2
(4 rows)

Massivni ikki xil yozish mumkin: matn '{#bozor,#uy}' yoki ARRAY['#uy', …]. Ikkinchisi xavfsizroq — ichidagi vergul va qo'shtirnoqlarni o'ylash shart emas.

Uchta tuzoq bitta jadvalda:

  1. Indeks 1 dan boshlanadi. teglar[1] — birinchi element. JS va JSONB'da 0 dan edi.
  2. Chegaradan tashqari — xato emas, NULL. teglar[5] hech qayerda yo'q, lekin xato chiqmadi.
  3. Bo'sh massivda array_length — NULL, cardinality — 0. "Tegsiz vazifalarni top" uchun array_length(teglar, 1) = 0 yozsangiz — hech narsa topilmaydi (NULL = 0 — NULL, NULL mantiqi). cardinality(teglar) = 0 yoki teglar = '{}' to'g'ri.

Tekshirib ko'ring: array_length(teglar, 1) dagi 1 nima?

Javob

O'lcham (dimension) raqami. Postgres massivi ko'p o'lchamli bo'lishi mumkin (jadval kabi int[][]), 1 — birinchi o'lcham bo'yicha uzunlik. Bir o'lchamli ro'yxat uchun cardinality qulayroq: argumenti bitta va bo'shga 0 qaytaradi.

2.2 Massiv ichidan qidirish

sql
SELECT id FROM m_vazifa WHERE '#uy' = ANY (teglar) ORDER BY id;
SELECT id FROM m_vazifa WHERE teglar @> '{#uy,#bozor}';
SELECT id FROM m_vazifa WHERE teglar && '{#ish,#to''lov}'
ORDER BY id;
text
 id
----
  1
  4
(2 rows)

 id
----
  1
(1 row)

 id
----
  2
  4
(2 rows)
Ifoda Savol
'#uy' = ANY (teglar) Massivda shu element bormi?
teglar @> '{#uy,#bozor}' Ro'yxatdagilarning hammasi bormi? (tartib muhim emas)
teglar && '{#ish,#to''lov}' Kamida bittasi bormi? (kesishadimi)

@> va && ni GIN indeksi qo'llaydi (Indeks turlari). = ANY esa Node kodida ko'p uchraydi: WHERE id = ANY($1) — bitta parametrga butun massivni berib, N ta so'rov o'rniga bitta yuborasiz (N+1 muammosi):

sql
SELECT 3 = ANY ('{3,5,7}'::int[]) AS bor,
       ARRAY[1,2] = ARRAY[2,1] AS teng,
       ARRAY[1,2] @> ARRAY[2,1] AS ichida;
text
 bor | teng | ichida
-----+------+--------
 t   | f    | t
(1 row)

= massivlarni tartibi bilan solishtiradi: {1,2} va {2,1} — teng emas. @> esa to'plam kabi: tartibga qaramaydi.

2.3 Massivni qatorlarga yoyish: unnest

Statistika uchun massivni "yoyish" kerak: har element alohida qator. unnest shuni qiladi:

sql
SELECT t, count(*) FROM m_vazifa, unnest(teglar) t
GROUP BY t ORDER BY 2 DESC, 1;
SELECT id, t, n
FROM m_vazifa, unnest(teglar) WITH ORDINALITY AS u(t, n)
WHERE id = 1;
text
    t    | count
---------+-------
 #uy     |     2
 #bozor  |     1
 #ish    |     1
 #to'lov |     1
(4 rows)

 id |   t    | n
----+--------+---
  1 | #bozor | 1
  1 | #uy    | 2
(2 rows)

WITH ORDINALITY — har elementga uning o'rnini (1, 2, …) qo'shadi. Teskari amal — array_agg: qatorlarni massivga yig'adi (1-mashqda ishlatamiz).

Bo'sh massivli 3-vazifa birinchi natijada umuman yo'q: unnest('{}') nol qator beradi, m_vazifa, unnest(…) esa ichki birlashtirish kabi ishlaydi. Tegsiz vazifalarni ham ko'rish kerak bo'lsa — LEFT JOIN LATERAL unnest(teglar) t ON true (Subquery va LATERAL).

2.4 O'zgartirish va massivning chegaralari

sql
BEGIN;
UPDATE m_vazifa
SET teglar = array_append(teglar, '#shoshilinch')
WHERE id = 2 RETURNING teglar;
UPDATE m_vazifa SET teglar = array_remove(teglar, '#uy')
WHERE id = 4 RETURNING teglar;
INSERT INTO m_vazifa VALUES (5, 'NULL teg', ARRAY[NULL]::text[])
RETURNING teglar, cardinality(teglar);
ROLLBACK;
text
BEGIN
       teglar
---------------------
 {#ish,#shoshilinch}
(1 row)

UPDATE 1
  teglar
-----------
 {#to'lov}
(1 row)

UPDATE 1
 teglar | cardinality
--------+-------------
 {NULL} |           1
(1 row)

INSERT 0 1
ROLLBACK

array_append — oxiriga qo'shadi, array_remove — shu qiymatli hamma elementlarni olib tashlaydi. Oxirgi INSERT — massiv ichida NULL element: {NULL}, uzunligi 1. NOT NULL cheklovi faqat massivning o'ziga tegishli, ichidagi elementlarga emas.

Massivning boshqa chegarasi: elementga tashqi kalit qo'yib bo'lmaydi. teglar ichidagi #uy hech qaysi teglar jadvali qatoriga bog'lanmaydi — teg o'chirilsa, massivda "yetim" qoladi. Kanon teglarni ko'prik jadvalda saqlashining asosiy sababi shu (Modellash naqshlari).

Tekshirib ko'ring: Massivda takror elementlarni qanday taqiqlash mumkin? Masalan, {#uy,#uy} kirmasin.

Javob

Massiv ustunida UNIQUE elementlar uchun ishlamaydi — u butun massivni solishtiradi. Bitta yo'l — CHECK: cardinality(teglar) = (SELECT count(DISTINCT x) FROM unnest(teglar) x)... lekin CHECK ichida ichki so'rov (subquery) taqiqlangan. Shuning uchun bunday qoida yo IMMUTABLE funksiyaga o'raladi, yo ilovada tekshiriladi, yo — eng to'g'risi — ko'prik jadvalda PRIMARY KEY (vazifa_id, teg_id) bilan. Bu massivning yana bir chegarasi.

3. Range: vaqt oraliqlari

3.1 Bron tizimi

Range (oraliq) — bitta qiymatda boshi va oxiri: "27-oktabr, 09:00 dan 10:00 gacha". Ikki ustun (boshlanish, tugash) o'rniga bitta. Afzalligi — oraliqlar uchun tayyor operatorlar va kesishishni taqiqlaydigan cheklov. Indeks turlari darsida EXCLUDE bilan bronni ko'rgan edik; endi tafsilotlariga kiramiz:

sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bron (
  id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  xona text NOT NULL,
  vaqt tstzrange NOT NULL,
  EXCLUDE USING gist (xona WITH =, vaqt WITH &&)
);
INSERT INTO bron (xona, vaqt) VALUES
  ('A', '[2026-10-27 09:00+05, 2026-10-27 10:00+05)'),
  ('A', '[2026-10-27 10:00+05, 2026-10-27 11:00+05)');
INSERT INTO bron (xona, vaqt) VALUES
  ('A', '[2026-10-27 09:30+05, 2026-10-27 10:30+05)');
text
CREATE EXTENSION
CREATE TABLE
INSERT 0 2
ERROR:  conflicting key value violates exclusion constraint "bron_xona_vaqt_excl"
DETAIL:  Key (xona, vaqt)=(A, ["2026-10-27 04:30:00+00","2026-10-27 05:30:00+00")) conflicts with existing key (xona, vaqt)=(A, ["2026-10-27 04:00:00+00","2026-10-27 05:00:00+00")).

Qatorma-qator:

  • tstzrange — vaqt zonasi bilan vaqt (timestamptz) oralig'i. Sana uchun — daterange, butun son uchun — int4range, pul/son uchun — numrange.
  • [ — boshi kiradi, ) — oxiri kirmaydi. Shuning uchun 09:00–10:00 va 10:00–11:00 kesishmaydi: 10:00 birinchisiga tegishli emas. Ikkala bron qabul qilindi.
  • EXCLUDE USING gist (xona WITH =, vaqt WITH &&) — "xonasi teng va vaqti kesishadigan ikki qator bo'lmasin". && — kesishish operatori.
  • btree_gist — oddiy = ni GiST indeksi ichida ishlatish uchun kengaytma (extension). Kengaytmalarni Extension'lar darsida to'liq o'rganamiz.

Uchinchi bron (09:30–10:30) ikkalasi bilan kesishadi — rad etildi. Xato kodi 23P01 (exclusion_violation). DETAIL da vaqtlar UTC'da: sessiyamiz zonasi UTC, 09:30+05 — 04:30+00.

Rasmda — xuddi shu uch bron. Nimaga qarang: 1- va 2-bron bir-biriga tegib turadi, lekin ustma-ust emas; 3-bron ikkalasini kesadi:

gantt
  title A xona, 27-oktabr
  dateFormat HH:mm
  axisFormat %H:%M
  section Qabul qilindi
  1-bron :a1, 09:00, 60m
  2-bron :a2, 10:00, 60m
  section Rad etildi
  3-bron :crit, a3, 09:30, 60m

Maslahat: Har doim [) ishlating. Bu yarim ochiq oraliq — algoritmlar qismida ko'rgan qoida: ketma-ket oraliqlar orasida bo'shliq ham, ustma-ust tushish ham bo'lmaydi. Postgres'da tstzrange(a, b) funksiyasi ham sukut bo'yicha [) yasaydi.

3.2 Oraliq bilan ishlash

sql
SET TIME ZONE 'Asia/Tashkent';
SELECT id, lower(vaqt) AS boshi,
       upper(vaqt) - lower(vaqt) AS davomiylik,
       vaqt @> '2026-10-27 10:00+05'::timestamptz AS soat10
FROM bron ORDER BY id;
RESET TIME ZONE;
text
SET
 id |         boshi          | davomiylik | soat10
----+------------------------+------------+--------
  1 | 2026-10-27 09:00:00+05 | 01:00:00   | f
  2 | 2026-10-27 10:00:00+05 | 01:00:00   | t
(2 rows)

RESET

lower / upper — boshi va oxiri. @> — "oraliq shu nuqtani o'z ichiga oladimi": soat 10:00 — 2-bronda (t), 1-bronda emas (f), chunki ). SET TIME ZONE faqat ko'rinishni o'zgartiradi — saqlangan qiymat bir xil (Sana va vaqt zonalari).

Boshqa operatorlar — butun sonli oraliqda ko'rish oson:

sql
SELECT int4range(1, 5) && int4range(5, 8) AS kesishadi,
       int4range(1, 5) -|- int4range(5, 8) AS yonma_yon,
       int4range(1, 5) << int4range(5, 8) AS chapda,
       int4range(2, 3) <@ int4range(1, 5) AS ichida;
text
 kesishadi | yonma_yon | chapda | ichida
-----------+-----------+--------+--------
 f         | t         | t      | t
(1 row)
Operator Ma'nosi
&& Kesishadimi (umumiy nuqta bormi)
-|- Yonma-yon (biri tugagan joyda ikkinchisi boshlanadi)
<< / >> To'liq chapda / o'ngda
@> / <@ O'z ichiga oladi / ichida

[1,5) va [5,8) — kesishmaydi, lekin yonma-yon. "Bron orasida bo'shliq qolmasin" kabi qoidalarda -|- qo'l keladi.

3.3 Kanonik shakl, cheksizlik va bo'sh oraliq

sql
SELECT '[2026-10-01, 2026-10-31]'::daterange AS oktabr,
       '[2026-10-01, 2026-10-31]'::daterange
         = '[2026-10-01, 2026-11-01)'::daterange AS teng;
SELECT tstzrange('2026-10-27 09:00+05', NULL) AS cheksiz,
       isempty('[5,5)'::int4range) AS bosh;
text
         oktabr          | teng
-------------------------+------
 [2026-10-01,2026-11-01) | t
(1 row)

           cheksiz           | bosh
-----------------------------+------
 ["2026-10-27 04:00:00+00",) | t
(1 row)

Uch narsa:

  1. Kanonik shakl. Sana va butun son "sanoqli" — 31-oktabrdan keyingi kun aniq: 1-noyabr. Shuning uchun Postgres [1, 31] ni o'zi [1, 32) ko'rinishiga keltiradi va ikkalasi teng. tstzrange da bunday yo'q: vaqtda "keyingi lahza" aniqlanmagan.
  2. Cheksiz chegara. NULL — "chegarasi yo'q": ["… 04:00:00+00",) — "shu paytdan (Toshkentda 09:00) boshlab, tugashi noma'lum". Vergul ortida hech narsa yo'q. Muddatsiz obuna, tugamagan ijara. (Bu blokda SET TIME ZONE yo'q — vaqt sessiya zonasida, UTC'da chiqdi.)
  3. Bo'sh oraliq. [5,5) — hech qanday nuqta yo'q (isempty — t). Bo'sh oraliq hech narsa bilan kesishmaydi — bu EXCLUDE dan "yashirinib o'tish" yo'li bo'lishi mumkin; kerak bo'lsa CHECK (NOT isempty(vaqt)) qo'shing.

3.4 Multirange: bo'sh vaqtni topish

PostgreSQL 14 dan multirange bor — bir nechta oraliqlar to'plami. "A xona 08:00–12:00 orasida qachon bo'sh?" — oldin bu murakkab so'rov edi. Endi:

sql
SET TIME ZONE 'Asia/Tashkent';
SELECT range_agg(vaqt) AS band,
       tstzmultirange(tstzrange('2026-10-27 08:00+05',
                                '2026-10-27 12:00+05'))
         - range_agg(vaqt) AS bosh
FROM bron WHERE xona = 'A';
RESET TIME ZONE;
text
SET
                         band                          |                                                   bosh
-------------------------------------------------------+-----------------------------------------------------------------------------------------------------------
 {["2026-10-27 09:00:00+05","2026-10-27 11:00:00+05")} | {["2026-10-27 08:00:00+05","2026-10-27 09:00:00+05"),["2026-10-27 11:00:00+05","2026-10-27 12:00:00+05")}
(1 row)

RESET
  • range_agg — oraliqlarni bitta multirange'ga yig'adi va yonma-yonlarini birlashtiradi: 09–10 va 10–11 → bitta 09:00–11:00.
  • - — ayirish: "kun" dan "band" ni olib tashlasak, bo'sh qoladi: 08–09 va 11–12.

Bitta so'rov, sikl yo'q, ilova kodida oraliq hisob-kitobi yo'q. Shifokor navbati, taksi haydovchisining smenasi, xona jadvali — hammasida aynan shu savol.

Tekshirib ko'ring: B xonaga 09:00–10:00 bron qilish mumkinmi? A xonada shu vaqt band-ku.

Javob

Mumkin. Cheklov xona WITH = va vaqt WITH && — ikkalasi bir vaqtda bajarilsa rad etadi. Xona har xil — = bajarilmaydi, kesishish muhim emas. Shuning uchun btree_gist kerak edi: xona ustuni ham GiST indeksiga kiradi.

4. DOMAIN: qoidasi bilan tur

4.1 Bir marta yozib, hamma joyda ishlatish

DOMAIN — mavjud turga (masalan, text) qoida qo'shib yaratilgan yangi tur. Qoida tur bilan birga yuradi: shu turdagi har ustun, har o'zgaruvchi, har cast tekshiriladi:

sql
CREATE DOMAIN email_manzil AS text
  CHECK (VALUE ~ '^[^@[:space:]]+@[^@[:space:]]+$');
CREATE DOMAIN vazifa_matni AS text NOT NULL
  CHECK (VALUE = btrim(VALUE)
         AND char_length(VALUE) BETWEEN 1 AND 100);
CREATE TABLE f_domen (email email_manzil, matn vazifa_matni);
INSERT INTO f_domen VALUES ('aziz@example.com', 'Non olish');
INSERT INTO f_domen VALUES ('aziz.example.com', 'Non olish');
INSERT INTO f_domen VALUES ('aziz@example.com', '  bo''shliq ');
SELECT 'yomon'::email_manzil;
text
CREATE DOMAIN
CREATE DOMAIN
CREATE TABLE
INSERT 0 1
ERROR:  value for domain email_manzil violates check constraint "email_manzil_check"
ERROR:  value for domain vazifa_matni violates check constraint "vazifa_matni_check"
ERROR:  value for domain email_manzil violates check constraint "email_manzil_check"

VALUE — tekshirilayotgan qiymat. Uchala xato bir xil manba: domen qoidasi — jadval cheklovi emas. Hatto oddiy SELECT 'yomon'::email_manzil ham — jadvalsiz — rad etildi. Bu CHECK'dan farqi: CHECK bitta jadvalni himoya qiladi, DOMAIN esa qiymatning o'zini.

vazifa_matni qoidasi — kanon vazifalar.matn CHECK'ining aynan nusxasi (CREATE TABLE va cheklovlar). DOMAIN bo'lganda, bir xil qoida izohlar, shablonlar va boshqa jadvallarda bir xil bo'lardi.

4.2 Tuzoq: domendagi NOT NULL

vazifa_matni da NOT NULL bor. Kutilmagan joyda ishlaydi:

sql
INSERT INTO f_domen (email) VALUES ('b@example.com');
text
ERROR:  domain vazifa_matni does not allow null values

Biz matn ga hech narsa yozmadik — sukut qiymat NULL. Domen uni rad etdi: "vazifa_matni domeni null qiymatga ruxsat bermaydi". Bu ko'pincha kutilgan xulq. Lekin hujjat ogohlantiradi: NOT NULL li domen ustuni ba'zi joylarda baribir NULL bo'lib chiqishi mumkin. Masalan, LEFT JOIN natijasida yoki hech qator qaytarmagan ichki so'rov qiymati yozilganda: INSERT INTO t (d) VALUES ((SELECT d FROM t WHERE false)) — biz sinadik, PG 17.6 bu NULL ni qabul qildi (postgresql.org/docs/18, "CREATE DOMAIN"). Hujjat tavsiyasi: NOT NULL ni domenda emas, ustunda yozing.

4.3 Qachon DOMAIN

Holat Tanlov
Qoida bitta jadvalda Oddiy CHECK
Bir xil qoida ko'p jadvalda (email, telefon, summa) DOMAIN
Qoida keyin o'zgarishi mumkin DOMAIN — ALTER DOMAIN bir joyda (3-mashq)

DOMAIN'ning noqulay tomoni — ORM'lar uni ko'pincha oddiy text deb ko'radi. Prisma va Drizzle darslarida buni eslatamiz.

5. ENUM, composite tur va citext

5.1 ENUM: tartibli ro'yxat

Pul, miqdor va enum'lar darsida vazifalar.ustuvorlik ni ENUM qilgan edik. Ikki muhim xususiyatini eslaymiz va bittasini qo'shamiz:

sql
CREATE TYPE u AS ENUM ('past', 'orta', 'yuqori');
SELECT 'yuqori'::u > 'past'::u AS katta, enum_range(NULL::u);
ALTER TYPE u ADD VALUE 'shoshilinch' AFTER 'yuqori';
SELECT enum_range(NULL::u);
SELECT 'juda'::u;
text
CREATE TYPE
 katta |     enum_range
-------+--------------------
 t     | {past,orta,yuqori}
(1 row)

ALTER TYPE
           enum_range
--------------------------------
 {past,orta,yuqori,shoshilinch}
(1 row)

ERROR:  invalid input value for enum u: "juda"
LINE 1: SELECT 'juda'::u;
               ^
  • Tartib — e'lon tartibida, alifboda emas: yuqori > past.
  • Yangi qiymat — ALTER TYPE … ADD VALUE, BEFORE/AFTER bilan kerakli joyga.
  • Ro'yxatda yo'q qiymat — invalid input value for enum u: "juda".

Qo'shimcha: ENUM'dan qiymatni olib tashlash buyrug'i yo'q (hujjat bo'yicha, "ALTER TYPE"). Qiymat ro'yxati tez-tez o'zgarsa — ENUM o'rniga ma'lumotnoma jadvali va tashqi kalit yaxshiroq.

5.2 Composite tur

Composite tur — bir nechta maydonli tur, jadval qatoriga o'xshash:

sql
CREATE TYPE manzil AS (shahar text, kocha text, uy int);
SELECT ROW('Toshkent', 'Amir Temur', 15)::manzil AS m,
       (ROW('Toshkent', 'Amir Temur', 15)::manzil).shahar;
text
CREATE TYPE
             m              |  shahar
----------------------------+----------
 (Toshkent,"Amir Temur",15) | Toshkent
(1 row)

Maydonga murojaat — (qiymat).maydon, qavs bilan (qavssiz Postgres qiymat ni jadval nomi deb o'ylaydi). Amalda composite tur ustun sifatida kam ishlatiladi — manzil uchun alohida ustunlar yoki jsonb qulayroq. Lekin u har joyda bor: har jadval avtomatik ravishda shu nomli composite tur ham bo'ladi. Funksiyalar qatorni qaytarganda aynan shu tur ishlatiladi (Funksiyalar va PL/pgSQL).

5.3 citext: registrga befarq matn

sql
CREATE EXTENSION IF NOT EXISTS citext;
SELECT 'Aziz@Example.COM'::citext = 'aziz@example.com' AS citext,
       'Aziz@Example.COM'::text = 'aziz@example.com' AS text_;
SELECT 'OʻZBEK'::citext = 'oʻzbek' AS ozbek,
       'İstanbul'::citext = 'istanbul' AS turkcha;
text
CREATE EXTENSION
 citext | text_
--------+-------
 t      | f
(1 row)

 ozbek | turkcha
-------+---------
 t     | f
(1 row)

citext solishtirishdan oldin ikkala tomonni lower() qiladi. Aziz@Example.COM = aziz@example.com. O'zbekcha ʻ bilan ham ishladi.

Halol topilma: oxirgi ustun muhitga bog'liq. Biz f oldik (klasterimiz ICU en-US locale bilan), kanon konteynerida (PostgreSQL 18.6, boshqa locale) xuddi shu so'rov t berdi. Turkcha İ ning kichik harfi qaysi qoidaga ko'ra olinishi bazaning locale sozlamasiga bog'liq. Xulosa: citext — "registrga befarq", lekin "har tilda bir xil" emas.

Kanon foydalanuvchilar.email uchun citext emas, lower(email) bo'yicha noyob indeks tanlagan — kengaytmasiz va qaysi qoida ishlashi aniq. Ikkalasi ham to'g'ri yo'l; muhimi — bitta loyihada bittasi.

Tajriba tugadi — yaratganlarimizni o'chiramiz, baza mashqlar uchun toza qolsin:

sql
DROP TABLE m_vazifa, bron, f_domen;
DROP DOMAIN email_manzil, vazifa_matni;
DROP TYPE manzil, u;
DROP EXTENSION btree_gist, citext;
text
DROP TABLE
DROP DOMAIN
DROP TYPE
DROP EXTENSION

6. Ko'p uchraydigan xatolar

1. array_length(x, 1) = 0 bilan bo'sh massivni qidirish. Natija bo'sh — bo'sh massivda array_length NULL. To'g'risi: cardinality(x) = 0.

2. Massiv indeksini 0 dan boshlash. teglar[0] — NULL, xato yo'q. Postgres massivi 1 dan; JSONB massivi 0 dan.

3. Oraliqni ikki ustunda saqlab, kesishishni ilovada tekshirish. Poyga holatida ikkala bron o'tadi. To'g'risi: tstzrange + EXCLUDE USING gist.

4. [] (ikkala chegara kiradi) ishlatish. 09:00–10:00 va 10:00–11:00 kesishib qoladi — ketma-ket bronlar rad etiladi. To'g'risi: [).

5. btree_gist siz EXCLUDE (xona WITH =, …). Postgres text uchun GiST operatorini topa olmaydi va xato beradi. To'g'risi: CREATE EXTENSION btree_gist.

6. Tez-tez o'zgaradigan ro'yxat uchun ENUM. Qiymatni olib tashlab bo'lmaydi. To'g'risi: ma'lumotnoma jadvali.

7. Domenda NOT NULL. Ba'zi holatlarda baribir NULL uchraydi. To'g'risi: NOT NULL — ustunda, domenda faqat CHECK.

7. Mashqlar

Mashqlar kurs/mashqlar/27/02-turlar/ da. Holat 01 bazasida; yangi jadval yaratadiganlari BEGIN … ROLLBACK ichida. Yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).

1-mashq (oson)

vazifalar ning teglari ko'prik jadvalda (vazifa_teglari + teglar). Har vazifaning teglarini massivga yig'ing va #ish hamda #to'lov teglari ikkalasi bor vazifalarni chiqaring: id va teglar massivi (alifbo tartibida). Ishora: array_agg(t.nom ORDER BY t.nom) + GROUP BY (Agregatlar), shart guruhga — HAVING; "hammasi bor" — @>.

Yechim
sql
SELECT v.id, array_agg(t.nom ORDER BY t.nom) AS teglar
FROM vazifalar v
JOIN vazifa_teglari vt ON vt.vazifa_id = v.id
JOIN teglar t ON t.id = vt.teg_id
GROUP BY v.id
HAVING array_agg(t.nom) @> ARRAY['#ish', '#to''lov']
ORDER BY v.id;
text
 id |     teglar
----+----------------
 32 | {#ish,#to'lov}
(1 row)

Bitta vazifa — 32 (soliq hisoboti). array_agg ko'prik jadvaldagi qatorlarni massivga yig'di — jadvaldan massivga o'tish. API ko'pincha aynan shunday qiladi: teglar ko'prik jadvalda saqlanadi, javobga esa massiv bo'lib chiqadi.

2-mashq (o'rta)

Shifokor navbati: navbat jadvali (shifokor text, vaqt tstzrange), bir shifokorda kesishadigan navbat bo'lmasin. Karimov shifokorga 09:00–09:30 va 10:00–11:00 navbat yozing, keyin 10:30–11:30 ni yozishga urinib ko'ring. Oxirida Karimovning 09:00–12:00 orasidagi bo'sh vaqtlarini chiqaring. Hammasi BEGIN … ROLLBACK ichida, vaqtlar +05, ko'rinish Asia/Tashkent da. Ishora: «Bron tizimi» va «Multirange» bo'limlari; SET LOCAL TIME ZONE — faqat shu tranzaksiyada.

Yechim
sql
BEGIN;
SET LOCAL TIME ZONE 'Asia/Tashkent';
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE navbat (
  shifokor text NOT NULL,
  vaqt tstzrange NOT NULL,
  EXCLUDE USING gist (shifokor WITH =, vaqt WITH &&)
);
INSERT INTO navbat VALUES
  ('Karimov', '[2026-10-28 09:00+05, 2026-10-28 09:30+05)'),
  ('Karimov', '[2026-10-28 10:00+05, 2026-10-28 11:00+05)');
SAVEPOINT s;
INSERT INTO navbat VALUES
  ('Karimov', '[2026-10-28 10:30+05, 2026-10-28 11:30+05)');
ROLLBACK TO SAVEPOINT s;
SELECT tstzmultirange(tstzrange('2026-10-28 09:00+05',
                                '2026-10-28 12:00+05'))
         - range_agg(vaqt) AS bosh
FROM navbat WHERE shifokor = 'Karimov';
ROLLBACK;
text
BEGIN
SET
CREATE EXTENSION
CREATE TABLE
INSERT 0 2
SAVEPOINT
ERROR:  conflicting key value violates exclusion constraint "navbat_shifokor_vaqt_excl"
DETAIL:  Key (shifokor, vaqt)=(Karimov, ["2026-10-28 10:30:00+05","2026-10-28 11:30:00+05")) conflicts with existing key (shifokor, vaqt)=(Karimov, ["2026-10-28 10:00:00+05","2026-10-28 11:00:00+05")).
ROLLBACK
                                                   bosh
-----------------------------------------------------------------------------------------------------------
 {["2026-10-28 09:30:00+05","2026-10-28 10:00:00+05"),["2026-10-28 11:00:00+05","2026-10-28 12:00:00+05")}
(1 row)

ROLLBACK

Bo'sh: 09:30–10:00 va 11:00–12:00. SAVEPOINT — tranzaksiya ichidagi "saqlash nuqtasi" (Tranzaksiya asoslari): xatodan keyin tranzaksiya faqat ROLLBACK ni qabul qiladi, ROLLBACK TO SAVEPOINT s esa faqat xatoli qismni bekor qilib, davom etishga imkon beradi. Agar sizda btree_gist allaqachon o'rnatilgan bo'lsa, CREATE EXTENSION o'rnida NOTICE: extension "btree_gist" already exists, skipping chiqadi — xato emas.

3-mashq (qiyin)

uz_telefon domenini yarating: +998 va 9 ta raqam. mijozlar (ism text, telefon uz_telefon) jadvaliga ikki to'g'ri raqam yozing. Keyin talab o'zgardi: faqat mobil operator kodlari (+99890…, +99891…, +99893…, +99894…, +99897…, +99899…) ruxsat. Bazada shahar raqami (+998712…) bor. Yangi qoidani domenga qo'shing: avval oddiy usulda (xatoni ko'ring), keyin eski qatorlarni tekshirmasdan, oxirida eski raqamni tuzatib — to'liq tekshiruv bilan. Hammasi BEGIN … ROLLBACK. Ishora: ALTER DOMAIN … ADD CONSTRAINT … NOT VALID va ALTER DOMAIN … VALIDATE CONSTRAINT — jadvaldagi NOT VALID kabi (ALTER TABLE); regex ^\+998(90|91|93|94|97|99)[0-9]{7}$; xatolardan keyin davom etish — 2-mashqdagi SAVEPOINT.

Yechim
sql
BEGIN;
CREATE DOMAIN uz_telefon AS text
  CHECK (VALUE ~ '^\+998[0-9]{9}$');
CREATE TABLE mijozlar (ism text, telefon uz_telefon);
INSERT INTO mijozlar VALUES
  ('Aziz', '+998901234567'), ('Do''kon', '+998712345678');
SAVEPOINT s;
ALTER DOMAIN uz_telefon ADD CONSTRAINT uz_mobil
  CHECK (VALUE ~ '^\+998(90|91|93|94|97|99)[0-9]{7}$');
ROLLBACK TO SAVEPOINT s;
ALTER DOMAIN uz_telefon ADD CONSTRAINT uz_mobil
  CHECK (VALUE ~ '^\+998(90|91|93|94|97|99)[0-9]{7}$')
  NOT VALID;
SAVEPOINT s2;
INSERT INTO mijozlar VALUES ('Yangi', '+998711112233');
ROLLBACK TO SAVEPOINT s2;
SAVEPOINT s3;
ALTER DOMAIN uz_telefon VALIDATE CONSTRAINT uz_mobil;
ROLLBACK TO SAVEPOINT s3;
UPDATE mijozlar SET telefon = '+998991112233'
WHERE ism = 'Do''kon';
ALTER DOMAIN uz_telefon VALIDATE CONSTRAINT uz_mobil;
ROLLBACK;
text
BEGIN
CREATE DOMAIN
CREATE TABLE
INSERT 0 2
SAVEPOINT
ERROR:  column "telefon" of table "mijozlar" contains values that violate the new constraint
ROLLBACK
ALTER DOMAIN
SAVEPOINT
ERROR:  value for domain uz_telefon violates check constraint "uz_mobil"
ROLLBACK
SAVEPOINT
ERROR:  column "telefon" of table "mijozlar" contains values that violate the new constraint
ROLLBACK
UPDATE 1
ALTER DOMAIN
ROLLBACK

Ketma-ketlik: oddiy ADD CONSTRAINT shu domenli hamma ustunlarni tekshirdi va do'kon raqamini topdi. Xabar: "mijozlar jadvalining telefon ustunida yangi cheklovni buzadigan qiymatlar bor". NOT VALID — eski qatorlar tekshirilmaydi, yangi qator esa darhol tekshiriladi (+99871… rad etildi). VALIDATE eski yomon qatorni topdi; tuzatgandan keyin o'tdi. Domen qoidasi bir joyda o'zgardi — mijozlar ham, shu domenli boshqa har jadval ham himoyalandi. Katta jadvallarda NOT VALID + VALIDATE nega kerakligini keyingi darsda ko'ramiz.

8. Loyiha qadami

Bu darsda vazifalar bazasi o'zgarmaydi — kanon tajribasi alohida jadvallarda o'tkazildi. Har tur uchun kanon qarori:

Tur vazifalar da Nega
Massiv Yo'q, teglar — ko'prik jadval FK, noyoblik, teg ma'lumoti (26-qism qarori)
Range Yo'q Vazifada "oraliq" yo'q: muddat — bitta nuqta
DOMAIN Yo'q, CHECK lar Har qoida bitta jadvalda — DOMAIN foyda bermaydi
ENUM ustuvorlik (26-qismdan) Uch qiymat, deyarli o'zgarmaydi
citext Yo'q, lower(email) indeksi Kengaytmasiz, natija locale'ga bog'liq emas

Bu ham muhim qaror: tur faqat "bor" bo'lgani uchun ishlatilmaydi. Commit yo'q. Keyingi darsdagi cheklovlar ham asosan tajriba — lekin ulardan biri (EXCLUDE … DEFERRABLE) vazifalar da 26-qismdan beri ishlab turibdi; uni o'sha yerda ochamiz.

9. Real ishda

  • Bron va jadval tizimlari — mehmonxona, shifokor navbati, o'quv xonalari, avtomobil ijarasi: tstzrange + EXCLUDE — "ikki marta sotish" bazada imkonsiz.
  • Narx va tarif tarixi — "shu narx 1-yanvardan 31-martgacha amal qilgan": daterange va @> bilan "shu sanadagi narx" bitta so'rov.
  • Node'da massiv parametr — pg drayveri JS massivini Postgres massiviga o'giradi: WHERE id = ANY($1) va [3, 5, 7] (node-postgres asoslari).
  • ORM'lar — Prisma massivlarni (String[]) Postgres'da qo'llaydi; range, DOMAIN va multirange uchun maxsus tur yo'q — xom SQL kerak bo'ladi.
  • Intervyuda: "Bron kesishmasligini qanday kafolatlaysiz?" — eng kuchli javob: EXCLUDE USING gist + [), ilova tekshiruvi emas.

Xulosa

  • Massiv: indeks 1 dan, chegaradan tashqari — NULL; bo'sh massivda array_length NULL, cardinality 0.
  • = ANY — "bormi", @> — "hammasi bormi", && — "kamida bittasi"; unnest yoyadi, array_agg yig'adi.
  • Range [) — ketma-ket oraliqlar kesishmaydi; EXCLUDE USING gist (… WITH =, vaqt WITH &&) kesishishni bazada taqiqlaydi.
  • Multirange (range_agg, ayirish) — "qachon bo'sh?" savoliga bitta so'rov.
  • DOMAIN — qoidasi bilan tur, qoida bir joyda o'zgaradi; NOT NULL ni ustunga yozing.
  • ENUM — tartibli, qiymat qo'shiladi, lekin olib tashlanmaydi; citext natijasi locale'ga bog'liq bo'lishi mumkin.

Keyingi dars: Ilg'or constraint'lar va generated ustunlar — EXCLUDE chuqurroq, generated ustunlar (PG 18 da VIRTUAL), katta jadvalga cheklovni to'xtatmasdan qo'shish va tartib ni almashtirishga imkon beradigan kechiktirilgan UNIQUE.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL massivlar, range, DOMAIN va maxsus turlar: boy tur tizimi — IlmHamroh