Changes between Version 3 and Version 4 of QueryOptimization


Ignore:
Timestamp:
09/21/26 16:54:34 (8 days ago)
Author:
231139
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • QueryOptimization

    v3 v4  
    4242
    4343Планот може да користи `Index Only Scan` со `Index Cond: account_id = ...`. Времето се намалило на **0.051 ms**, односно прашалникот е **6.80 пати побрз**.
     44
     45
    4446
    4547
    … …  
    7274
    7375Времето се намалило на **0.093 ms**, односно подобрување од **9.7%**. Разликата е мала поради малиот резултат, но индексот станува поважен со растењето на CDR табелата.
     76
     77
    7478
    7579
    … …  
    105109
    106110
     111
     112
    107113== 4. Дневна потрошувачка по претплата и датум ==
    108114
    … …  
    135141
    136142
     143
     144
    137145== 5. CRM тикети по корисник и датум ==
    138146
    … …  
    167175
    168176
     177
     178
    169179== 6. Мрежни прекини по локација и време ==
    170180
    … …  
    196206
    197207Времето се намалило на **0.059 ms**, односно подобрување од **16.9%**.
     208
     209
    198210
    199211
    … …  
    228240
    229241Времето се намалило на **0.627 ms**. Прашалникот е **79.81 пати побрз**, односно времето е намалено за **98.7%**. Ова е најголемото Telecom подобрување.
     242
     243
    230244
    231245
    … …  
    267281
    268282
     283
     284
    269285== 9. Најблиска активна мрежна локација ==
    270286
    … …  
    292308
    293309
    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 на поврзаната функција ===
    748312
    749313{{{
    … …  
    828392$$;
    829393}}}
     394
     395
     396== 10. Кориснички адреси во радиус од 2 km ==
     397
     398{{{
     399SELECT count(*), max(address.address_id)
     400FROM public.customer_addresses address
     401JOIN public.network_sites site ON site.site_id = :site_id
     402WHERE address.location IS NOT NULL
     403  AND address.location && ST_Envelope(
     404      ST_Buffer(site.location::geography, 2000)::geometry
     405  )
     406  AND ST_DWithin(
     407      address.location::geography,
     408      site.location::geography,
     409      2000
     410  );
     411}}}
     412
     413=== Пред оптимизацијата
     414
     415Времето било **39.591 ms**. Без индекс PostgreSQL ја чита customer_addresses и ја проверува оддалеченоста за голем број адреси.
     416
     417=== Избран индекс
     418
     419{{{
     420CREATE INDEX idx_customer_addresses_location_gist
     421ON public.customer_addresses
     422USING gist (location);
     423}}}
     424
     425GiST индексот го користи условот `location && ...` за да ги избере точките во приближната област. `ST_DWithin` потоа го проверува точното растојание само за тие кандидати. Во планот ова може да се појави како `Bitmap Index Scan`.
     426
     427=== По оптимизацијата
     428
     429Времето се намалило на **1.149 ms**. Прашалникот е **34.46 пати побрз**, односно времето е намалено за **97.1%**. Ова е најголемото GIS подобрување.
     430
     431
     432== Вкупен резултат ==
     433
     434За десетте прашалници, збирот на медијаните се намалил од **452.530 ms** на **204.042 ms**. Вкупно се заштедени **248.488 ms**, што е намалување од **54.9%** и забрзување од **2.22 пати**.
     435
     436Најголемо подобрување има кај сообраќајот по сектор и GIS пребарувањето на адреси во радиус од 2 km. Во двата случаи EXPLAIN ANALYZE покажува дека проблемот бил читање голем број непотребни редови, а индексот овозможува директен пристап до релевантното множество.
     437
     438Индексите се избрани според реалните услови во прашалниците. Колоните во `Index Cond` се клучеви на индексот, колоните потребни само за резултатот се ставаат во INCLUDE, а за просторни операции се користи GiST. Така секој задржан индекс има конкретна намена и измерено подобрување.
     439