loko/streetup/reporting/sql/mv_interventions.sql
2026-07-22 14:48:40 +02:00

232 lines
10 KiB
SQL

-- ============================================================
-- Materialized view : reporting.mv_interventions
-- Scope : interventions des 12 derniers mois
-- Refresh : via management command refresh_reporting_views (daily)
--
-- Pour recréer manuellement :
-- DROP MATERIALIZED VIEW IF EXISTS reporting.mv_interventions;
-- \i mv_interventions.sql
-- ============================================================
DROP MATERIALIZED VIEW IF EXISTS reporting.mv_interventions;
CREATE MATERIALIZED VIEW reporting.mv_interventions AS
WITH
-- ── Moment de passage à 'à traiter' ──────────────────────────────────────
tbe AS (
SELECT intervention_id, MIN(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status = 'to_be_processed'
GROUP BY intervention_id
),
-- ── Premier passage à 'pris en charge' ───────────────────────────────────
first_assigned AS (
SELECT intervention_id, MIN(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status = 'assigned'
GROUP BY intervention_id
),
-- ── Premier passage à 'démarré' ──────────────────────────────────────────
first_ip AS (
SELECT intervention_id, MIN(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status = 'in_progress'
GROUP BY intervention_id
),
-- ── Premier passage à 'terminé' ──────────────────────────────────────────
first_finished AS (
SELECT intervention_id, MIN(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status = 'finished'
GROUP BY intervention_id
),
-- ── Premier passage à 'en pause' ou 'terminé' ───────────────────────────
first_paused_or_finished AS (
SELECT intervention_id, MIN(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status IN ('on_pause', 'finished')
GROUP BY intervention_id
),
-- ── Dernier passage à 'terminé' ──────────────────────────────────────────
last_finished AS (
SELECT intervention_id, MAX(event_time) AS dt
FROM django.interventions_interventiontimeline
WHERE to_status = 'finished'
GROUP BY intervention_id
),
-- ── Temps d'exécution : somme des segments actifs en 'démarré' ───────────
-- La n-ième entrée en in_progress est appariée à la n-ième sortie de in_progress
-- grâce au ROW_NUMBER, ce qui gère correctement les pauses successives.
ip_starts AS (
SELECT
intervention_id,
event_time,
ROW_NUMBER() OVER (PARTITION BY intervention_id ORDER BY event_time) AS rn
FROM django.interventions_interventiontimeline
WHERE to_status = 'in_progress'
),
ip_ends AS (
SELECT
intervention_id,
event_time,
ROW_NUMBER() OVER (PARTITION BY intervention_id ORDER BY event_time) AS rn
FROM django.interventions_interventiontimeline
WHERE from_status = 'in_progress'
),
exec_time AS (
SELECT
s.intervention_id,
ROUND(
SUM(EXTRACT(EPOCH FROM (e.event_time - s.event_time)) / 60.0)
)::INTEGER AS total_minutes
FROM ip_starts s
JOIN ip_ends e
ON e.intervention_id = s.intervention_id
AND e.rn = s.rn
AND e.event_time >= s.event_time
GROUP BY s.intervention_id
),
-- ── Première localisation associée à l'intervention ──────────────────────
-- On prend la première par id (ordre d'insertion).
first_loc AS (
SELECT DISTINCT ON (il.intervention_id)
il.intervention_id,
il.content_type_id AS loc_content_type_id,
il.object_id AS loc_object_id,
ct.app_label AS loc_app_label,
ct.model AS loc_model
FROM django.interventions_interventionlocation il
JOIN django.django_content_type ct ON ct.id = il.content_type_id
ORDER BY il.intervention_id, il.id
)
SELECT
-- ── Identité ─────────────────────────────────────────────────────────────
i.id,
i.code,
i.title,
i.lon,
i.lat,
-- ── Thématique ───────────────────────────────────────────────────────────
i.thematic_id,
t.name_fr AS thematic_name_fr,
t.name_nl AS thematic_name_nl,
-- ── Catégorie d'asset ────────────────────────────────────────────────────
i.asset_category_id,
ac.name_fr AS asset_category_name_fr,
ac.name_nl AS asset_category_name_nl,
-- ── Statut & priorité ────────────────────────────────────────────────────
i.status,
i.priority,
-- ── Catégorie source ─────────────────────────────────────────────────────
i.source_category_id,
sc.name_fr AS source_category_name_fr,
-- ── Contrat & ordre ──────────────────────────────────────────────────────
i.contract_id,
c.contract_number,
i.order_id,
i.created_by_provider,
-- ── Dimensions maintenance ───────────────────────────────────────────────
i.maintain_type AS maintenance_type,
i.origin_type,
i.type AS intervention_type,
-- ── Date de création ─────────────────────────────────────────────────────
i.creation_time,
-- ── Horodatages bruts des jalons (utiles pour calculs complémentaires) ───
tbe.dt AS dt_to_be_processed,
fa.dt AS dt_first_assigned,
fip.dt AS dt_first_inprogress,
ffin.dt AS dt_first_finished,
lfin.dt AS dt_last_finished,
-- ── Métriques de temps en minutes entières (NULL si statut non atteint) ──
-- Temps de prise en charge : 'à traiter' → 'pris en charge'
CASE
WHEN fa.dt IS NOT NULL AND tbe.dt IS NOT NULL
THEN ROUND(EXTRACT(EPOCH FROM (fa.dt - tbe.dt)) / 60.0)::INTEGER
END AS response_time_min,
-- Temps d'intervention : 'à traiter' → premier 'démarré'
CASE
WHEN fip.dt IS NOT NULL AND tbe.dt IS NOT NULL
THEN ROUND(EXTRACT(EPOCH FROM (fip.dt - tbe.dt)) / 60.0)::INTEGER
END AS time_to_inprogress_min,
-- Premier temps de réalisation : 'à traiter' → premier 'en pause' ou 'terminé'
CASE
WHEN fpof.dt IS NOT NULL AND tbe.dt IS NOT NULL
THEN ROUND(EXTRACT(EPOCH FROM (fpof.dt - tbe.dt)) / 60.0)::INTEGER
END AS first_resolution_min,
-- Temps de réalisation final : 'à traiter' → dernier 'terminé'
CASE
WHEN lfin.dt IS NOT NULL AND tbe.dt IS NOT NULL
THEN ROUND(EXTRACT(EPOCH FROM (lfin.dt - tbe.dt)) / 60.0)::INTEGER
END AS final_resolution_min,
-- Temps d'exécution : somme des durées actives en 'démarré'
exec_t.total_minutes AS execution_time_min,
-- ── Localisation (GenericFK décodée) ─────────────────────────────────────
fl.loc_content_type_id,
fl.loc_object_id,
fl.loc_app_label,
fl.loc_model
FROM django.interventions_intervention i
LEFT JOIN django.common_thematic t ON t.id = i.thematic_id
LEFT JOIN django.assets_assetcategory ac ON ac.id = i.asset_category_id
LEFT JOIN django.interventions_sourcecategory sc ON sc.id = i.source_category_id
LEFT JOIN django.contracts_contract c ON c.id = i.contract_id
LEFT JOIN tbe ON tbe.intervention_id = i.id
LEFT JOIN first_assigned fa ON fa.intervention_id = i.id
LEFT JOIN first_ip fip ON fip.intervention_id = i.id
LEFT JOIN first_finished ffin ON ffin.intervention_id = i.id
LEFT JOIN first_paused_or_finished fpof ON fpof.intervention_id = i.id
LEFT JOIN last_finished lfin ON lfin.intervention_id = i.id
LEFT JOIN exec_time exec_t ON exec_t.intervention_id = i.id
LEFT JOIN first_loc fl ON fl.intervention_id = i.id
WHERE (
SELECT MIN(tl.event_time)
FROM django.interventions_interventiontimeline tl
WHERE tl.intervention_id = i.id
) > NOW() - INTERVAL '12 months'
WITH DATA;
-- ── Index unique requis pour REFRESH MATERIALIZED VIEW CONCURRENTLY ─────────
CREATE UNIQUE INDEX reporting_mv_interventions_pk
ON reporting.mv_interventions (id);
-- ── Index opérationnels ──────────────────────────────────────────────────────
CREATE INDEX reporting_mv_interventions_creation
ON reporting.mv_interventions (creation_time);
CREATE INDEX reporting_mv_interventions_thematic
ON reporting.mv_interventions (thematic_id);
CREATE INDEX reporting_mv_interventions_status
ON reporting.mv_interventions (status);
CREATE INDEX reporting_mv_interventions_contract
ON reporting.mv_interventions (contract_id);
CREATE INDEX reporting_mv_interventions_loc
ON reporting.mv_interventions (loc_content_type_id, loc_object_id);