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

PostGIS va geo so'rovlar: geography, ST_DWithin va eng yaqin nuqtalar

Qisqacha: PostGIS — Postgres'ga xarita ma'lumotlarini qo'shadigan extension (kengaytma). Joylashuv uchun geography(Point, 4326) turi masofani metrda beradi (geometry esa gradusda). "3 km ichidagilar" — ST_DWithin (indeksdan foydalanadi), "eng yaqin 10 ta" — ORDER BY joy <-> nuqta LIMIT 10 (KNN). Ikkalasi ham GiST indeks bilan: kanonda 200 000 nuqtada indekssiz 315 ms, GiST bilan 2.3 ms, eng yaqin 5 ta — 0.19 ms. Eng ko'p uchraydigan xato — POINT(kenglik uzunlik): to'g'risi POINT(uzunlik kenglik).

Bu darsda

  • geometry va geography farqini, SRID 4326 nimaligini tushuntirasiz.
  • ST_Distance, ST_DWithin, ST_Covers va KNN <-> bilan so'rov yozasiz.
  • Nega ST_Distance(…) < N sekin, ST_DWithin tez ekanini EXPLAIN orqali ko'rsatasiz.
  • Uzunlik/kenglik tartibi xatosini taniysiz.
  • PostGIS'siz ham masofani hisoblay olasiz (Haversine formulasi — SQL va JS'da).

Oldin bilishingiz kerak: Extension'lar, Indeks turlari: GIN, GiST, BRIN va pg_trgm, EXPLAIN ANALYZE'ni o'qish.

1. Nega bu kerak?

Yandex Go "eng yaqin haydovchini" topadi. Uzum Market "sizga eng yaqin topshirish punktini" ko'rsatadi. Bank ilovasida "yaqindagi bankomatlar". Hammasi bitta savol: "menga eng yaqin 10 ta nuqta qaysi va ular qancha uzoqda?"

