| 294 | | == 10. Кориснички адреси во радиус од 2 km == |
| 295 | | |
| 296 | | {{{ |
| 297 | | SELECT count(*), max(address.address_id) |
| 298 | | FROM public.customer_addresses address |
| 299 | | JOIN public.network_sites site ON site.site_id = :site_id |
| 300 | | WHERE address.location IS NOT NULL |
| 301 | | AND address.location && ST_Envelope( |
| 302 | | ST_Buffer(site.location::geography, 2000)::geometry |
| 303 | | ) |
| 304 | | AND ST_DWithin( |
| 305 | | address.location::geography, |
| 306 | | site.location::geography, |
| 307 | | 2000 |
| 308 | | ); |
| 309 | | }}} |
| 310 | | |
| 311 | | === Пред оптимизацијата |
| 312 | | |
| 313 | | Времето било **39.591 ms**. Без индекс PostgreSQL ја чита customer_addresses и ја проверува оддалеченоста за голем број адреси. |
| 314 | | |
| 315 | | === Избран индекс |
| 316 | | |
| 317 | | {{{ |
| 318 | | CREATE INDEX idx_customer_addresses_location_gist |
| 319 | | ON public.customer_addresses |
| 320 | | USING gist (location); |
| 321 | | }}} |
| 322 | | |
| 323 | | GiST индексот го користи условот `location && ...` за да ги избере точките во приближната област. `ST_DWithin` потоа го проверува точното растојание само за тие кандидати. Во планот ова може да се појави како `Bitmap Index Scan`. |
| 324 | | |
| 325 | | === По оптимизацијата |
| 326 | | |
| 327 | | Времето се намалило на **1.149 ms**. Прашалникот е **34.46 пати побрз**, односно времето е намалено за **97.1%**. Ова е најголемото GIS подобрување. |
| 328 | | |
| 329 | | |
| 330 | | == Вкупен резултат == |
| 331 | | |
| 332 | | За десетте прашалници, збирот на медијаните се намалил од **452.530 ms** на **204.042 ms**. Вкупно се заштедени **248.488 ms**, што е намалување од **54.9%** и забрзување од **2.22 пати**. |
| 333 | | |
| 334 | | Најголемо подобрување има кај сообраќајот по сектор и GIS пребарувањето на адреси во радиус од 2 km. Во двата случаи EXPLAIN ANALYZE покажува дека проблемот бил читање голем број непотребни редови, а индексот овозможува директен пристап до релевантното множество. |
| 335 | | |
| 336 | | Индексите се избрани според реалните услови во прашалниците. Колоните во `Index Cond` се клучеви на индексот, колоните потребни само за резултатот се ставаат во INCLUDE, а за просторни операции се користи GiST. Така секој задржан индекс има конкретна намена и измерено подобрување. |
| 337 | | |
| 338 | | == SQL дефиниции на поврзаните погледи и функции == |
| 339 | | |
| 340 | | Подолу е вметнат SQL кодот што претходно беше во посебните датотеки. |
| 341 | | |
| 342 | | === Customer Subscription Overview === |
| 343 | | |
| 344 | | {{{ |
| 345 | | DROP VIEW IF EXISTS public.v_customer_subscription_overview; |
| 346 | | |
| 347 | | CREATE VIEW public.v_customer_subscription_overview AS |
| 348 | | SELECT |
| 349 | | c.customer_id, |
| 350 | | CASE |
| 351 | | WHEN c.customer_type = 'business' THEN c.company_name || ' (' || c.first_name || ' ' || c.last_name || ')' |
| 352 | | ELSE c.first_name || ' ' || c.last_name |
| 353 | | END AS customer_name, |
| 354 | | c.customer_type, |
| 355 | | c.email, |
| 356 | | a.account_id, |
| 357 | | a.account_number, |
| 358 | | a.account_status, |
| 359 | | a.current_balance, |
| 360 | | bc.cycle_name AS billing_cycle, |
| 361 | | s.subscription_id, |
| 362 | | s.subscription_number, |
| 363 | | s.status AS subscription_status, |
| 364 | | s.activation_date, |
| 365 | | s.end_date, |
| 366 | | p.plan_name, |
| 367 | | p.monthly_fee, |
| 368 | | con.contract_number, |
| 369 | | con.contract_type, |
| 370 | | con.status AS contract_status, |
| 371 | | current_sim.msisdn, |
| 372 | | current_sim.sim_type, |
| 373 | | current_device.manufacturer AS device_manufacturer, |
| 374 | | current_device.model AS device_model, |
| 375 | | current_device.device_type, |
| 376 | | COALESCE(active_addons.recurring_addon_charge, 0) AS recurring_addon_charge, |
| 377 | | p.monthly_fee + COALESCE(active_addons.recurring_addon_charge, 0) AS total_monthly_recurring_charge |
| 378 | | FROM public.customers c |
| 379 | | JOIN public.accounts a ON a.customer_id = c.customer_id |
| 380 | | LEFT JOIN public.billing_cycles bc ON bc.billing_cycle_id = a.billing_cycle_id |
| 381 | | JOIN public.subscriptions s ON s.account_id = a.account_id |
| 382 | | JOIN public.plans p ON p.plan_id = s.plan_id |
| 383 | | LEFT JOIN public.contracts con ON con.contract_id = s.contract_id |
| 384 | | LEFT JOIN LATERAL ( |
| 385 | | SELECT sc.msisdn, sc.sim_type |
| 386 | | FROM public.sim_card_subscription_history ssh |
| 387 | | JOIN public.sim_cards sc ON sc.sim_id = ssh.sim_id |
| 388 | | WHERE ssh.subscription_id = s.subscription_id |
| 389 | | AND ssh.end_date IS NULL |
| 390 | | ORDER BY ssh.start_date DESC |
| 391 | | LIMIT 1 |
| 392 | | ) current_sim ON TRUE |
| 393 | | LEFT JOIN LATERAL ( |
| 394 | | SELECT d.manufacturer, d.model, d.device_type |
| 395 | | FROM public.device_assignments da |
| 396 | | JOIN public.devices d ON d.device_id = da.device_id |
| 397 | | WHERE da.subscription_id = s.subscription_id |
| 398 | | AND da.assigned_to IS NULL |
| 399 | | ORDER BY da.assigned_from DESC |
| 400 | | LIMIT 1 |
| 401 | | ) current_device ON TRUE |
| 402 | | LEFT JOIN LATERAL ( |
| 403 | | SELECT SUM(sa.price_at_activation) AS recurring_addon_charge |
| 404 | | FROM public.subscription_addons sa |
| 405 | | JOIN public.addons ad ON ad.addon_id = sa.addon_id |
| 406 | | WHERE sa.subscription_id = s.subscription_id |
| 407 | | AND sa.status = 'active' |
| 408 | | AND sa.deactivation_date IS NULL |
| 409 | | AND ad.is_recurring |
| 410 | | ) active_addons ON TRUE; |
| 411 | | }}} |
| 412 | | |
| 413 | | === Customer Call History === |
| 414 | | |
| 415 | | {{{ |
| 416 | | DROP VIEW IF EXISTS public.v_customer_call_history; |
| 417 | | |
| 418 | | CREATE VIEW public.v_customer_call_history AS |
| 419 | | SELECT |
| 420 | | c.customer_id, |
| 421 | | CASE WHEN c.customer_type = 'business' THEN c.company_name ELSE c.first_name || ' ' || c.last_name END AS customer_name, |
| 422 | | a.account_number, |
| 423 | | s.subscription_number, |
| 424 | | p.plan_name, |
| 425 | | cdr.call_cdr_id, |
| 426 | | cdr.originating_msisdn AS from_number, |
| 427 | | cdr.destination_msisdn AS to_number, |
| 428 | | cdr.event_start_time AS call_started_at, |
| 429 | | cdr.event_end_time AS call_ended_at, |
| 430 | | ROUND(cdr.duration_seconds::numeric / 60.0, 2) AS duration_minutes, |
| 431 | | cdr.call_type, |
| 432 | | cdr.direction, |
| 433 | | cdr.charge_amount, |
| 434 | | cdr.fraud_score, |
| 435 | | cdr.roaming_partner_id IS NOT NULL AS is_roaming, |
| 436 | | COALESCE(rp.country, 'North Macedonia') AS network_country, |
| 437 | | ns.region AS network_region, |
| 438 | | ns.site_code, |
| 439 | | ct.tower_code, |
| 440 | | ts.sector_label, |
| 441 | | ts.frequency_band, |
| 442 | | nt.generation AS network_generation |
| 443 | | FROM public.customers c |
| 444 | | JOIN public.accounts a ON a.customer_id = c.customer_id |
| 445 | | JOIN public.subscriptions s ON s.account_id = a.account_id |
| 446 | | JOIN public.plans p ON p.plan_id = s.plan_id |
| 447 | | JOIN public.usage_cdr_calls cdr ON cdr.subscription_id = s.subscription_id |
| 448 | | LEFT JOIN public.roaming_partners rp ON rp.roaming_partner_id = cdr.roaming_partner_id |
| 449 | | LEFT JOIN public.tower_sectors ts ON ts.sector_id = cdr.sector_id |
| 450 | | LEFT JOIN public.cell_towers ct ON ct.tower_id = ts.tower_id |
| 451 | | LEFT JOIN public.network_sites ns ON ns.site_id = ct.site_id |
| 452 | | LEFT JOIN public.network_technologies nt ON nt.technology_id = ts.technology_id; |
| 453 | | }}} |
| 454 | | |
| 455 | | === Customer Data Usage History === |
| 456 | | |
| 457 | | {{{ |
| 458 | | DROP VIEW IF EXISTS public.v_customer_data_usage_history; |
| 459 | | |
| 460 | | CREATE VIEW public.v_customer_data_usage_history AS |
| 461 | | SELECT |
| 462 | | c.customer_id, |
| 463 | | CASE WHEN c.customer_type = 'business' THEN c.company_name ELSE c.first_name || ' ' || c.last_name END AS customer_name, |
| 464 | | a.account_number, |
| 465 | | s.subscription_number, |
| 466 | | p.plan_name, |
| 467 | | d.data_cdr_id, |
| 468 | | d.session_start, |
| 469 | | d.session_end, |
| 470 | | ROUND(EXTRACT(EPOCH FROM (d.session_end - d.session_start))::numeric / 60.0, 2) AS session_duration_minutes, |
| 471 | | d.data_used_mb, |
| 472 | | ROUND(d.data_used_mb / 1024.0, 4) AS data_used_gb, |
| 473 | | d.apn, |
| 474 | | d.ip_address, |
| 475 | | d.charge_amount, |
| 476 | | d.roaming_partner_id IS NOT NULL AS is_roaming, |
| 477 | | COALESCE(rp.country, 'North Macedonia') AS network_country, |
| 478 | | ns.region AS network_region, |
| 479 | | ns.site_code, |
| 480 | | ts.sector_label, |
| 481 | | ts.frequency_band, |
| 482 | | nt.generation AS network_generation |
| 483 | | FROM public.customers c |
| 484 | | JOIN public.accounts a ON a.customer_id = c.customer_id |
| 485 | | JOIN public.subscriptions s ON s.account_id = a.account_id |
| 486 | | JOIN public.plans p ON p.plan_id = s.plan_id |
| 487 | | JOIN public.usage_cdr_data d ON d.subscription_id = s.subscription_id |
| 488 | | LEFT JOIN public.roaming_partners rp ON rp.roaming_partner_id = d.roaming_partner_id |
| 489 | | LEFT JOIN public.tower_sectors ts ON ts.sector_id = d.sector_id |
| 490 | | LEFT JOIN public.cell_towers ct ON ct.tower_id = ts.tower_id |
| 491 | | LEFT JOIN public.network_sites ns ON ns.site_id = ct.site_id |
| 492 | | LEFT JOIN public.network_technologies nt ON nt.technology_id = ts.technology_id; |
| 493 | | }}} |
| 494 | | |
| 495 | | === Customer Daily Usage Summary === |
| 496 | | |
| 497 | | {{{ |
| 498 | | DROP VIEW IF EXISTS public.v_customer_daily_usage_summary; |
| 499 | | |
| 500 | | CREATE VIEW public.v_customer_daily_usage_summary AS |
| 501 | | SELECT |
| 502 | | c.customer_id, |
| 503 | | CASE WHEN c.customer_type = 'business' THEN c.company_name ELSE c.first_name || ' ' || c.last_name END AS customer_name, |
| 504 | | a.account_number, |
| 505 | | s.subscription_id, |
| 506 | | s.subscription_number, |
| 507 | | p.plan_name, |
| 508 | | uad.usage_date, |
| 509 | | ROUND(uad.total_call_seconds::numeric / 60.0, 2) AS total_call_minutes, |
| 510 | | uad.total_sms_count, |
| 511 | | ROUND(uad.total_data_mb / 1024.0, 4) AS total_data_gb, |
| 512 | | uad.total_charge_amount, |
| 513 | | SUM(uad.total_data_mb) OVER ( |
| 514 | | PARTITION BY s.subscription_id, date_trunc('month', uad.usage_date::timestamp) |
| 515 | | ORDER BY uad.usage_date |
| 516 | | ) AS month_to_date_data_mb |
| 517 | | FROM public.customers c |
| 518 | | JOIN public.accounts a ON a.customer_id = c.customer_id |
| 519 | | JOIN public.subscriptions s ON s.account_id = a.account_id |
| 520 | | JOIN public.plans p ON p.plan_id = s.plan_id |
| 521 | | JOIN public.usage_aggregates_daily uad ON uad.subscription_id = s.subscription_id; |
| 522 | | }}} |
| 523 | | |
| 524 | | === Customer Support Ticket Timeline === |
| 525 | | |
| 526 | | {{{ |
| 527 | | DROP VIEW IF EXISTS public.v_customer_support_ticket_timeline; |
| 528 | | |
| 529 | | CREATE VIEW public.v_customer_support_ticket_timeline AS |
| 530 | | SELECT |
| 531 | | c.customer_id, |
| 532 | | CASE WHEN c.customer_type = 'business' THEN c.company_name ELSE c.first_name || ' ' || c.last_name END AS customer_name, |
| 533 | | a.account_number, |
| 534 | | s.subscription_number, |
| 535 | | p.plan_name, |
| 536 | | t.ticket_id, |
| 537 | | t.ticket_type, |
| 538 | | t.subject, |
| 539 | | t.priority, |
| 540 | | t.status AS ticket_status, |
| 541 | | t.created_at AS ticket_created_at, |
| 542 | | t.closed_at AS ticket_closed_at, |
| 543 | | ROUND(EXTRACT(EPOCH FROM (t.closed_at - t.created_at))::numeric / 3600.0, 2) AS resolution_hours, |
| 544 | | owner.first_name || ' ' || owner.last_name AS assigned_employee, |
| 545 | | ci.interaction_id, |
| 546 | | ci.interaction_type, |
| 547 | | ci.channel, |
| 548 | | ci.interaction_time, |
| 549 | | ci.notes, |
| 550 | | ci.old_status, |
| 551 | | ci.new_status, |
| 552 | | actor.first_name || ' ' || actor.last_name AS interaction_employee, |
| 553 | | ROW_NUMBER() OVER (PARTITION BY t.ticket_id ORDER BY ci.interaction_time NULLS LAST, ci.interaction_id) AS interaction_sequence |
| 554 | | FROM public.customers c |
| 555 | | JOIN public.crm_tickets t ON t.customer_id = c.customer_id |
| 556 | | LEFT JOIN public.accounts a ON a.account_id = t.account_id |
| 557 | | LEFT JOIN public.subscriptions s ON s.subscription_id = t.subscription_id |
| 558 | | LEFT JOIN public.plans p ON p.plan_id = s.plan_id |
| 559 | | LEFT JOIN public.employees owner ON owner.employee_id = t.assigned_employee_id |
| 560 | | LEFT JOIN public.crm_interactions ci ON ci.ticket_id = t.ticket_id |
| 561 | | LEFT JOIN public.employees actor ON actor.employee_id = ci.employee_id; |
| 562 | | }}} |
| 563 | | |
| 564 | | === Network Outage Operations === |
| 565 | | |
| 566 | | {{{ |
| 567 | | DROP VIEW IF EXISTS public.v_network_outage_operations; |
| 568 | | |
| 569 | | CREATE VIEW public.v_network_outage_operations AS |
| 570 | | WITH assignment_summary AS ( |
| 571 | | SELECT outage_id, |
| 572 | | COUNT(*) AS assignment_count, |
| 573 | | COUNT(*) FILTER (WHERE assignment_type = 'outage_lead') AS lead_assignments, |
| 574 | | COUNT(*) FILTER (WHERE assignment_type = 'field_support') AS field_assignments, |
| 575 | | MIN(start_time) AS first_assignment_at, |
| 576 | | MAX(end_time) AS last_assignment_end |
| 577 | | FROM public.employee_assignments |
| 578 | | WHERE outage_id IS NOT NULL |
| 579 | | GROUP BY outage_id |
| 580 | | ) |
| 581 | | SELECT |
| 582 | | o.outage_id, |
| 583 | | ns.site_id, |
| 584 | | ns.site_code, |
| 585 | | ns.site_name, |
| 586 | | ns.region, |
| 587 | | o.outage_type, |
| 588 | | o.status, |
| 589 | | o.start_time, |
| 590 | | o.end_time, |
| 591 | | CASE WHEN o.end_time IS NOT NULL |
| 592 | | THEN ROUND(EXTRACT(EPOCH FROM (o.end_time - o.start_time))::numeric / 60.0, 2) |
| 593 | | END AS outage_duration_minutes, |
| 594 | | CASE |
| 595 | | WHEN o.end_time IS NULL THEN 'open' |
| 596 | | WHEN o.end_time - o.start_time <= INTERVAL '60 minutes' THEN 'under_1_hour' |
| 597 | | WHEN o.end_time - o.start_time <= INTERVAL '4 hours' THEN 'one_to_four_hours' |
| 598 | | ELSE 'over_4_hours' |
| 599 | | END AS duration_band, |
| 600 | | o.root_cause, |
| 601 | | na.alarm_id, |
| 602 | | na.alarm_type, |
| 603 | | na.severity AS alarm_severity, |
| 604 | | na.raised_at AS alarm_raised_at, |
| 605 | | COALESCE(ass.assignment_count, 0) AS assignment_count, |
| 606 | | COALESCE(ass.lead_assignments, 0) AS lead_assignments, |
| 607 | | COALESCE(ass.field_assignments, 0) AS field_assignments, |
| 608 | | ass.first_assignment_at, |
| 609 | | ass.last_assignment_end |
| 610 | | FROM public.outages o |
| 611 | | JOIN public.network_sites ns ON ns.site_id = o.site_id |
| 612 | | LEFT JOIN public.network_alarms na ON na.alarm_id = o.alarm_id |
| 613 | | LEFT JOIN assignment_summary ass ON ass.outage_id = o.outage_id; |
| 614 | | }}} |
| 615 | | |
| 616 | | === Sector Traffic Map === |
| 617 | | |
| 618 | | {{{ |
| 619 | | -- CDR activity summarized per map polygon without multiplying event rows. |
| 620 | | -- QGIS polygon layer. Unique key: coverage_zone_id; geometry: coverage_area. |
| 621 | | CREATE OR REPLACE VIEW public.v_sector_traffic_map AS |
| 622 | | WITH call_stats AS |
| 623 | | ( |
| 624 | | SELECT |
| 625 | | sector_id, |
| 626 | | count(*) AS call_count, |
| 627 | | coalesce(sum(duration_seconds), 0) AS call_seconds, |
| 628 | | max(event_start_time) AS last_call_at |
| 629 | | FROM public.usage_cdr_calls |
| 630 | | WHERE sector_id IS NOT NULL |
| 631 | | GROUP BY sector_id |
| 632 | | ), |
| 633 | | sms_stats AS |
| 634 | | ( |
| 635 | | SELECT |
| 636 | | sector_id, |
| 637 | | count(*) AS sms_count, |
| 638 | | max(event_time) AS last_sms_at |
| 639 | | FROM public.usage_cdr_sms |
| 640 | | WHERE sector_id IS NOT NULL |
| 641 | | GROUP BY sector_id |
| 642 | | ), |
| 643 | | data_stats AS |
| 644 | | ( |
| 645 | | SELECT |
| 646 | | sector_id, |
| 647 | | count(*) AS data_session_count, |
| 648 | | coalesce(sum(data_used_mb), 0) AS data_used_mb, |
| 649 | | max(session_start) AS last_data_at |
| 650 | | FROM public.usage_cdr_data |
| 651 | | WHERE sector_id IS NOT NULL |
| 652 | | GROUP BY sector_id |
| 653 | | ) |
| 654 | | SELECT |
| 655 | | coverage.coverage_zone_id, |
| 656 | | coverage.site_id, |
| 657 | | coverage.site_code, |
| 658 | | coverage.site_name, |
| 659 | | coverage.region, |
| 660 | | coverage.tower_id, |
| 661 | | coverage.tower_code, |
| 662 | | coverage.sector_id, |
| 663 | | coverage.sector_label, |
| 664 | | coverage.technology_name, |
| 665 | | coverage.generation, |
| 666 | | coalesce(calls.call_count, 0) AS call_count, |
| 667 | | coalesce(calls.call_seconds, 0) AS call_seconds, |
| 668 | | coalesce(messages.sms_count, 0) AS sms_count, |
| 669 | | coalesce(data_usage.data_session_count, 0) AS data_session_count, |
| 670 | | coalesce(data_usage.data_used_mb, 0) AS data_used_mb, |
| 671 | | coalesce(calls.call_count, 0) |
| 672 | | + coalesce(messages.sms_count, 0) |
| 673 | | + coalesce(data_usage.data_session_count, 0) AS total_events, |
| 674 | | greatest(calls.last_call_at, messages.last_sms_at, data_usage.last_data_at) |
| 675 | | AS last_event_at, |
| 676 | | coverage.coverage_area |
| 677 | | FROM public.v_network_coverage_map coverage |
| 678 | | LEFT JOIN call_stats calls ON calls.sector_id = coverage.sector_id |
| 679 | | LEFT JOIN sms_stats messages ON messages.sector_id = coverage.sector_id |
| 680 | | LEFT JOIN data_stats data_usage ON data_usage.sector_id = coverage.sector_id; |
| 681 | | }}} |
| 682 | | |
| 683 | | === Customer Coverage Detail === |
| 684 | | |
| 685 | | {{{ |
| 686 | | -- One row for every active sector covering an active customer's primary address. |
| 687 | | -- QGIS point layer. Unique key can be customer_id + coverage_zone_id. |
| 688 | | CREATE OR REPLACE VIEW public.v_customer_coverage_detail AS |
| 689 | | SELECT |
| 690 | | c.customer_id, |
| 691 | | concat_ws(' ', c.first_name, c.last_name) AS customer_name, |
| 692 | | ca.address_id, |
| 693 | | concat_ws(', ', ca.street, ca.city, ca.country) AS full_address, |
| 694 | | cz.coverage_zone_id, |
| 695 | | ns.site_id, |
| 696 | | ns.site_code, |
| 697 | | ns.site_name, |
| 698 | | ct.tower_code, |
| 699 | | ts.sector_id, |
| 700 | | ts.sector_label, |
| 701 | | nt.technology_name, |
| 702 | | nt.generation, |
| 703 | | cz.signal_quality_score, |
| 704 | | round( |
| 705 | | ST_Distance(ca.location::geography, ns.location::geography)::numeric, |
| 706 | | 1 |
| 707 | | ) AS distance_to_site_m, |
| 708 | | ca.location AS customer_location |
| 709 | | FROM public.customers c |
| 710 | | JOIN public.customer_addresses ca |
| 711 | | ON ca.customer_id = c.customer_id |
| 712 | | AND ca.is_primary |
| 713 | | JOIN public.coverage_zones cz |
| 714 | | ON ca.location IS NOT NULL |
| 715 | | AND cz.coverage_area IS NOT NULL |
| 716 | | AND ST_Covers(cz.coverage_area, ca.location) |
| 717 | | JOIN public.tower_sectors ts ON ts.sector_id = cz.sector_id |
| 718 | | JOIN public.cell_towers ct ON ct.tower_id = ts.tower_id |
| 719 | | JOIN public.network_sites ns ON ns.site_id = ct.site_id |
| 720 | | JOIN public.network_technologies nt |
| 721 | | ON nt.technology_id = ts.technology_id |
| 722 | | WHERE c.status = 'active' |
| 723 | | AND ns.status = 'active' |
| 724 | | AND ct.status = 'active' |
| 725 | | AND ts.status = 'active'; |
| 726 | | }}} |
| 727 | | |
| 728 | | === Network Site Points === |
| 729 | | |
| 730 | | {{{ |
| 731 | | -- QGIS point layer. Unique key: site_id; geometry: location. |
| 732 | | CREATE OR REPLACE VIEW public.v_network_site_points AS |
| 733 | | SELECT |
| 734 | | ns.site_id, |
| 735 | | ns.site_code, |
| 736 | | ns.site_name, |
| 737 | | ns.address, |
| 738 | | ns.region, |
| 739 | | ns.site_type, |
| 740 | | ns.status, |
| 741 | | ns.opened_at, |
| 742 | | ns.location |
| 743 | | FROM public.network_sites ns |
| 744 | | WHERE ns.location IS NOT NULL; |
| 745 | | }}} |
| 746 | | |
| 747 | | === Find Nearest Network Sites === |
| | 310 | |
| | 311 | === SQL на поврзаната функција === |