| | 1 | = Напредни бази на податоци = |
| | 2 | = Бонус — PostGIS просторни прашалници = |
| | 3 | == Проект: DriveNet == |
| | 4 | |
| | 5 | Александар Милошевски 231138 |
| | 6 | Исидора Кузмановска 231052 |
| | 7 | Андон Михајлов 231016 |
| | 8 | |
| | 9 | Во оваа фаза го прошируваме проектот DriveNet со PostGIS — просторно проширување за PostgreSQL кое овозможува складирање и пребарување на географски податоци. Наместо пресметување на растојанија во апликацискиот код, сите просторни операции се извршуваат директно во базата со индексирани геометриски колони. |
| | 10 | |
| | 11 | ---- |
| | 12 | |
| | 13 | = Зошто PostGIS? = |
| | 14 | |
| | 15 | Во нашата постоечка шема имаме `lat/lng` колони во следните табели: |
| | 16 | |
| | 17 | || **Табела** || **Употреба** || |
| | 18 | || `locations` || Места за качување и слегување || |
| | 19 | || `cities` || Центри на градови || |
| | 20 | || `location_pings` || Живо следење на возачот || |
| | 21 | || `toll_points` || Патарински точки на рутата || |
| | 22 | || `user_addresses` || Зачувани адреси на корисници || |
| | 23 | |
| | 24 | Без PostGIS, пребарување "возења во близина" би барало пресметување на Haversine формулата во апликацискиот код за секој ред. Со PostGIS, истото се прави со еден индексиран SQL прашалник. |
| | 25 | |
| | 26 | ---- |
| | 27 | |
| | 28 | = Инсталација и активација = |
| | 29 | |
| | 30 | {{{ |
| | 31 | -- Активирање на PostGIS проширувањето |
| | 32 | CREATE EXTENSION postgis; |
| | 33 | |
| | 34 | -- Верификација |
| | 35 | SELECT PostGIS_Version(); |
| | 36 | -- Резултат: 3.6 USE_GEOS=1 USE_PROJ=1 USE_STATS=1 |
| | 37 | }}} |
| | 38 | |
| | 39 | ---- |
| | 40 | |
| | 41 | = Миграција на постоечката шема = |
| | 42 | |
| | 43 | ==== Чекор 1 — Додавање на геометриски колони: |
| | 44 | {{{ |
| | 45 | ALTER TABLE locations ADD COLUMN geom GEOMETRY(Point, 4326); |
| | 46 | ALTER TABLE cities ADD COLUMN geom GEOMETRY(Point, 4326); |
| | 47 | ALTER TABLE location_pings ADD COLUMN geom GEOMETRY(Point, 4326); |
| | 48 | ALTER TABLE toll_points ADD COLUMN geom GEOMETRY(Point, 4326); |
| | 49 | ALTER TABLE user_addresses ADD COLUMN geom GEOMETRY(Point, 4326); |
| | 50 | }}} |
| | 51 | |
| | 52 | ==== Чекор 2 — Пополнување од постоечките lat/lng: |
| | 53 | {{{ |
| | 54 | UPDATE locations SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326); |
| | 55 | UPDATE cities SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326); |
| | 56 | UPDATE location_pings SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326); |
| | 57 | UPDATE toll_points SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326); |
| | 58 | UPDATE user_addresses SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326); |
| | 59 | }}} |
| | 60 | |
| | 61 | ==== Чекор 3 — Просторни индекси (GIST): |
| | 62 | {{{ |
| | 63 | CREATE INDEX idx_locations_geom ON locations USING GIST(geom); |
| | 64 | CREATE INDEX idx_cities_geom ON cities USING GIST(geom); |
| | 65 | CREATE INDEX idx_location_pings_geom ON location_pings USING GIST(geom); |
| | 66 | CREATE INDEX idx_toll_points_geom ON toll_points USING GIST(geom); |
| | 67 | CREATE INDEX idx_user_addresses_geom ON user_addresses USING GIST(geom); |
| | 68 | }}} |
| | 69 | |
| | 70 | Просторните индекси користат `GIST` (Generalized Search Tree) наместо стандардниот `BTREE` — специјализирани за двородимензионални геометриски пребарувања. |
| | 71 | |
| | 72 | ---- |
| | 73 | |
| | 74 | = Просторни функции = |
| | 75 | |
| | 76 | === 1. find_rides_near === |
| | 77 | |
| | 78 | ==== Опис: |
| | 79 | |
| | 80 | Ги наоѓа сите достапни возења чија почетна точка се наоѓа во одреден радиус од локацијата на патникот. Го заменува комплексното пресметување на растојанија во апликацискиот код со еден индексиран просторен прашалник. |
| | 81 | |
| | 82 | ==== Сигнатура: |
| | 83 | {{{ |
| | 84 | find_rides_near(p_lat DECIMAL, p_lng DECIMAL, p_radius_km INT DEFAULT 10) |
| | 85 | RETURNS TABLE (ride_id, departure_time, driver_name, driver_avg_rating, |
| | 86 | origin_city, origin_location, destination_city, |
| | 87 | estimated_fare, seats_available, distance_km) |
| | 88 | }}} |
| | 89 | |
| | 90 | ==== Влезни параметри: |
| | 91 | |
| | 92 | || **Параметар** || **Тип** || **Опис** || |
| | 93 | || `p_lat` || DECIMAL || Географска ширина на патникот || |
| | 94 | || `p_lng` || DECIMAL || Географска должина на патникот || |
| | 95 | || `p_radius_km` || INT || Радиус на пребарување во км (default 10) || |
| | 96 | |
| | 97 | ==== Логика: |
| | 98 | |
| | 99 | 1. Ја зема локацијата на патникот и ја претвора во PostGIS точка со `ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)` |
| | 100 | 2. Ги филтрира сите достапни возења преку `v_available_rides` со `ST_DWithin` — ги враќа само возењата чија почетна локација е во рамки на бараниот радиус |
| | 101 | 3. За секое возење го пресметува точното растојание во км со `ST_Distance` и `::geography` cast за метарска точност |
| | 102 | 4. Ги сортира резултатите по растојание |
| | 103 | |
| | 104 | ==== Имплементација: |
| | 105 | {{{ |
| | 106 | CREATE OR REPLACE FUNCTION find_rides_near( |
| | 107 | p_lat DECIMAL, |
| | 108 | p_lng DECIMAL, |
| | 109 | p_radius_km INT DEFAULT 10 |
| | 110 | ) |
| | 111 | RETURNS TABLE ( |
| | 112 | ride_id INT, |
| | 113 | departure_time TIMESTAMP, |
| | 114 | driver_name TEXT, |
| | 115 | driver_avg_rating NUMERIC, |
| | 116 | origin_city TEXT, |
| | 117 | origin_location TEXT, |
| | 118 | destination_city TEXT, |
| | 119 | estimated_fare NUMERIC, |
| | 120 | seats_available INT, |
| | 121 | distance_km NUMERIC |
| | 122 | ) |
| | 123 | LANGUAGE plpgsql |
| | 124 | AS $$ |
| | 125 | BEGIN |
| | 126 | RETURN QUERY |
| | 127 | SELECT |
| | 128 | var.ride_id, |
| | 129 | var.departure_time, |
| | 130 | var.driver_name::TEXT, |
| | 131 | var.driver_avg_rating, |
| | 132 | var.origin_city::TEXT, |
| | 133 | var.origin_location::TEXT, |
| | 134 | var.destination_city::TEXT, |
| | 135 | var.estimated_fare, |
| | 136 | var.seats_available, |
| | 137 | ROUND(ST_Distance( |
| | 138 | l.geom::geography, |
| | 139 | ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography |
| | 140 | )::numeric / 1000, 2) AS distance_km |
| | 141 | FROM v_available_rides var |
| | 142 | JOIN locations l ON l.name = var.origin_location |
| | 143 | WHERE ST_DWithin( |
| | 144 | l.geom::geography, |
| | 145 | ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography, |
| | 146 | p_radius_km * 1000 |
| | 147 | ) |
| | 148 | ORDER BY distance_km; |
| | 149 | END; |
| | 150 | $$; |
| | 151 | }}} |
| | 152 | |
| | 153 | ==== Пример за употреба: |
| | 154 | {{{ |
| | 155 | -- Пронајди возења во близина на центарот на Скопје (радиус 10km) |
| | 156 | SELECT * FROM find_rides_near(41.9981, 21.4254, 10); |
| | 157 | |
| | 158 | -- Пронајди возења во близина на одредена локација (радиус 25km) |
| | 159 | SELECT * FROM find_rides_near(41.9981, 21.4254, 25); |
| | 160 | }}} |
| | 161 | |
| | 162 | ==== Споредба — без PostGIS vs со PostGIS: |
| | 163 | |
| | 164 | Без PostGIS (Haversine во SQL): |
| | 165 | {{{ |
| | 166 | SELECT r.id, ... |
| | 167 | FROM rides r |
| | 168 | JOIN locations l ON ... |
| | 169 | WHERE ( |
| | 170 | 6371 * acos( |
| | 171 | cos(radians(41.9981)) * cos(radians(l.lat)) * |
| | 172 | cos(radians(l.lng) - radians(21.4254)) + |
| | 173 | sin(radians(41.9981)) * sin(radians(l.lat)) |
| | 174 | ) |
| | 175 | ) <= 10; |
| | 176 | -- Нема индекс — целосен скен на табелата |
| | 177 | }}} |
| | 178 | |
| | 179 | Со PostGIS: |
| | 180 | {{{ |
| | 181 | WHERE ST_DWithin( |
| | 182 | l.geom::geography, |
| | 183 | ST_SetSRID(ST_MakePoint(21.4254, 41.9981), 4326)::geography, |
| | 184 | 10000 |
| | 185 | ); |
| | 186 | -- Користи GIST индекс — директен просторен пристап |
| | 187 | }}} |
| | 188 | |
| | 189 | ---- |
| | 190 | |
| | 191 | === 2. find_nearest_stop === |
| | 192 | |
| | 193 | ==== Опис: |
| | 194 | |
| | 195 | За дадено возење и локација на патник, ги враќа сите постојки на рутата сортирани по растојание. Му помага на патникот да ја избере најблиската постојка за качување. |
| | 196 | |
| | 197 | ==== Сигнатура: |
| | 198 | {{{ |
| | 199 | find_nearest_stop(p_ride_id INT, p_lat DECIMAL, p_lng DECIMAL) |
| | 200 | RETURNS TABLE (stop_id, stop_order, location_name, distance_meters) |
| | 201 | }}} |
| | 202 | |
| | 203 | ==== Влезни параметри: |
| | 204 | |
| | 205 | || **Параметар** || **Тип** || **Опис** || |
| | 206 | || `p_ride_id` || INT || ID на возењето || |
| | 207 | || `p_lat` || DECIMAL || Географска ширина на патникот || |
| | 208 | || `p_lng` || DECIMAL || Географска должина на патникот || |
| | 209 | |
| | 210 | ==== Логика: |
| | 211 | |
| | 212 | 1. Го наоѓа `route_id` на возењето |
| | 213 | 2. За сите постојки на рутата го пресметува растојанието во метри од локацијата на патникот со `ST_Distance` |
| | 214 | 3. Ги сортира по растојание — најблиската постојка е прва |
| | 215 | |
| | 216 | ==== Имплементација: |
| | 217 | {{{ |
| | 218 | CREATE OR REPLACE FUNCTION find_nearest_stop( |
| | 219 | p_ride_id INT, |
| | 220 | p_lat DECIMAL, |
| | 221 | p_lng DECIMAL |
| | 222 | ) |
| | 223 | RETURNS TABLE ( |
| | 224 | stop_id INT, |
| | 225 | stop_order INT, |
| | 226 | location_name TEXT, |
| | 227 | distance_meters NUMERIC |
| | 228 | ) |
| | 229 | LANGUAGE plpgsql |
| | 230 | AS $$ |
| | 231 | BEGIN |
| | 232 | RETURN QUERY |
| | 233 | SELECT |
| | 234 | rs.id AS stop_id, |
| | 235 | rs.stop_order, |
| | 236 | l.name::TEXT AS location_name, |
| | 237 | ROUND(ST_Distance( |
| | 238 | l.geom::geography, |
| | 239 | ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography |
| | 240 | )::numeric, 2) AS distance_meters |
| | 241 | FROM route_stops rs |
| | 242 | JOIN locations l ON l.id = rs.location_id |
| | 243 | WHERE rs.route_id = (SELECT route_id FROM rides WHERE id = p_ride_id) |
| | 244 | ORDER BY distance_meters; |
| | 245 | END; |
| | 246 | $$; |
| | 247 | }}} |
| | 248 | |
| | 249 | ==== Пример за употреба: |
| | 250 | {{{ |
| | 251 | -- Најди најблиска постојка за возење 1, патник е во центарот на Скопје |
| | 252 | SELECT * FROM find_nearest_stop(1, 41.9981, 21.4254); |
| | 253 | }}} |
| | 254 | |
| | 255 | ---- |
| | 256 | |
| | 257 | = Дополнителни просторни прашалници = |
| | 258 | |
| | 259 | ==== Локации во рамки на одреден радиус: |
| | 260 | {{{ |
| | 261 | SELECT |
| | 262 | l.name, |
| | 263 | c.name AS city, |
| | 264 | ROUND(ST_Distance( |
| | 265 | l.geom::geography, |
| | 266 | ST_SetSRID(ST_MakePoint(21.4254, 41.9981), 4326)::geography |
| | 267 | )::numeric / 1000, 2) AS distance_km |
| | 268 | FROM locations l |
| | 269 | JOIN cities c ON c.id = l.city_id |
| | 270 | ORDER BY distance_km |
| | 271 | LIMIT 10; |
| | 272 | }}} |
| | 273 | |
| | 274 | ==== Патарински точки долж рута: |
| | 275 | {{{ |
| | 276 | SELECT tp.name, tp.price_car |
| | 277 | FROM toll_points tp |
| | 278 | WHERE ST_DWithin( |
| | 279 | tp.geom::geography, |
| | 280 | ST_MakeLine( |
| | 281 | ST_SetSRID(ST_MakePoint(21.43, 41.99), 4326), |
| | 282 | ST_SetSRID(ST_MakePoint(22.64, 41.04), 4326) |
| | 283 | )::geography, |
| | 284 | 500 |
| | 285 | ); |
| | 286 | }}} |
| | 287 | |
| | 288 | ==== Најблизок град до одредена точка: |
| | 289 | {{{ |
| | 290 | SELECT |
| | 291 | name, |
| | 292 | ROUND(ST_Distance( |
| | 293 | geom::geography, |
| | 294 | ST_SetSRID(ST_MakePoint(21.4254, 41.9981), 4326)::geography |
| | 295 | )::numeric / 1000, 2) AS distance_km |
| | 296 | FROM cities |
| | 297 | ORDER BY distance_km |
| | 298 | LIMIT 5; |
| | 299 | }}} |
| | 300 | |
| | 301 | ---- |
| | 302 | |
| | 303 | = Резиме = |
| | 304 | |
| | 305 | || **Функција** || **PostGIS функција** || **Опис** || |
| | 306 | || Растојание меѓу две точки || `ST_Distance` || Точно растојание во метри || |
| | 307 | || Точки во радиус || `ST_DWithin` || Индексирано просторно филтрирање || |
| | 308 | || Креирање точка || `ST_MakePoint` || Од lng/lat во геометрија || |
| | 309 | || Поставување координатен систем || `ST_SetSRID(..., 4326)` || WGS84 — стандард за GPS || |
| | 310 | || Просторен индекс || `USING GIST` || Наместо BTREE за 2D пребарување || |
| | 311 | || Geography cast || `::geography` || Метарска точност наместо степени || |
| | 312 | |
| | 313 | Клучната предност на PostGIS е комбинацијата од `ST_DWithin` + `GIST` индекс — просторното филтрирање се извршува директно во индексот без скенирање на целата табела, слично како `idx_rides_status_dep` го забрза пребарувањето на возења. |