Birinchi fikr: kenglik va uzunlikni ikki float ustunda saqlab, Pifagor bilan hisoblash. Ikki muammo: Yer — shar (aniqrog'i, biroz yassilangan shar), gradus esa metr emas; va har so'rovda har qator uchun formula — indekssiz to'liq o'qish.

O'xshatish: kutubxona javonlari va katalog kartochkalari. Har kitobni ochib "bu fizika kitobimi?" deb ko'rish — sekin. Katalog esa "fizika — 5-qator, 3-javon" deydi. Geo indeks (GiST) — xarita uchun katalog: "bu hududdagi nuqtalar — mana bu bloklarda".

Halol eslatma: PostGIS Postgres bilan birga kelmaydi (Extension'lar darsida extension "postgis" is not available ni ko'rdik). Kanon uni alohida Docker image'da sinagan: postgis/postgis:18-3.6 (PostGIS loyihasining image'i, Docker Official Image emas), PostGIS 3.6.4, PostgreSQL 18.6 (kanon o'lchovi, IlmHamroh, 2026-10). Bizning PostgreSQL 17.6 klasterimizda PostGIS yo'q — shuning uchun PostGIS so'rovlari ostidagi natijalar — kanon chiqishi. PostGIS'siz hisoblar (Haversine) esa o'zimiz ishga tushirgan. Koordinatalar xarita bo'yicha taxminiy, o'quv misoli.

2. Koordinatalar va SRID

2.1 Kenglik va uzunlik

Yer sharidagi har nuqta ikki son bilan:

  • Kenglik (latitude, lat) — ekvatordan shimol yoki janubga, −90…90 gradus. Toshkent ≈ 41.3.
  • Uzunlik (longitude, lon) — Grinvichdan sharq yoki g'arbga, −180…180. Toshkent ≈ 69.2.

Google Maps "41.3266, 69.2346" deb ko'rsatadi — avval kenglik. PostGIS va GeoJSON esa avval uzunlik: POINT(69.2346 41.3266) — chunki matematikada avval x (gorizontal — uzunlik), keyin y. Bu tartib farqi — butun mavzudagi eng ko'p xato manbai (pastda o'lchangan).

2.2 SRID 4326

SRID (Spatial Reference ID) — koordinatalar qaysi tizimda ekanini bildiradigan raqam. 4326 — WGS 84: GPS, telefonlar, Google va Yandex xaritalari ishlatadigan tizim. Amalda: telefondan kelgan har koordinata — 4326.

2.3 geometry yoki geography

PostGIS ikki tur beradi:

geometry geography
Yer shakli tekis tekislik sferoid (haqiqiy Yer)
4326 da masofa gradusda metrda
Tezlik va funksiyalar tezroq, funksiyalar ko'p sekinroq, asosiy funksiyalar
Qachon xaritada chizish, kichik hudud (proyeksiyada) "qancha metr?", butun dunyo bo'ylab

Kanon ikkalasini bir xil nuqtalarda solishtirgan:

sql
CREATE TABLE joylar (
  id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  nom text NOT NULL,
  joy geography(Point, 4326) NOT NULL);
INSERT INTO joylar (nom, joy) VALUES
  -- POINT(uzunlik kenglik) — TARTIB: lon lat!
  ('Chorsu bozori', 'SRID=4326;POINT(69.2346 41.3266)'),
  ('Amir Temur xiyoboni', 'SRID=4326;POINT(69.2797 41.3111)'),
  ('Toshkent temir yo''l vokzali',
   'SRID=4326;POINT(69.2880 41.2920)'),
  ('Samarqand Registon', 'SRID=4326;POINT(66.9750 39.6547)');
CREATE INDEX joylar_joy_idx ON joylar USING gist (joy);

SELECT a.nom, b.nom, round(ST_Distance(a.joy, b.joy)) AS metr,
       round(ST_Distance(a.joy::geometry,
                         b.joy::geometry)::numeric, 4) AS gradus
FROM joylar a, joylar b WHERE a.id = 1 AND b.id > 1 ORDER BY b.id;

Kanon chiqishi (PostgreSQL 18.6, PostGIS 3.6.4):

text
      nom      |             nom             |  metr  | gradus
---------------+-----------------------------+--------+--------
 Chorsu bozori | Amir Temur xiyoboni         |   4150 | 0.0477
 Chorsu bozori | Toshkent temir yo'l vokzali |   5896 | 0.0636
 Chorsu bozori | Samarqand Registon          | 266750 | 2.8109
(3 rows)

geography — 4 150 m: Chorsu'dan Amir Temur xiyobonigacha taxminan 4 km, haqiqatga mos. geometry — 0.0477 "gradus": bu tekislikdagi masofa, metrga aylantirib bo'lmaydi (bir gradus uzunlik ekvatorda ~111 km, Toshkentda ~84 km). Ilova uchun geography — "qancha metr" savoli uchun to'g'ri tur.

'SRID=4326;POINT(…)' — EWKT (Extended Well-Known Text) shakli: nuqtani oddiy matn bilan yozish, oldida SRID raqami bilan. Funksiya bilan ham bo'ladi: ST_SetSRID(ST_MakePoint(uzunlik, kenglik), 4326)::geography — e'tibor bering, bu yerda ham avval uzunlik.

Tekshirib ko'ring: Nega geometry uchun "0.0477 gradus" ni oddiy 111 000 ga ko'paytirib metr qilib bo'lmaydi?

Javob

Uzunlik gradusining metrdagi qiymati kenglikka bog'liq: ekvatorda ~111 km, Toshkentda (41°) ~84 km, qutbda 0. geometry masofasi esa ikki o'qni aralashtirib, tekislikdagi Pifagor bilan hisoblaydi — natija gradusning "aralash" birligi. Qaysi koeffitsientga ko'paytirish — yo'nalishga bog'liq. geography buni sferoid ustida to'g'ri hisoblaydi.

3. PostGIS'siz masofa: Haversine

3.1 Formulani SQL'da

PostGIS nima qilayotganini tushunish uchun masofani o'zimiz hisoblaymiz. Haversine formulasi — shar ustidagi ikki nuqta orasidagi eng qisqa yo'l. Uni oddiy Postgres'da, PostGIS'siz ishga tushirdik (PostgreSQL 17.6):

sql
CREATE TABLE m10_joylar (id int PRIMARY KEY, nom text,
  uzunlik float8, kenglik float8);
INSERT INTO m10_joylar VALUES
  (1, 'Chorsu bozori', 69.2346, 41.3266),
  (2, 'Amir Temur xiyoboni', 69.2797, 41.3111),
  (3, 'Toshkent temir yo''l vokzali', 69.2880, 41.2920),
  (4, 'Samarqand Registon', 66.9750, 39.6547);
SELECT b.nom, round(2 * 6371000 * asin(sqrt(
    sin(radians(b.kenglik - a.kenglik) / 2) ^ 2
    + cos(radians(a.kenglik)) * cos(radians(b.kenglik))
      * sin(radians(b.uzunlik - a.uzunlik) / 2) ^ 2))) AS metr
FROM m10_joylar a, m10_joylar b
WHERE a.id = 1 AND b.id > 1 ORDER BY b.id;
DROP TABLE m10_joylar;
text
CREATE TABLE
INSERT 0 4
             nom             |  metr
-----------------------------+--------
 Amir Temur xiyoboni         |   4142
 Toshkent temir yo'l vokzali |   5890
 Samarqand Registon          | 266578
(3 rows)

DROP TABLE

PostGIS'ning geography natijasi bilan solishtiring: 4 142 va 4 150 m, 266 578 va 266 750 m — farq 0.2 % atrofida. Haversine Yerni ideal shar (radius 6 371 km) deb oladi, PostGIS esa sferoid (qutblarda biroz yassilangan WGS 84 modeli). Taksi narxi uchun ikkalasi ham yetadi; geodeziya uchun — sferoid.

3.2 Xuddi shu — JavaScript'da

Ko'pincha masofa brauzer yoki serverda ham kerak (masalan, "kuryer 300 metr qoldi" xabari). Formula bir xil — bu blokni saytda «Ishga tushir» bilan sinab ko'ring:

js
const R = 6371000; // Yer radiusi, metr (o'rtacha)
const rad = (gradus) => (gradus * Math.PI) / 180;

function masofaMetr([uz1, ken1], [uz2, ken2]) {
  const dKen = rad(ken2 - ken1);
  const dUz = rad(uz2 - uz1);
  const a = Math.sin(dKen / 2) ** 2
    + Math.cos(rad(ken1)) * Math.cos(rad(ken2))
      * Math.sin(dUz / 2) ** 2;
  return Math.round(2 * R * Math.asin(Math.sqrt(a)));
}

const chorsu = [69.2346, 41.3266]; // [uzunlik, kenglik]
console.log(masofaMetr(chorsu, [69.2797, 41.3111])); // 4142
console.log(masofaMetr(chorsu, [66.9750, 39.6547])); // 266578
console.log(masofaMetr(chorsu, [41.3266, 69.2346])); // 3501472

Uchinchi qatorga qarang: o'sha Chorsu, faqat sonlar almashgan — va 3 501 km. Bu tuzoqqa PostGIS bo'limida qaytamiz.

Haversine bitta masofani yaxshi hisoblaydi. Lekin "200 000 nuqtadan 1 km ichidagilarni top" uchun har qatorda formula — to'liq o'qish. Indeks kerak — PostGIS shu uchun.

4. Asosiy so'rovlar

4.1 Radius ichida: ST_DWithin

"Amir Temur xiyobonidan 3 km ichidagi joylar":

sql
SELECT nom FROM joylar
WHERE ST_DWithin(joy,
  'SRID=4326;POINT(69.2797 41.3111)'::geography, 3000)
ORDER BY id;

Kanon chiqishi:

text
             nom
-----------------------------
 Amir Temur xiyoboni
 Toshkent temir yo'l vokzali
(2 rows)

Chorsu (4.15 km) — tashqarida, vokzal — ichida. ST_DWithin(a, b, metr) — "orasidagi masofa ≤ metr". Uni ST_Distance(a, b) <= 3000 bilan almashtirish mumkindek tuyuladi — natija bir xil. Lekin tezlik emas (pastda o'lchov).

4.2 Eng yaqinlar: KNN <->

"Menga eng yaqin 2 ta joy" — KNN (k-nearest neighbors, k ta eng yaqin qo'shni). <-> operatori — masofa; ORDER BY … <-> … LIMIT k ni GiST indeks to'g'ridan-to'g'ri bajaradi:

sql
SELECT nom,
       round(joy <-> 'SRID=4326;POINT(69.2400 41.3200)'::geography)
         AS metr
FROM joylar
ORDER BY joy <-> 'SRID=4326;POINT(69.2400 41.3200)'::geography
LIMIT 2;

Kanon chiqishi:

text
         nom         | metr
---------------------+------
 Chorsu bozori       |  861
 Amir Temur xiyoboni | 3460
(2 rows)

Foydalanuvchi Chorsu yaqinida (861 m). Indeks butun jadvalni saralamaydi — "eng yaqin" bloklardan boshlab, kerakli 2 tasini topgach to'xtaydi.

Maslahat: geography uchun <-> masofani shar modelida beradi (PostGIS hujjati), ST_Distance esa sferoidda. Farq kichik (~0.2 %), lekin aniq metr kerak bo'lsa — javobdagi masofani ST_Distance bilan hisoblang, <-> ni esa faqat saralash uchun qoldiring.

Amalda ikkalasi birga: "5 km ichidagi eng yaqin 10 ta filial" — WHERE ST_DWithin(…, 5000) ORDER BY joy <-> … LIMIT 10. ST_DWithin siz KNN yuzlab km uzoqdagi filialni ham "eng yaqin" deb qaytarishi mumkin (agar yaqinida boshqasi yo'q bo'lsa).

4.3 Poligon ichida nuqta

Yetkazib berish zonasi — poligon (ko'pburchak): "bu manzil zonamizdami?" Kanon taxminiy "Toshkent markazi" to'rtburchagini chizgan (oxirgi nuqta birinchisi bilan bir xil — poligon yopiq):

sql
CREATE TABLE hudud (nom text, chegara geography(Polygon, 4326));
INSERT INTO hudud VALUES ('Markaz (taxminiy)',
  'SRID=4326;POLYGON((69.22 41.28, 69.30 41.28, 69.30 41.34, '
  || '69.22 41.34, 69.22 41.28))');
SELECT j.nom, ST_Covers(h.chegara, j.joy) AS ichida
FROM joylar j CROSS JOIN hudud h ORDER BY j.id;
SELECT round((ST_Area(chegara) / 1e6)::numeric, 1) AS km2
FROM hudud;

Kanon chiqishi (kanon poligonni bitta satrda yozgan; biz telefon uchun || bilan bo'ldik):

text
             nom             | ichida
-----------------------------+--------
 Chorsu bozori               | t
 Amir Temur xiyoboni         | t
 Toshkent temir yo'l vokzali | t
 Samarqand Registon          | f
(4 rows)

 km2
------
 44.6
(1 row)

ST_Covers(zona, nuqta) — "zona nuqtani qoplaydimi" (chegaradagi nuqta ham kiradi). ST_Area — maydon kvadrat metrda; / 1e6 — km². Real ilovada zonalarni xaritada chizib, GeoJSON sifatida yuklashadi.

5. Tuzoq: kenglik va uzunlik almashsa

Kanon ataylab xato qildi — Chorsu koordinatalarini Google Maps tartibida (avval kenglik) yozdi:

sql
SELECT ST_AsText('SRID=4326;POINT(41.3266 69.2346)'::geography)
         AS teskari,
       round(ST_Distance(
         'SRID=4326;POINT(41.3266 69.2346)'::geography, joy) / 1000)
         AS km_xato
FROM joylar WHERE id = 1;

Kanon chiqishi:

text
        teskari         | km_xato
------------------------+---------
 POINT(41.3266 69.2346) |    3507
(1 row)

Xato yo'q, ogohlantirish yo'q. Nuqta shunchaki Arktikada (kenglik 69° — Shimoliy muz okeani yaqini) va asl Chorsu'dan 3 507 km uzoqda. Bizning JS hisobimiz ham shuni ko'rsatdi (3 501 km — shar va sferoid farqi). Baza buni ushlay olmaydi: 69 — haqiqiy kenglik. Himoya ilovada:

  • Ma'lumot qayerdan kelsa ham, kirishda nomlangan maydonlar: { kenglik, uzunlik } ([a, b] massiv emas).
  • Hudud tekshiruvi: O'zbekiston uchun kenglik ~37–46, uzunlik ~56–74. Tashqarida bo'lsa — xato.
  • Testda bitta ma'lum masofa (Chorsu → Amir Temur ≈ 4.1 km) — xato tartib darhol ko'rinadi.

Frontend xaritasi uchun natija odatda GeoJSON formatida beriladi. GeoJSON — xarita ma'lumotlari uchun kelishilgan JSON shakli (RFC 7946 standarti). Unda ham tartib [uzunlik, kenglik]:

sql
SELECT ST_AsGeoJSON(joy) FROM joylar WHERE id = 1;
text
                   st_asgeojson
--------------------------------------------------
 {"type":"Point","coordinates":[69.2346,41.3266]}
(1 row)

Leaflet (brauzerda xarita chizadigan JS kutubxonasi; kursda alohida o'tmaymiz, hozir bilish shart emas) esa L.marker([kenglik, uzunlik]) qabul qiladi — teskari! Bir loyihada ikki tartib uchraydi; har chegarada (baza ↔ API ↔ xarita) aniq bo'ling.

Tekshirib ko'ring: Telefon GPS'i latitude: 41.31, longitude: 69.28 qaytardi. PostGIS uchun EWKT satrini yozing.

Javob

'SRID=4326;POINT(69.28 41.31)' — avval uzunlik (longitude), keyin kenglik (latitude). Yoki ST_SetSRID(ST_MakePoint(69.28, 41.31), 4326)::geography. Nomlangan maydonlardan qurish xatoni kamaytiradi: ST_MakePoint(longitude, latitude).

6. Indeks: GiST va o'lchov

6.1 Nega ST_DWithin tez, ST_Distance < sekin

Kanon 200 000 tasodifiy nuqta (Toshkent atrofi) yaratib, 1 km radiusni o'lchagan. Indekssiz ikkala so'rov ham Parallel Seq Scan — 200 000 qatorning har birida formula (ST_DWithin 315 ms, ST_Distance < 1000 278 ms). Keyin GiST indeks:

sql
CREATE INDEX nuqtalar_joy_idx ON nuqtalar USING gist (joy);
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY ON)
SELECT count(*) FROM nuqtalar
WHERE ST_DWithin(joy,
  'SRID=4326;POINT(69.2797 41.3111)'::geography, 1000);

Kanon chiqishi (indeks 970 ms da qurildi, hajmi 14 MB — jadval bilan bir xil; qisqartirilgan — Buffers qatorlarisiz, uzun qatorlar psql'dagidek). Uzun hex satrlarga qaramang — bu shunchaki nuqtaning ichki yozuvi. Uchta narsaga qarang: Index Cond, Filter va actual rows sonlari.

text
 Aggregate (actual rows=1.00 loops=1)
   ->  Bitmap Heap Scan on nuqtalar (actual rows=718.00 loops=1)
         Filter: st_dwithin(joy, '0101000020E61000003D2CD49AE65151408FE4F21FD2A74440'::geography, '1000'::double precision, true)
         Rows Removed by Filter: 475
         ->  Bitmap Index Scan on nuqtalar_joy_idx (actual rows=1193.00 loops=1)
               Index Cond: (joy && _st_expand('0101000020E61000003D2CD49AE65151408FE4F21FD2A74440'::geography, '1000'::double precision))
 Execution Time: 2.348 ms

Nima bo'ldi? So'rov ikki bosqichda bajarildi:

  1. Index Cond: (joy && _st_expand(…)) — ST_DWithin ichida avval "nuqta atrofida 1 km lik quti" yasaladi va indeksdan shu qutiga tushgan nuqtalar olinadi (1 193 ta). && — "qutilar kesishadimi" — GiST buni juda tez qiladi.
  2. Filter: st_dwithin(…) — faqat shu 1 193 tasi uchun aniq masofa. Burchakdagilar (475 ta) tashlandi, 718 tasi — natija.

Diagrammada xuddi shu yo'l — 200 000 nuqtadan 718 tagacha:

flowchart TD
  A["200 000 nuqta"] --> B["GiST: 1 km lik quti<br/>joy && _st_expand"]
  B -->|"1 193 nomzod"| C["Aniq masofa<br/>st_dwithin"]
  C -->|"475 burchakda"| D["Tashlandi"]
  C -->|"718 ta"| E["Natija, 2.3 ms"]

Doira qutiga sig'adi, lekin qutining burchaklari doiradan tashqarida — shuning uchun ikkinchi, aniq bosqich kerak.

ST_Distance(…) < 1000 esa — "funksiya natijasi sondan kichik" — planner unda quti yasashni bilmaydi va indeksni ishlata olmaydi. Kanon bu so'rovni faqat indekssiz o'lchagan (278 ms); indeks bo'lganda ham uni ishlatmasligi — PostGIS hujjatiga ko'ra. Shuning uchun qoida: radius — doim ST_DWithin.

Eng yaqin 5 ta (KNN) — Index Scan … Order By: (joy <-> …), 0.19 ms: indeks nuqtalarni masofa tartibida beradi, 5 tadan keyin to'xtaydi.

200 000 nuqta, 1 km radius: so'rov vaqti (ms)
  • ST_DWithin, indekssiz315 ms
  • ST_Distance < 1000indekssiz; indeksni ishlata olmaydi (hujjat)278 ms
  • ST_DWithin + GiST2,3 ms
  • KNN top-5 + GiST0,19 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, PostGIS 3.6.4 (postgis/postgis:18-3.6), 200 000 tasodifiy nuqta

GiST — Indeks turlari darsida range va EXCLUDE uchun ko'rgan "ichida/kesishadi" indeksi. Geo uchun u nuqtalarni ichma-ich to'rtburchak qutilarga guruhlaydi (R-tree g'oyasi).

7. Halollik va chegaralar

  • Sinalmagan: Linux'dagi PostGIS paketlari, managed bazalardagi PostGIS (ko'p provayderlarda bor, versiyasi turlicha — Managed va serverless Postgres), geocoding (manzil → koordinata) xizmatlari.
  • geography funksiyalari geometry dagidan kam. Murakkab tahlil (buferlar, kesishma, marshrut) uchun geometry + mahalliy proyeksiya (masalan, UTM zonasi) ishlatiladi — bu GIS mutaxassisligi, kursimiz doirasidan tashqarida.
  • Real yo'l masofasi (ko'chalar bo'ylab) — boshqa vazifa: marshrutlash xizmatlari (OSRM va boshqalar). PostGIS "qush uchib o'tadigan" masofani beradi.
  • Geolokatsiya mavzusi (IP bo'yicha joy, kuryer kuzatuvi, Redis GEO, Mongo geo) — kursda alohida dars bor (Geolokatsiya); hozir bilish shart emas.

8. Ko'p uchraydigan xatolar

8.1 POINT(kenglik uzunlik)

Nuqta boshqa qit'ada, xato xabari yo'q. POINT(uzunlik kenglik), nomlangan maydonlar, hudud tekshiruvi.

8.2 geometry 4326 da masofa

Natija gradusda — ma'nosiz. Metr kerak bo'lsa — geography (yoki ::geography ga o'tkazish).

8.3 Radius — ST_Distance < N

Indeks ishlamaydi. ST_DWithin.

8.4 GiST indeksni unutish

Har so'rov — to'liq o'qish (kanon: 315 ms → 2.3 ms). CREATE INDEX … USING gist (joy).

8.5 KNN radiusiz

"Eng yaqin" yuzlab km uzoqda bo'lishi mumkin. ST_DWithin bilan chegaralang.

9. Mashqlar

PostGIS sizning kompyuteringizda bo'lmasligi mumkin — shuning uchun 1- va 2-mashq PostGIS'siz, 3-mashq — PostGIS so'rovini yozish (sinash — PostGIS bor bazada).

1-mashq (oson): Eng yaqin topshirish punkti (JS)

masofaMetr funksiyasidan foydalanib, foydalanuvchi [69.2400, 41.3200] nuqtasidan uchta punktgacha masofani hisoblang va eng yaqinini chiqaring: Chorsu [69.2346, 41.3266], Amir Temur [69.2797, 41.3111], Vokzal [69.2880, 41.2920]. Ishora: map va toSorted (Metod zanjiri va ma'lumot pipeline).

Yechim
js
const R = 6371000;
const rad = (gradus) => (gradus * Math.PI) / 180;
function masofaMetr([uz1, ken1], [uz2, ken2]) {
  const a = Math.sin(rad(ken2 - ken1) / 2) ** 2
    + Math.cos(rad(ken1)) * Math.cos(rad(ken2))
      * Math.sin(rad(uz2 - uz1) / 2) ** 2;
  return Math.round(2 * R * Math.asin(Math.sqrt(a)));
}

const men = [69.2400, 41.3200];
const punktlar = [
  { nom: "Chorsu", joy: [69.2346, 41.3266] },
  { nom: "Amir Temur", joy: [69.2797, 41.3111] },
  { nom: "Vokzal", joy: [69.2880, 41.2920] },
];
const saralangan = punktlar
  .map((p) => ({ nom: p.nom, metr: masofaMetr(men, p.joy) }))
  .toSorted((a, b) => a.metr - b.metr);
for (const p of saralangan) console.log(`${p.nom}: ${p.metr} m`);
console.log("Eng yaqini:", saralangan[0].nom);

Konsolda:

text
Chorsu: 861 m
Amir Temur: 3460 m
Vokzal: 5076 m
Eng yaqini: Chorsu

Chorsu — 861 m, Amir Temur — 3 460 m: kanondagi PostGIS KNN natijasi bilan aynan bir xil. Nega ST_Distance (4 150) bilan Haversine (4 142) farq qilgan-u, bu yerda farq yo'q? PostGIS hujjatiga ko'ra geography uchun <-> masofani shar ustida hisoblaydi (sferoid emas) — ya'ni bizning Haversine bilan bir xil model. Bu usul o'nlab punkt uchun yetadi; minglab bo'lsa — bazada GiST bilan.

2-mashq (o'rta): Radius ichidagilar (SQL, PostGIS'siz)

m10_filial (nom text, uzunlik float8, kenglik float8) jadvaliga uchta joyni qo'shing (1-mashqdagi punktlar) va [69.2400, 41.3200] dan 4 km ichidagilarni masofasi bilan, yaqinidan uzog'iga chiqaring. Haversine'ni m10_masofa(uz1, ken1, uz2, ken2) funksiyasiga o'rang (LANGUAGE sql IMMUTABLE, Funksiyalar va PL/pgSQL).

Yechim
sql
CREATE FUNCTION m10_masofa(uz1 float8, ken1 float8,
                           uz2 float8, ken2 float8)
RETURNS float8 LANGUAGE sql IMMUTABLE
RETURN 2 * 6371000 * asin(sqrt(
  sin(radians(ken2 - ken1) / 2) ^ 2
  + cos(radians(ken1)) * cos(radians(ken2))
    * sin(radians(uz2 - uz1) / 2) ^ 2));
CREATE TABLE m10_filial (nom text, uzunlik float8, kenglik float8);
INSERT INTO m10_filial VALUES ('Chorsu', 69.2346, 41.3266),
  ('Amir Temur', 69.2797, 41.3111), ('Vokzal', 69.2880, 41.2920);
SELECT nom || ': ' || round(m10_masofa(69.2400, 41.3200,
         uzunlik, kenglik)) || ' m'
FROM m10_filial
WHERE m10_masofa(69.2400, 41.3200, uzunlik, kenglik) <= 4000
ORDER BY m10_masofa(69.2400, 41.3200, uzunlik, kenglik);
DROP TABLE m10_filial;
DROP FUNCTION m10_masofa;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
Chorsu: 861 m
Amir Temur: 3460 m

Vokzal (5.1 km) tushib qoldi. Bu so'rov ishlaydi, lekin har qatorda formula — 200 000 filialda kanondagi indekssiz holat kabi sekin. Real ishda PostGIS + GiST yoki hech bo'lmasa avval "quti" bo'yicha filtr: kenglik BETWEEN 41.28 AND 41.36 kabi oddiy B-tree indeksli shart, keyin aniq formula.

3-mashq (qiyin): Yetkazib berish so'rovi (PostGIS)

"Bahor" oshxonasi filiallar (id, nom, joy geography(Point, 4326)) va zonalar (filial_id, chegara geography(Polygon, 4326)) jadvallarini ishlatadi. Mijoz [69.2400, 41.3200] da. Bitta so'rov yozing: mijozni zonasi qoplaydigan filiallar ichidan, 5 km dan yaqinlarini, eng yaqinidan boshlab, ko'pi bilan 3 tasini, masofasi (metr, butun) bilan qaytarsin. Qaysi indekslar kerak?

Yechim
sql
WITH mijoz AS (
  SELECT 'SRID=4326;POINT(69.2400 41.3200)'::geography AS nuqta)
SELECT f.nom, round(f.joy <-> m.nuqta) AS metr
FROM filiallar f
JOIN zonalar z ON z.filial_id = f.id
CROSS JOIN mijoz m
WHERE ST_Covers(z.chegara, m.nuqta)
  AND ST_DWithin(f.joy, m.nuqta, 5000)
ORDER BY f.joy <-> m.nuqta
LIMIT 3;

Bu so'rov bizning muhitda sinalmagan (PostGIS yo'q); naqsh kanondagi ST_Covers, ST_DWithin va <-> so'rovlaridan yig'ilgan. Indekslar: CREATE INDEX ON filiallar USING gist (joy); (radius va KNN), CREATE INDEX ON zonalar USING gist (chegara); (qoplash tekshiruvi), zonalar (filial_id) — JOIN uchun. Nuqtani WITH ga chiqarish — bir xil ifodani uch marta yozmaslik uchun (CTE (WITH)).

10. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar sxemasiga PostGIS qo'shilmagan. Sabablar:

  • Loyihada joylashuvga bog'liq imkoniyat yo'q ("vazifa joyi" g'oyasi kanonning misoli edi, API'da bunday maydon yo'q).
  • PostGIS boshqa Docker image talab qiladi — loyihaning postgres:18 image'idan voz kechish faqat kerakli imkoniyat uchun arziydi.
  • Kanon tajribasi alohida vaqtinchalik konteynerda o'tkazildi va ish tugagach o'chirildi — asosiy bazaga tegilmadi.

Agar o'z loyihangizda "vazifani joyga bog'lash" (masalan, "Chorsu'ga yetganda eslat") kerak bo'lsa — bu darsdagi joylar jadvali va GiST indeksi tayyor andoza.

11. Real ishda

  • Taksi va yetkazib berish xizmatlari — "eng yaqin haydovchi/kuryer" va "manzil zonadami" — har soniyada minglab KNN va poligon so'rovlari.
  • E'lonlar va ko'chmas mulk saytlari — "xaritada qidirish", "metro yaqinida".
  • Kuryer kuzatuvi — tez o'zgaradigan koordinatalar ko'pincha Redis GEO'da (Redis'ni shu qismda o'rganamiz — Redis nima va ulanish), tarix va tahlil — PostGIS'da.
  • Intervyu: "Eng yaqin 10 ta nuqtani qanday tez topasiz?", "geometry va geography farqi?".

Xulosa

  • PostGIS — alohida kengaytma (alohida image/paket); kanon PostGIS 3.6.4 da sinagan.
  • geography(Point, 4326) — masofa metrda; geometry 4326 da — gradusda.
  • POINT(uzunlik kenglik) — avval uzunlik; almashsa — xatosiz 3 507 km xato.
  • Radius — ST_DWithin (indeks quti bilan), ST_Distance < N emas; eng yaqinlar — ORDER BY <-> LIMIT k.
  • GiST indeks: 200 000 nuqtada 315 ms → 2.3 ms, KNN 0.19 ms.
  • PostGIS'siz — Haversine (shar modeli, ~0.2 % farq), lekin indekssiz. geography uchun <-> ham shar modelida hisoblaydi.

Keyingi dars: Rollar va huquqlar — ilova superuser bilan ulanmasligi uchun migratsiya, ilova va hisobot rollarini quramiz.

Manbalar

  • PostGIS hujjati: PostGIS 3.6 Manual — geography, ST_DWithin, ST_Distance, ST_Covers, <-> — 2026-10-25 da ko'rildi.
  • PostGIS hujjati: <-> operatori — "geography KNN is based on sphere rather than spheroid" — 2026-10-27 da ko'rildi.
  • PostGIS hujjati: Using PostGIS — spatial indexes.
  • EPSG:4326 (WGS 84) — epsg.io/4326.
  • GeoJSON standarti: RFC 7946 (koordinatalar tartibi — [longitude, latitude]).
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostGIS va geo so'rovlar: geography, STDWithin va eng yaqin nuqtalar — IlmHamroh