Skip to content

Commit 23438de

Browse files
committed
refactor(visits): aggregate visit tables
Previously we had an analytics_visits table that held all the raw visit data. This mean our analytics UI was constantly executing queries across 18 mil records which was extremely slow. This change aggregates those analytics every month and then deletes all the raw data. We keep 2 months worth of raw data and then everything else gets pushed into the aggregate tables.
1 parent 4cd5fc0 commit 23438de

9 files changed

Lines changed: 1825 additions & 113 deletions

File tree

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ migrate:
150150
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251226_add_pipe_monitoring.sql
151151
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260116_add_analytics_filter_indexes.sql
152152
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260305_add_forms_table.sql
153+
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260503_add_analytics_summary_tables.sql
153154
.PHONY: migrate
154155

155156
latest:
156-
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260305_add_forms_table.sql
157+
$(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260503_add_analytics_summary_tables.sql
157158
.PHONY: latest
158159

159160
psql:
@@ -164,8 +165,9 @@ dump:
164165
$(DOCKER_CMD) exec $(DB_CONTAINER) pg_dump -U $(PGUSER) $(PGDATABASE) > ./backup.sql
165166
.PHONY: dump
166167

168+
BACKUP_FILE?=./backup.sql
167169
restore:
168-
$(DOCKER_CMD) cp ./backup.sql $(DB_CONTAINER):/backup.sql
170+
$(DOCKER_CMD) cp $(BACKUP_FILE) $(DB_CONTAINER):/backup.sql
169171
$(DOCKER_CMD) exec -it $(DB_CONTAINER) /bin/bash
170172
# psql postgres -U postgres -d pico < /backup.sql
171173
.PHONY: restore
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log/slog"
7+
"os"
8+
"time"
9+
10+
"github.com/picosh/pico/pkg/apps/auth"
11+
"github.com/picosh/pico/pkg/db/postgres"
12+
)
13+
14+
func main() {
15+
monthPtr := flag.String("month", "", "target month in YYYY-MM format (default: previous month)")
16+
backfill := flag.Bool("backfill", false, "aggregate all historical months up to the month before last")
17+
dryRun := flag.Bool("dry-run", false, "print months that would be processed without running aggregation")
18+
flag.Parse()
19+
20+
logger := slog.Default()
21+
dbURL := os.Getenv("DATABASE_URL")
22+
if dbURL == "" {
23+
fmt.Fprintln(os.Stderr, "DATABASE_URL must be set")
24+
os.Exit(1)
25+
}
26+
dbpool := postgres.NewDB(dbURL, logger)
27+
defer func() { _ = dbpool.Close() }()
28+
29+
if *backfill {
30+
runBackfill(dbpool, logger, *dryRun)
31+
return
32+
}
33+
34+
targetMonth := parseMonth(*monthPtr, logger)
35+
if err := auth.RunAnalyticsAggregation(dbpool, logger, targetMonth); err != nil {
36+
logger.Error("aggregation failed", "err", err)
37+
os.Exit(1)
38+
}
39+
}
40+
41+
func runBackfill(dbpool *postgres.PsqlDB, logger *slog.Logger, dryRun bool) {
42+
months, err := fetchHistoricalMonths(dbpool)
43+
if err != nil {
44+
logger.Error("failed to fetch historical months", "err", err)
45+
os.Exit(1)
46+
}
47+
if dryRun {
48+
fmt.Println("Months to backfill:")
49+
for _, m := range months {
50+
fmt.Println(" ", m.Format("2006-01"))
51+
}
52+
return
53+
}
54+
for _, m := range months {
55+
if err := auth.RunAnalyticsAggregation(dbpool, logger, m); err != nil {
56+
logger.Error("aggregation failed for month", "month", m.Format("2006-01"), "err", err)
57+
}
58+
}
59+
logger.Info("backfill complete", "months", len(months))
60+
}
61+
62+
// fetchHistoricalMonths returns all distinct months that have data in analytics_visits,
63+
// excluding the current month and the previous month (handled by the auth service cron).
64+
func fetchHistoricalMonths(dbpool *postgres.PsqlDB) ([]time.Time, error) {
65+
cutoff := time.Now().AddDate(0, -1, 0)
66+
cutoffMonth := time.Date(cutoff.Year(), cutoff.Month(), 1, 0, 0, 0, 0, time.UTC)
67+
68+
rows, err := dbpool.Db.Queryx(`
69+
SELECT DISTINCT date_trunc('month', created_at)::date AS month_start
70+
FROM analytics_visits
71+
WHERE created_at < $1
72+
ORDER BY month_start ASC
73+
`, cutoffMonth)
74+
if err != nil {
75+
return nil, err
76+
}
77+
defer func() { _ = rows.Close() }()
78+
79+
var months []time.Time
80+
for rows.Next() {
81+
var monthDate time.Time
82+
if err := rows.Scan(&monthDate); err != nil {
83+
return nil, err
84+
}
85+
months = append(months, monthDate)
86+
}
87+
return months, rows.Err()
88+
}
89+
90+
func parseMonth(arg string, logger *slog.Logger) time.Time {
91+
now := time.Now()
92+
if arg != "" {
93+
t, err := time.Parse("2006-01", arg)
94+
if err != nil {
95+
logger.Error("invalid month format, use YYYY-MM", "err", err, "input", arg)
96+
os.Exit(1)
97+
}
98+
return t
99+
}
100+
return now.AddDate(0, -1, 0)
101+
}

0 commit comments

Comments
 (0)