Replies: 1 comment 2 replies
-
Analytics Schema RewriteGoalPostgreSQL-only, ClickHouse-level performance, small DB, fast dashboards. Time BucketshourlyBucket = unixSeconds / 3600
dailyBucket = unixSeconds / 86400Bun ModelsClient// Unique visitor, deduplicated by hash. Stores stable attributes.
type Client struct {
bun.BaseModel `bun:"table:clients,alias:c"`
ID int64 `bun:"id,pk,autoincrement"`
SiteID int64 `bun:"site_id,notnull"` // FK to Site
Hash string `bun:"hash,notnull,type:varchar(64)"` // SHA-256 = 64 hex chars
Country string `bun:"country,type:varchar(2)"` // ISO 3166-1 alpha-2: "US", "DE"
Device string `bun:"device,type:varchar(10)"` // "desktop", "mobile", "tablet"
Browser string `bun:"browser,type:varchar(32)"` // "Chrome", "Firefox", "Safari"
OS string `bun:"os,type:varchar(32)"` // "Windows", "macOS", "Android"
ScreenSize string `bun:"screen_size,type:varchar(16)"` // "1920x1080", "375x667"
Site *Site `bun:"rel:belongs-to,join:site_id=id"`
Sessions []*Session `bun:"rel:has-many,join:id=client_id"`
}Session// Single visit. Stores per-visit attributes. Timeout: 30min inactivity.
type Session struct {
bun.BaseModel `bun:"table:sessions,alias:s"`
ID int64 `bun:"id,pk,autoincrement"`
SiteID int64 `bun:"site_id,notnull"` // denormalized for query perf
ClientID int64 `bun:"client_id,notnull"` // FK to Client
// Entry (set once)
EnterTime int64 `bun:"enter_time,notnull"` // unix seconds
EnterHour int64 `bun:"enter_hour,notnull"` // unix / 3600 → hourly charts
EnterDay int64 `bun:"enter_day,notnull"` // unix / 86400 → daily charts
EnterPath string `bun:"enter_path,notnull,type:varchar(2048)"` // landing page URL
// Exit (updated each event)
ExitTime int64 `bun:"exit_time,notnull"` // unix seconds
ExitHour int64 `bun:"exit_hour,notnull"` // unix / 3600
ExitDay int64 `bun:"exit_day,notnull"` // unix / 86400
ExitPath string `bun:"exit_path,notnull,type:varchar(2048)"` // last page URL
// Attribution
Referrer string `bun:"referrer,type:varchar(2048)"` // external source URL
UTMSource string `bun:"utm_source,type:varchar(128)"` // utm_source param
UTMMedium string `bun:"utm_medium,type:varchar(128)"` // utm_medium param
UTMCampaign string `bun:"utm_campaign,type:varchar(256)"` // utm_campaign param
// Metrics
Duration int `bun:"duration,notnull,default:0"` // ExitTime - EnterTime (sec)
PageViewCount int `bun:"page_view_count,notnull,default:0"` // bounce = count == 1
Site *Site `bun:"rel:belongs-to,join:site_id=id"`
Client *Client `bun:"rel:belongs-to,join:client_id=id"`
Events []*Event `bun:"rel:has-many,join:id=session_id"`
}Event// EventType enum
type EventType int8
const (
EventTypePageview EventType = 0
EventTypeCustom EventType = 1
)
// Unified: pageviews + custom events.
// Pageview: Type=0, Name=title, DefinitionID=null
// Custom: Type=1, Name=event name, DefinitionID=FK
type Event struct {
bun.BaseModel `bun:"table:events,alias:e"`
ID int64 `bun:"id,pk,autoincrement"`
SessionID int64 `bun:"session_id,notnull"` // FK to Session
// Time buckets
Time int64 `bun:"time,notnull"` // unix seconds
Hour int64 `bun:"hour,notnull"` // unix / 3600
Day int64 `bun:"day,notnull"` // unix / 86400
// Data
Path string `bun:"path,notnull,type:varchar(2048)"` // page URL
Name string `bun:"name,notnull,type:varchar(256)"` // page title or event name
Type EventType `bun:"type,notnull"` // 0=pageview, 1=custom
DefinitionID *int64 `bun:"definition_id"` // FK to EventDefinition (null for pageviews)
Session *Session `bun:"rel:belongs-to,join:session_id=id"`
Definition *EventDefinition `bun:"rel:belongs-to,join:definition_id=id"`
Data []*EventData `bun:"rel:has-many,join:id=event_id"`
}EventDefinition// Schema for allowed custom events per site.
type EventDefinition struct {
bun.BaseModel `bun:"table:event_definitions,alias:ed"`
ID int64 `bun:"id,pk,autoincrement"`
SiteID int64 `bun:"site_id,notnull"` // FK to Site
Name string `bun:"name,notnull,type:varchar(128)"` // must match Event.Name
Site *Site `bun:"rel:belongs-to,join:site_id=id"`
Fields []*EventDefinitionField `bun:"rel:has-many,join:id=event_definition_id"`
}EventDefinitionField// FieldType enum
type FieldType int8
const (
FieldTypeString FieldType = 0
FieldTypeInt FieldType = 1
FieldTypeFloat FieldType = 2
FieldTypeBool FieldType = 3
)
// Property definition for custom events.
type EventDefinitionField struct {
bun.BaseModel `bun:"table:event_definition_fields,alias:edf"`
ID int64 `bun:"id,pk,autoincrement"`
EventDefinitionID int64 `bun:"event_definition_id,notnull"`
Key string `bun:"key,notnull,type:varchar(64)"` // property name
Type FieldType `bun:"type,notnull"` // 0=string, 1=int, 2=float, 3=bool
Required bool `bun:"required,notnull,default:false"`
MaxLength int `bun:"max_length,notnull,default:500"` // 0 = no limit
EventDefinition *EventDefinition `bun:"rel:belongs-to,join:event_definition_id=id"`
}EventData// Property values for custom events. One row per property.
type EventData struct {
bun.BaseModel `bun:"table:event_data,alias:evd"`
ID int64 `bun:"id,pk,autoincrement"`
EventID int64 `bun:"event_id,notnull"` // FK to Event
FieldID int64 `bun:"field_id,notnull"` // FK to EventDefinitionField
Value string `bun:"value,notnull,type:varchar(1024)"` // stored as string, typed by field
Event *Event `bun:"rel:belongs-to,join:event_id=id"`
Field *EventDefinitionField `bun:"rel:belongs-to,join:field_id=id"`
}RelationshipsIndexes-- Sessions
CREATE INDEX idx_sessions_site_day ON sessions (site_id, enter_day);
CREATE INDEX idx_sessions_site_hour ON sessions (site_id, enter_hour);
CREATE INDEX idx_sessions_client ON sessions (client_id);
CREATE INDEX idx_sessions_site_day_referrer ON sessions (site_id, enter_day, referrer);
-- Events
CREATE INDEX idx_events_session ON events (session_id);
CREATE INDEX idx_events_day ON events (day);
CREATE INDEX idx_events_hour ON events (hour);
CREATE INDEX idx_events_path ON events (path);
-- Clients
CREATE UNIQUE INDEX idx_clients_hash ON clients (site_id, hash);
CREATE INDEX idx_clients_site_country ON clients (site_id, country);
CREATE INDEX idx_clients_site_device ON clients (site_id, device);
CREATE INDEX idx_clients_site_browser ON clients (site_id, browser);
-- EventData
CREATE INDEX idx_event_data_event ON event_data (event_id);Query Examples
|
Beta Was this translation helpful? Give feedback.
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
After initial deployment on my personal website, I want to move forward and polish an app by rewriting the database schema to be performant on billions of rows to avoid big refactoring of the database in the future, for those who use this analytics.
AI helps me brainstorm the new schema design step by step.
Beta Was this translation helpful? Give feedback.
All reactions