Enhance tracksolid_DB_manual.md with full analytics suite

- Add sections 16–21: Daily, Weekly, Monthly, Quarterly analytics,
  new table docs (device_events, fuel_readings, temperature_readings,
  lbs_readings, geofences), and updated Known Data Issues
- Fix all distance queries: remove erroneous /1000000.0 division
  (column is now distance_km in kilometres after migration 04)
- Update alarms section to reflect BUG-01 field mapping fix
- Update parking section to reflect POLL-02 acc_type/durSecond fix
- Rewrite "verify distance" section as accuracy cross-check query
- Expand row count query to include 5 new tables from migration 05

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
David Kiania 2026-04-10 22:33:06 +03:00
parent c05b47abe2
commit 05993100e9

View file

@ -30,7 +30,12 @@ docker exec timescale_db-bo3nov2ija7g8wn9b1g2paxs-210508774107 psql -U postgres
13. [dwh_gold.fact_daily_fleet_metrics](#13-dwh_goldfact_daily_fleet_metrics) 13. [dwh_gold.fact_daily_fleet_metrics](#13-dwh_goldfact_daily_fleet_metrics)
14. [Business Intelligence Queries](#14-business-intelligence-queries) 14. [Business Intelligence Queries](#14-business-intelligence-queries)
15. [Today's Metrics — From 00:00 Nairobi Time to Now](#15-todays-metrics--from-0000-nairobi-time-to-now) 15. [Today's Metrics — From 00:00 Nairobi Time to Now](#15-todays-metrics--from-0000-nairobi-time-to-now)
16. [Known Data Issues](#16-known-data-issues) 16. [Daily Analytics](#16-daily-analytics)
17. [Weekly Analytics](#17-weekly-analytics)
18. [Monthly Analytics](#18-monthly-analytics)
19. [Quarterly Analytics](#19-quarterly-analytics)
20. [New Tables from Migration 05](#20-new-tables-from-migration-05)
21. [Known Data Issues](#21-known-data-issues)
--- ---
@ -74,9 +79,14 @@ UNION ALL SELECT 'tracksolid.obd_readings', COUNT(*) FROM tracksolid.ob
UNION ALL SELECT 'tracksolid.parking_events', COUNT(*) FROM tracksolid.parking_events UNION ALL SELECT 'tracksolid.parking_events', COUNT(*) FROM tracksolid.parking_events
UNION ALL SELECT 'tracksolid.live_positions', COUNT(*) FROM tracksolid.live_positions UNION ALL SELECT 'tracksolid.live_positions', COUNT(*) FROM tracksolid.live_positions
UNION ALL SELECT 'tracksolid.fault_codes', COUNT(*) FROM tracksolid.fault_codes UNION ALL SELECT 'tracksolid.fault_codes', COUNT(*) FROM tracksolid.fault_codes
UNION ALL SELECT 'tracksolid.device_events', COUNT(*) FROM tracksolid.device_events
UNION ALL SELECT 'tracksolid.fuel_readings', COUNT(*) FROM tracksolid.fuel_readings
UNION ALL SELECT 'tracksolid.temperature_readings', COUNT(*) FROM tracksolid.temperature_readings
UNION ALL SELECT 'tracksolid.lbs_readings', COUNT(*) FROM tracksolid.lbs_readings
UNION ALL SELECT 'tracksolid.geofences', COUNT(*) FROM tracksolid.geofences
UNION ALL SELECT 'tracksolid.ingestion_log', COUNT(*) FROM tracksolid.ingestion_log UNION ALL SELECT 'tracksolid.ingestion_log', COUNT(*) FROM tracksolid.ingestion_log
UNION ALL SELECT 'dwh_gold.dim_vehicles', COUNT(*) FROM dwh_gold.dim_vehicles UNION ALL SELECT 'dwh_gold.dim_vehicles', COUNT(*) FROM dwh_gold.dim_vehicles
UNION ALL SELECT 'dwh_gold.fact_daily_fleet_metrics',COUNT(*) FROM dwh_gold.fact_daily_fleet_metrics; UNION ALL SELECT 'dwh_gold.fact_daily_fleet_metrics', COUNT(*) FROM dwh_gold.fact_daily_fleet_metrics;
``` ```
--- ---
@ -244,20 +254,23 @@ ORDER BY t.start_time DESC
LIMIT 20; LIMIT 20;
``` ```
### Verify distance units (cross-check via speed × time) ### Verify distance accuracy (cross-check via speed × time)
This diagnostic query confirms that `distance_km` is stored in millimetres by comparing the raw value (divided by 1,000,000 to get km) against the expected distance calculated from average speed and driving time. The two `_km` columns should match closely. Run this whenever the distance figures seem implausible. Sanity-check query confirming that `distance_km` values are reasonable by comparing them against the expected distance derived from `avg_speed_kmh × driving_time_s`. After migration 04 the two columns should match closely (within a few percent). Run this after deploying updated ingestion containers or after any data correction.
```sql ```sql
SELECT SELECT
t.imei, t.imei,
t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_nbi, t.start_time AT TIME ZONE 'Africa/Nairobi' AS start_nbi,
t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_nbi, t.end_time AT TIME ZONE 'Africa/Nairobi' AS end_nbi,
t.distance_km AS raw_distance_kmm, ROUND(t.distance_km::numeric, 3) AS distance_km,
ROUND(t.distance_km, 3) AS distance_km,
t.avg_speed_kmh, t.avg_speed_kmh,
t.driving_time_s, t.driving_time_s,
ROUND((t.avg_speed_kmh * t.driving_time_s / 3600.0), 3) AS expected_km_from_speed ROUND((t.avg_speed_kmh * t.driving_time_s / 3600.0), 3) AS expected_km_from_speed,
ROUND(
ABS(t.distance_km - (t.avg_speed_kmh * t.driving_time_s / 3600.0))
/ NULLIF(t.avg_speed_kmh * t.driving_time_s / 3600.0, 0) * 100
, 1) AS variance_pct
FROM tracksolid.trips t FROM tracksolid.trips t
WHERE t.avg_speed_kmh IS NOT NULL WHERE t.avg_speed_kmh IS NOT NULL
AND t.driving_time_s IS NOT NULL AND t.driving_time_s IS NOT NULL
@ -332,7 +345,7 @@ ORDER BY lp.updated_at DESC;
The alarms table records every alarm event reported by the Tracksolid API for any device in the fleet. Alarm types can include overspeed, harsh braking, geofence violations, power disconnection, low battery, tampering, and more — the exact set depends on the tracker model and account configuration. Each alarm record captures the device IMEI, alarm type and name, the timestamp and GPS coordinates at which the alarm fired, the vehicle's speed at that moment, and ignition status. The alarms table records every alarm event reported by the Tracksolid API for any device in the fleet. Alarm types can include overspeed, harsh braking, geofence violations, power disconnection, low battery, tampering, and more — the exact set depends on the tracker model and account configuration. Each alarm record captures the device IMEI, alarm type and name, the timestamp and GPS coordinates at which the alarm fired, the vehicle's speed at that moment, and ignition status.
At the time of audit, all 1,054 records had `alarm_type` and `alarm_name` set to null, meaning the alarm classification is not yet being parsed from the API response. This is a data pipeline gap that needs to be fixed — the raw alarm events are being stored but are not yet actionable without the type label. **Fixed [FIX-E06]:** At the time of the initial audit all 1,054 records had `alarm_type` and `alarm_name` null because the polling code was reading webhook field names (`alarmType`, `alarmName`, `alarmTime`) instead of the poll API field names (`alertTypeId`, `alarmTypeName`, `alertTime`). This has been corrected in `ingest_events_rev.py`. New alarm records ingested after the fix will contain classified alarm types. Migration 05 also added `severity`, `geofence_id`, `geofence_name`, `acknowledged_at`, and `acknowledged_by` columns for richer alarm management.
### Describe table structure ### Describe table structure
@ -468,7 +481,7 @@ At the time of audit this table contained **0 rows**. The JC400P and X3 tracker
This table is designed to store discrete parking events — records of when a vehicle stopped and for how long. Rather than deriving parking from position history (which requires scanning many rows), the Tracksolid API's `jimi.open.platform.report.parking` endpoint provides pre-computed parking events. These are useful for calculating driver wait times, identifying vehicles left idle in locations for long periods, and auditing whether vehicles are parked at authorised locations overnight. This table is designed to store discrete parking events — records of when a vehicle stopped and for how long. Rather than deriving parking from position history (which requires scanning many rows), the Tracksolid API's `jimi.open.platform.report.parking` endpoint provides pre-computed parking events. These are useful for calculating driver wait times, identifying vehicles left idle in locations for long periods, and auditing whether vehicles are parked at authorised locations overnight.
At the time of audit this table contained **0 rows**, despite the ingestion service successfully calling the parking endpoint 358 times. The API is returning empty results, suggesting parking detection thresholds may need to be adjusted in the Tracksolid account settings, or the vehicles have not yet triggered the platform's parking detection criteria. **Fixed [FIX-M13]:** At the time of the initial audit this table contained 0 rows despite 358 successful API calls. The root cause was two missing parameters in the request — `account` (required to scope results to the fleet) and `acc_type=0` (required to include all stop types). Additionally, the response field `durSecond` was being read as `seconds`, causing duration to always be null. All three issues are corrected in `ingest_movement_rev.py`. Parking events should now populate on the next poll cycle.
### Describe table structure ### Describe table structure
@ -538,7 +551,7 @@ WITH daily AS (
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date, DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi') AS first_trip_start, MIN(t.start_time AT TIME ZONE 'Africa/Nairobi') AS first_trip_start,
MAX(t.end_time AT TIME ZONE 'Africa/Nairobi') AS last_trip_end, MAX(t.end_time AT TIME ZONE 'Africa/Nairobi') AS last_trip_end,
ROUND(SUM(t.distance_km) / 1000000.0, 2) AS total_km, ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
COUNT(*) AS trip_count, COUNT(*) AS trip_count,
ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min, ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min,
ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min, ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min,
@ -579,7 +592,7 @@ SELECT
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date, DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
COUNT(DISTINCT t.imei) AS active_vehicles, COUNT(DISTINCT t.imei) AS active_vehicles,
COUNT(*) AS total_trips, COUNT(*) AS total_trips,
ROUND(SUM(t.distance_km) / 1000000.0, 2) AS total_fleet_km, ROUND(SUM(t.distance_km)::numeric, 2) AS total_fleet_km,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh, ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours, ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours
@ -715,7 +728,7 @@ SELECT
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_start, TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_start,
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS last_activity, TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS last_activity,
COUNT(*) AS trips_so_far, COUNT(*) AS trips_so_far,
ROUND(SUM(t.distance_km) / 1000000.0, 2) AS total_km, ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min, ROUND(SUM(t.driving_time_s) / 60.0, 1) AS total_drive_min,
ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min, ROUND(SUM(t.idle_time_s) / 60.0, 1) AS total_idle_min,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh
@ -763,7 +776,7 @@ WITH today_start AS (
SELECT SELECT
COUNT(DISTINCT t.imei) AS active_vehicles_today, COUNT(DISTINCT t.imei) AS active_vehicles_today,
COUNT(*) AS total_trips_today, COUNT(*) AS total_trips_today,
ROUND(SUM(t.distance_km) / 1000000.0, 2) AS total_fleet_km, ROUND(SUM(t.distance_km)::numeric, 2) AS total_fleet_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours, ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS total_drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours, ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS total_idle_hours,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh, ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS fleet_avg_speed_kmh,
@ -776,7 +789,655 @@ WHERE t.start_time >= ts.ts
--- ---
## 16. Known Data Issues ## 20. New Tables from Migration 05
Migration 05 adds the following tables to support expanded webhook ingestion and enriched reporting. All tables have corresponding handlers in `webhook_receiver_rev.py` or are populated via ETL.
### tracksolid.device_events
Records device network login and logout events received via the `/pushevent` webhook. Distinguishes "vehicle parked, tracker alive" from "tracker lost power or was disconnected". Each row captures the device IMEI, whether it connected (LOGIN) or disconnected (LOGOUT), the event timestamp, and the device's reported timezone. Use this table to calculate true uptime per device and to detect potential tampering (unexpected LOGOUT during operational hours).
```sql
SELECT
de.imei,
d.vehicle_name,
de.event_type,
de.event_time AT TIME ZONE 'Africa/Nairobi' AS event_nairobi,
de.timezone
FROM tracksolid.device_events de
LEFT JOIN tracksolid.devices d ON d.imei = de.imei
ORDER BY de.event_time DESC
LIMIT 50;
```
### tracksolid.fuel_readings
Stores fuel and oil sensor readings from the `/pushoil` webhook. Relevant for vehicles equipped with a direct fuel tank sensor — these report tank level (in cm depth, percentage, voltage, or litres depending on sensor type). Sudden unexplained drops in fuel level between readings indicate possible fuel theft. Each reading includes the sensor channel ID (`sensor_path`), the measured value, the unit, and the GPS position at time of reading.
```sql
SELECT
fr.imei,
d.vehicle_name,
fr.reading_time AT TIME ZONE 'Africa/Nairobi' AS reading_nairobi,
fr.sensor_path,
fr.value,
fr.unit,
fr.lat,
fr.lng
FROM tracksolid.fuel_readings fr
LEFT JOIN tracksolid.devices d ON d.imei = fr.imei
ORDER BY fr.reading_time DESC
LIMIT 50;
```
### tracksolid.temperature_readings
Stores cabin or cargo temperature and humidity readings from the `/pushtem` webhook. Critical for cold-chain logistics where temperature excursions during transit can compromise cargo. Each row holds the sensor reading at a point in time per device. Pair with `position_history` to map temperature along a route.
```sql
SELECT
tr.imei,
d.vehicle_name,
tr.reading_time AT TIME ZONE 'Africa/Nairobi' AS reading_nairobi,
tr.temperature,
tr.humidity_pct
FROM tracksolid.temperature_readings tr
LEFT JOIN tracksolid.devices d ON d.imei = tr.imei
ORDER BY tr.reading_time DESC
LIMIT 50;
```
### tracksolid.lbs_readings
Stores cell tower and WiFi-based positioning data from the `/pushlbs` webhook. Used as a fallback when GPS signal is unavailable (tunnels, underground parking, dense urban areas). The raw cell data is stored as JSONB in `lbs_data` containing MCC, MNC, and cell tower list which can be forwarded to a geocell API (e.g. Google Geolocation, HERE) for approximate coordinate resolution.
```sql
SELECT
lr.imei,
d.vehicle_name,
lr.gate_time AT TIME ZONE 'Africa/Nairobi' AS gate_nairobi,
lr.post_type,
lr.lbs_data
FROM tracksolid.lbs_readings lr
LEFT JOIN tracksolid.devices d ON d.imei = lr.imei
ORDER BY lr.gate_time DESC
LIMIT 50;
```
### tracksolid.geofences
Stores geofence boundary definitions synced from the Tracksolid platform. Each row represents a named geographic boundary (circle or polygon) used for arrival/departure alerting. Once populated, this table enables joining alarm events to geofence names so that `tracksolid.alarms.geofence_name` is human-readable rather than just an ID.
```sql
SELECT
fence_id,
fence_name,
fence_type,
radius_m,
description,
created_at AT TIME ZONE 'Africa/Nairobi' AS created_nairobi
FROM tracksolid.geofences
ORDER BY created_at DESC;
```
---
## 16. Daily Analytics
Daily analytics answer the question: **"What happened across the fleet today?"** All queries use `Africa/Nairobi` timezone so that a working day (typically 08:0020:00 EAT) maps cleanly to a single calendar date. The `dwh_gold.refresh_daily_metrics()` function pre-aggregates these into `fact_daily_fleet_metrics` for fast dashboard queries — run it nightly for the previous day.
### Day summary — all vehicles, single date
Replace the date literal with any past date or use `CURRENT_DATE - 1` for yesterday. This is the primary end-of-day report: who drove, how far, when they started and finished, and how much time was wasted idling.
```sql
SELECT
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
d.vehicle_name,
d.vehicle_number,
d.driver_name,
t.imei,
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_start,
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS day_end,
ROUND(
EXTRACT(EPOCH FROM (MAX(t.end_time) - MIN(t.start_time))) / 3600.0
, 2) AS operational_hours,
COUNT(*) AS trips,
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
SUM(t.fuel_consumed_l) AS fuel_l
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
AND t.end_time IS NOT NULL
GROUP BY 1, d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
ORDER BY total_km DESC;
```
### Daily alarm summary
Counts alarm events per vehicle per type for a single day. Used in end-of-day driver behaviour reporting. Sort by total alarms to surface the most at-risk drivers.
```sql
SELECT
DATE(a.alarm_time AT TIME ZONE 'Africa/Nairobi') AS alarm_date,
d.vehicle_name,
d.driver_name,
a.imei,
a.alarm_type,
a.alarm_name,
COUNT(*) AS alarm_count,
MAX(a.speed) AS max_speed_at_alarm
FROM tracksolid.alarms a
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
WHERE DATE(a.alarm_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
GROUP BY 1, d.vehicle_name, d.driver_name, a.imei, a.alarm_type, a.alarm_name
ORDER BY alarm_count DESC;
```
### Vehicles not used today
Flags registered vehicles that had zero trips on a given day. Cross-reference with `heartbeats` and `live_positions` to determine if the device was online (vehicle was parked) or offline (device problem).
```sql
SELECT
d.imei,
d.vehicle_name,
d.vehicle_number,
d.driver_name,
d.current_mileage_km,
lp.device_status,
lp.updated_at AT TIME ZONE 'Africa/Nairobi' AS last_seen_nairobi
FROM tracksolid.devices d
LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei
WHERE d.enabled_flag = 1
AND d.imei NOT IN (
SELECT DISTINCT imei FROM tracksolid.trips
WHERE DATE(start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
)
ORDER BY d.vehicle_name NULLS LAST;
```
### Daily parking events summary
Lists all parking stops recorded in a day with duration and location. Highlights vehicles parked for extended periods at unplanned locations. A stop longer than 2 hours away from the depot during working hours warrants investigation.
```sql
SELECT
d.vehicle_name,
d.driver_name,
p.imei,
p.start_time AT TIME ZONE 'Africa/Nairobi' AS parked_at,
p.end_time AT TIME ZONE 'Africa/Nairobi' AS departed_at,
ROUND(p.duration_seconds / 60.0, 1) AS duration_min,
p.address
FROM tracksolid.parking_events p
LEFT JOIN tracksolid.devices d ON d.imei = p.imei
WHERE DATE(p.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
ORDER BY p.duration_seconds DESC;
```
### First departure and last return — all vehicles
Gives the operational window for each vehicle: when the first trip began (proxy for driver start time) and when the last trip ended (proxy for sign-off time). Used to enforce working-hours policy and detect unauthorised after-hours use.
```sql
SELECT
d.vehicle_name,
d.vehicle_number,
d.driver_name,
t.imei,
TO_CHAR(MIN(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS first_departure,
TO_CHAR(MAX(t.end_time AT TIME ZONE 'Africa/Nairobi'), 'HH24:MI') AS last_return,
ROUND(
EXTRACT(EPOCH FROM (MAX(t.end_time) - MIN(t.start_time))) / 3600.0
, 2) AS operational_hours
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') = '2026-04-10'
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
ORDER BY first_departure;
```
---
## 17. Weekly Analytics
Weekly analytics answer: **"How did the fleet perform over the past 7 days?"** Replace the date range with the desired ISO week or rolling window. For MondaySunday weeks use `DATE_TRUNC('week', ...)`.
### Weekly driving summary per vehicle
One row per vehicle per week. Shows total km, trips, drive hours, idle ratio, and average speed. Useful for identifying vehicles consistently underperforming or overloading drivers.
```sql
WITH week_range AS (
SELECT
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' AS week_start,
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
)
SELECT
d.vehicle_name,
d.vehicle_number,
d.driver_name,
t.imei,
COUNT(*) AS total_trips,
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
CROSS JOIN week_range wr
WHERE t.start_time >= wr.week_start
AND t.start_time < wr.week_end
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
ORDER BY total_km DESC;
```
### Weekly alarm league table — by driver
Ranks drivers by total alarm count for the week. Helps safety managers identify drivers needing coaching. Break down by `alarm_type` to differentiate overspeed from geofence violations vs. device health alerts.
```sql
WITH week_range AS (
SELECT
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' AS week_start,
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
)
SELECT
d.driver_name,
d.vehicle_name,
a.imei,
COUNT(*) AS total_alarms,
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%speed%') AS overspeed,
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%geofence%'
OR a.alarm_type ILIKE '%fence%') AS geofence,
COUNT(*) FILTER (WHERE a.alarm_type ILIKE '%power%'
OR a.alarm_type ILIKE '%battery%') AS device_health
FROM tracksolid.alarms a
LEFT JOIN tracksolid.devices d ON d.imei = a.imei
CROSS JOIN week_range wr
WHERE a.alarm_time >= wr.week_start
AND a.alarm_time < wr.week_end
GROUP BY d.driver_name, d.vehicle_name, a.imei
ORDER BY total_alarms DESC;
```
### Weekly idle time analysis — worst offenders
Surfaces vehicles where idle time exceeded 25% of total engine-on time across the week. Sustained high idle ratios point to route inefficiencies, long customer wait times, or drivers keeping engines running while stationary.
```sql
WITH week_range AS (
SELECT
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' AS week_start,
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
)
SELECT
d.vehicle_name,
d.driver_name,
t.imei,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
CROSS JOIN week_range wr
WHERE t.start_time >= wr.week_start
AND t.start_time < wr.week_end
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.driver_name, t.imei
HAVING
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0) > 25
ORDER BY idle_pct DESC;
```
### Weekly day-by-day fleet activity
Shows fleet-wide totals for each day of the current week. Good for identifying quiet days (public holidays, weather events) or usage patterns (e.g. Saturdays consistently low).
```sql
WITH week_range AS (
SELECT
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' AS week_start,
DATE_TRUNC('week', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '7 days' AS week_end
)
SELECT
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS work_date,
TO_CHAR(DATE(t.start_time AT TIME ZONE 'Africa/Nairobi'), 'Day') AS day_name,
COUNT(DISTINCT t.imei) AS active_vehicles,
COUNT(*) AS trips,
ROUND(SUM(t.distance_km)::numeric, 2) AS fleet_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours
FROM tracksolid.trips t
CROSS JOIN week_range wr
WHERE t.start_time >= wr.week_start
AND t.start_time < wr.week_end
AND t.end_time IS NOT NULL
GROUP BY DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
ORDER BY work_date;
```
---
## 18. Monthly Analytics
Monthly analytics answer: **"How did the fleet perform this month?"** Queries use `DATE_TRUNC('month', ...)` to anchor to the first of the current month. For historical months replace with a specific date literal. When `dwh_gold.fact_daily_fleet_metrics` is fully populated these queries can be rewritten against that table for much faster execution.
### Monthly summary per vehicle
The primary monthly management report. One row per vehicle for the selected month, showing utilisation, distance, fuel, and alarm counts side by side. Replace the date with the first day of the target month.
```sql
SELECT
d.vehicle_name,
d.vehicle_number,
d.driver_name,
t.imei,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
COUNT(*) AS total_trips,
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(AVG(t.avg_speed_kmh)::numeric, 2) AS avg_speed_kmh,
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l,
ROUND(
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
, 2) AS fuel_per_100km
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND t.start_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
ORDER BY total_km DESC;
```
### Monthly odometer progression
Shows how much each vehicle's cumulative mileage grew during the month, derived from the difference between the earliest and latest `current_mileage` in `position_history`. Cross-check against `trips.distance_km` totals to validate data consistency.
```sql
SELECT
ph.imei,
d.vehicle_name,
MIN(ph.current_mileage) AS mileage_start,
MAX(ph.current_mileage) AS mileage_end,
ROUND((MAX(ph.current_mileage) - MIN(ph.current_mileage))::numeric, 2) AS km_added
FROM tracksolid.position_history ph
JOIN tracksolid.devices d ON d.imei = ph.imei
WHERE ph.gps_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND ph.gps_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
GROUP BY ph.imei, d.vehicle_name
ORDER BY km_added DESC;
```
### Monthly alarm trends — week by week
Breaks the month into ISO weeks to show whether alarm frequency is increasing or decreasing. A rising trend requires management intervention; a falling trend after coaching sessions confirms improvement.
```sql
SELECT
DATE_TRUNC('week', a.alarm_time AT TIME ZONE 'Africa/Nairobi') AS week_start,
a.alarm_type,
COUNT(*) AS alarm_count,
COUNT(DISTINCT a.imei) AS vehicles_affected
FROM tracksolid.alarms a
WHERE a.alarm_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND a.alarm_time < DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month'
AND a.alarm_type IS NOT NULL
GROUP BY 1, a.alarm_type
ORDER BY week_start, alarm_count DESC;
```
### Monthly utilisation rate — fleet availability vs. usage
Compares the number of days each vehicle was used against the number of working days in the month. A utilisation rate below 60% may indicate excess fleet capacity; above 95% may indicate a vehicle at risk of missing maintenance.
```sql
WITH month_days AS (
SELECT
DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' AS month_start,
DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi' + INTERVAL '1 month' AS month_end,
-- Approximate working days: calendar days minus weekends
(EXTRACT(DAY FROM
(DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
+ INTERVAL '1 month'
- DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi'))
) * 5 / 7)::int AS working_days
)
SELECT
d.vehicle_name,
d.vehicle_number,
t.imei,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_used,
md.working_days,
ROUND(
100.0 * COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi'))
/ NULLIF(md.working_days, 0)
, 1) AS utilisation_pct
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
CROSS JOIN month_days md
WHERE t.start_time >= md.month_start
AND t.start_time < md.month_end
GROUP BY d.vehicle_name, d.vehicle_number, t.imei, md.working_days
ORDER BY utilisation_pct DESC;
```
### Monthly speed compliance summary
Calculates the share of trips within each speed band for the month. Feeds directly into a safety compliance report. A healthy fleet should have the majority of trips below 60 km/h average for urban operations.
```sql
SELECT
CASE
WHEN avg_speed_kmh < 20 THEN '020 km/h (slow / congested)'
WHEN avg_speed_kmh < 40 THEN '2040 km/h (normal urban)'
WHEN avg_speed_kmh < 60 THEN '4060 km/h (arterial / highway)'
WHEN avg_speed_kmh < 80 THEN '6080 km/h (fast highway)'
ELSE '80+ km/h (excessive)'
END AS speed_band,
COUNT(*) AS trips,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct_of_total,
ROUND(SUM(t.distance_km)::numeric, 2) AS km_in_band
FROM tracksolid.trips t
WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
AND t.avg_speed_kmh IS NOT NULL
GROUP BY 1
ORDER BY MIN(avg_speed_kmh);
```
---
## 19. Quarterly Analytics
Quarterly analytics answer: **"What are the trends over the past three months?"** These queries are designed to surface patterns that are invisible at the daily or weekly level — vehicle degradation, seasonal demand shifts, cumulative fuel costs, and driver behaviour trajectories. Use `DATE_TRUNC('quarter', ...)` for clean calendar quarters or specify explicit date ranges for financial quarters.
### Quarterly fleet KPI dashboard row
A single aggregated row per quarter giving headline KPIs: total km, fuel, drive hours, idle ratio, and alarm rate per 1,000 km. Feed this into a year-over-year comparison table.
```sql
SELECT
DATE_TRUNC('quarter', t.start_time AT TIME ZONE 'Africa/Nairobi') AS quarter,
COUNT(DISTINCT t.imei) AS avg_active_vehicles,
COUNT(*) AS total_trips,
ROUND(SUM(t.distance_km)::numeric, 0) AS total_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 0) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 0) AS idle_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(SUM(t.fuel_consumed_l)::numeric, 0) AS fuel_l,
ROUND(
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
, 2) AS fuel_per_100km
FROM tracksolid.trips t
WHERE t.end_time IS NOT NULL
GROUP BY DATE_TRUNC('quarter', t.start_time AT TIME ZONE 'Africa/Nairobi')
ORDER BY quarter;
```
### Quarterly vehicle performance ranking
Ranks each vehicle across the quarter by distance, drive hours, and idle ratio. Highlights top performers and outliers. Vehicles consistently ranking at the bottom may need reassignment, maintenance, or driver change.
```sql
SELECT
d.vehicle_name,
d.vehicle_number,
d.driver_name,
t.imei,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS active_days,
ROUND(SUM(t.distance_km)::numeric, 2) AS total_km,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(
100.0 * SUM(t.idle_time_s) /
NULLIF(SUM(t.driving_time_s) + SUM(t.idle_time_s), 0)
, 1) AS idle_pct,
ROUND(SUM(t.fuel_consumed_l)::numeric, 2) AS fuel_l,
ROUND(
SUM(t.fuel_consumed_l) / NULLIF(SUM(t.distance_km), 0) * 100
, 2) AS fuel_per_100km,
RANK() OVER (ORDER BY SUM(t.distance_km) DESC) AS km_rank
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.vehicle_number, d.driver_name, t.imei
ORDER BY total_km DESC;
```
### Quarterly alarm frequency trend — month over month
Shows whether safety events are improving or worsening over the quarter. Break down by `alarm_type` to distinguish overspeed trends from geofence or mechanical alarms.
```sql
SELECT
TO_CHAR(DATE_TRUNC('month', a.alarm_time AT TIME ZONE 'Africa/Nairobi'), 'YYYY-MM') AS month,
a.alarm_type,
COUNT(*) AS alarm_count,
COUNT(DISTINCT a.imei) AS vehicles_affected,
ROUND(
COUNT(*) * 1000.0 /
NULLIF((
SELECT SUM(t2.distance_km) FROM tracksolid.trips t2
WHERE DATE_TRUNC('month', t2.start_time AT TIME ZONE 'Africa/Nairobi')
= DATE_TRUNC('month', a.alarm_time AT TIME ZONE 'Africa/Nairobi')
), 0)
, 2) AS alarms_per_1000km
FROM tracksolid.alarms a
WHERE a.alarm_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND a.alarm_type IS NOT NULL
GROUP BY 1, a.alarm_type
ORDER BY month, alarm_count DESC;
```
### Quarterly high-mileage vehicles — service interval flag
Flags vehicles that have accumulated more than a configurable kilometre threshold in the quarter. At 5,000 km/quarter a vehicle has driven ~20,000 km/year. Adjust the `HAVING` threshold to match your service interval policy (e.g. every 5,000 km or every 10,000 km).
```sql
SELECT
d.vehicle_name,
d.vehicle_number,
d.imei,
d.current_mileage_km AS current_odometer,
ROUND(SUM(t.distance_km)::numeric, 2) AS km_this_quarter,
CASE
WHEN SUM(t.distance_km) > 10000 THEN 'URGENT SERVICE'
WHEN SUM(t.distance_km) > 5000 THEN 'SERVICE DUE'
ELSE 'OK'
END AS service_flag
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.vehicle_number, d.imei, d.current_mileage_km
HAVING SUM(t.distance_km) > 5000
ORDER BY km_this_quarter DESC;
```
### Quarterly work-hours pattern — average start and finish times
Calculates the average first-departure and last-return time per vehicle across the quarter. Surfaces systematic early-starters, late-finishers, and vehicles being used outside contracted hours. Shows the standard deviation to identify inconsistent schedules.
```sql
SELECT
d.vehicle_name,
d.driver_name,
t.imei,
TO_CHAR(
TO_TIMESTAMP(AVG(EXTRACT(EPOCH FROM (MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time))))
, 'HH24:MI') AS avg_day_start,
TO_CHAR(
TO_TIMESTAMP(AVG(EXTRACT(EPOCH FROM (MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::time))))
, 'HH24:MI') AS avg_day_end,
ROUND(STDDEV(EXTRACT(EPOCH FROM (MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time))) / 60, 0)
AS start_time_std_min,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_worked
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= DATE_TRUNC('quarter', CURRENT_DATE::timestamptz AT TIME ZONE 'Africa/Nairobi')
AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
GROUP BY d.vehicle_name, d.driver_name, t.imei,
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')
-- Outer aggregation over the per-day rows
-- Wrap in a CTE if additional aggregation by vehicle is needed
ORDER BY avg_day_start;
```
---
## 21. Known Data Issues
The following issues were identified during the April 2026 audit. Each represents either a data pipeline gap or a missing enrichment step. The following issues were identified during the April 2026 audit. Each represents either a data pipeline gap or a missing enrichment step.