Walle uses PostgreSQL table partitioning to efficiently manage large volumes of telemetry data. The points table is partitioned by date (using timestamp as the partition key), with each partition storing one day of data.Partitioning provides several benefits:
Query Performance: Faster queries by scanning only relevant partitions
Maintenance: Easier to archive or drop old data
Scalability: Better handling of large datasets
Index Size: Smaller, more efficient indexes per partition
The parent table is defined in src/app/database/constants/points.sql.ts:1 and created by PartitionManagerService on application startup (src/main.ts:13).
CREATE TABLE points ( id UUID NOT NULL DEFAULT gen_random_uuid(), timestamp BIGINT NOT NULL, -- Device and location fields tracker_device_imei BIGINT NOT NULL, location GEOMETRY(Point, 4326), tracker_device_latitude DOUBLE PRECISION NOT NULL, tracker_device_longitude DOUBLE PRECISION NOT NULL, -- Additional telemetry fields... PRIMARY KEY (id, timestamp)) PARTITION BY RANGE (timestamp);
The composite primary key (id, timestamp) is required because the partition key (timestamp) must be part of the primary key in PostgreSQL range partitioning.
You can also create partitions directly using SQL:
-- Calculate timestamps (example for 2026-04-01)SELECT EXTRACT(EPOCH FROM '2026-04-01 00:00:00'::timestamp) * 1000 AS start_ts, EXTRACT(EPOCH FROM '2026-04-02 00:00:00'::timestamp) * 1000 AS end_ts;-- Create partitionCREATE TABLE points_2026_04_01 PARTITION OF points FOR VALUES FROM (1743465600000) TO (1743552000000);-- Create indexesCREATE INDEX idx_points_2026_04_01_timestamp ON points_2026_04_01 (timestamp);CREATE INDEX idx_points_2026_04_01_imei_ts ON points_2026_04_01 (tracker_device_imei, timestamp);CREATE INDEX idx_points_2026_04_01_imei_ts_trip ON points_2026_04_01 (tracker_device_imei, timestamp, tracker_device_trip_id);CREATE INDEX idx_points_2026_04_01_location ON points_2026_04_01 USING GIST (location);
You can enable automated cleanup by uncommenting the cron job in src/app/points/partition-manager.service.ts:118-121:
@Cron('5 0 * * *') // Every day at 00:05async cleanYesterday(): Promise<void> { await this.dropPartitionDaysAgo(1);}
By default, this cleanup job is disabled to prevent accidental data loss. Only enable it if you have a data retention policy and backup strategy in place.
Before dropping old partitions, consider archiving the data:
# Export partition data to CSVpsql -U ngynx -d walledb -c "\COPY points_2026_01_01 TO '/backup/points_2026_01_01.csv' CSV HEADER"# Or use pg_dump for full backuppg_dump -U ngynx -d walledb -t points_2026_01_01 -f /backup/points_2026_01_01.sql# Then drop the partitionpsql -U ngynx -d walledb -c "DROP TABLE points_2026_01_01"
SELECT child.relname AS partition_name, pg_get_expr(child.relpartbound, child.oid) AS partition_bounds, pg_size_pretty(pg_total_relation_size(child.oid)) AS sizeFROM pg_inheritsJOIN pg_class parent ON pg_inherits.inhparent = parent.oidJOIN pg_class child ON pg_inherits.inhrelid = child.oidWHERE parent.relname = 'points'ORDER BY child.relname;
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size, pg_total_relation_size(schemaname||'.'||tablename) AS size_bytesFROM pg_tablesWHERE tablename LIKE 'points_%'ORDER BY size_bytes DESC;
SELECT child.relname AS partition_name, (SELECT count(*) FROM ONLY points_table) AS row_countFROM pg_inheritsJOIN pg_class parent ON pg_inherits.inhparent = parent.oidJOIN pg_class child ON pg_inherits.inhrelid = child.oidWHERE parent.relname = 'points'ORDER BY child.relname;
WITH date_series AS ( SELECT generate_series( CURRENT_DATE - INTERVAL '30 days', CURRENT_DATE + INTERVAL '7 days', INTERVAL '1 day' )::date AS date),expected_partitions AS ( SELECT 'points_' || to_char(date, 'YYYY_MM_DD') AS partition_name FROM date_series),existing_partitions AS ( SELECT child.relname AS partition_name FROM pg_inherits JOIN pg_class parent ON pg_inherits.inhparent = parent.oid JOIN pg_class child ON pg_inherits.inhrelid = child.oid WHERE parent.relname = 'points')SELECT ep.partition_nameFROM expected_partitions epLEFT JOIN existing_partitions ex ON ep.partition_name = ex.partition_nameWHERE ex.partition_name IS NULLORDER BY ep.partition_name;
Always maintain partitions for at least tomorrow to prevent insert failures at midnight.The automated cron job handles this, but you can manually create additional future partitions:
// Create partitions for next 7 daysfor (let i = 0; i < 7; i++) { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + i); await partitionManager.createPartitionForDate(futureDate);}
Monitor Partition Growth
Track partition sizes to identify unusual data growth:
-- Alert if any partition exceeds 10GBSELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass))FROM pg_tablesWHERE tablename LIKE 'points_%' AND pg_total_relation_size(tablename::regclass) > 10737418240ORDER BY pg_total_relation_size(tablename::regclass) DESC;
Regular Index Maintenance
Reindex partitions periodically to maintain performance:
-- Reindex a specific partitionREINDEX TABLE points_2026_03_03;-- Or reindex all partitionsREINDEX TABLE points;
Define and document a retention policy:Example Policy:
Hot data: Last 30 days (full partitions in database)
Warm data: 31-90 days (compressed, archived to S3)
Cold data: 90+ days (archived, partitions dropped)
Automate cleanup:
@Cron('0 2 * * 0') // Weekly at 2 AM on Sundaysasync archiveAndCleanup(): Promise<void> { // Archive partitions older than 30 days await this.archivePartitionDaysAgo(30); // Drop partitions older than 90 days await this.dropPartitionDaysAgo(90);}
-- Check PostGISSELECT PostGIS_version();-- Grant permissionsGRANT CREATE ON SCHEMA public TO ngynx;-- Check existing partitions for overlapsSELECT child.relname, pg_get_expr(child.relpartbound, child.oid)FROM pg_inheritsJOIN pg_class parent ON pg_inherits.inhparent = parent.oidJOIN pg_class child ON pg_inherits.inhrelid = child.oidWHERE parent.relname = 'points'ORDER BY child.relname;
Insert fails: no partition found
Problem: ERROR: no partition of relation "points" found for rowCause: Attempting to insert data for a date without a partitionSolution:
// Create partition for the data's date before insertingconst dataDate = new Date(point.timestamp);await partitionManager.createPartitionForDate(dataDate);
Slow queries across partitions
Problem: Queries scanning multiple partitions are slowSolutions:
Always include timestamp in WHERE clause to enable partition pruning:
-- Good: Only scans one partitionSELECT * FROM points WHERE timestamp >= 1709424000000 AND timestamp < 1709510400000 AND tracker_device_imei = 123456;-- Bad: Scans all partitionsSELECT * FROM points WHERE tracker_device_imei = 123456;
Use EXPLAIN to verify partition pruning:
EXPLAIN SELECT * FROM points WHERE timestamp >= 1709424000000;
Indexes not being used
Problem: Queries not using indexesCheck:
-- Verify indexes existSELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'points_2026_03_03';-- Check query planEXPLAIN ANALYZE SELECT * FROM points_2026_03_03 WHERE tracker_device_imei = 123456;