Skip to content

Commit b407d87

Browse files
committed
feat(models): optimize user loading from 3 queries to 1 using JSON aggregation
Reduces FindUserWithRefreshToken from 3 separate queries (user + identities + factors) to a single query using json_agg subqueries. This optimization impacts the /token endpoint (~45% of total traffic) and /user endpoint by eliminating 2 database round-trips per call. Since both of those endpoints call FindUserWithRefreshToken twice it removes 4 database roundtrips per request. Performance impact: - Query execution: 399µs → 209µs (47.6% faster) - Memory allocations: 18.4KB → 7.1KB (61% reduction) - Allocation count: 299 → 117 allocs (61% reduction) - /token throughput: +20.3% (55.18 vs 45.87 req/s) in local testing - /token latency: -16.8% (181ms vs 218ms) Replaces Pop ORM .Eager() pattern with explicit SQL column enumeration and coalesce(json_agg()) for related entities. No changes to User struct or API.
1 parent effd662 commit b407d87

1 file changed

Lines changed: 63 additions & 3 deletions

File tree

internal/models/user.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/sha256"
66
"database/sql"
77
"encoding/base64"
8+
"encoding/json"
89
"fmt"
910
"strings"
1011
"time"
@@ -604,15 +605,74 @@ func CountOtherUsers(tx *storage.Connection, id uuid.UUID) (int, error) {
604605
}
605606

606607
func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, error) {
607-
obj := &User{}
608-
if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil {
608+
// Single query with JSON aggregation - embed by VALUE not pointer
609+
type userWithJSON struct {
610+
User
611+
IdentitiesJSON []byte `db:"identities_json"`
612+
FactorsJSON []byte `db:"factors_json"`
613+
}
614+
615+
var result userWithJSON
616+
617+
sqlQuery := `
618+
select
619+
u.id, u.aud, u.role, u.email, u.is_sso_user,
620+
u.encrypted_password, u.email_confirmed_at, u.invited_at,
621+
u.phone, u.phone_confirmed_at,
622+
u.confirmation_token, u.confirmation_sent_at, u.confirmed_at,
623+
u.recovery_token, u.recovery_sent_at,
624+
u.email_change_token_current, u.email_change_token_new, u.email_change,
625+
u.email_change_sent_at, u.email_change_confirm_status,
626+
u.phone_change_token, u.phone_change, u.phone_change_sent_at,
627+
u.reauthentication_token, u.reauthentication_sent_at,
628+
u.last_sign_in_at,
629+
u.raw_app_meta_data, u.raw_user_meta_data,
630+
u.created_at, u.updated_at, u.banned_until, u.deleted_at, u.is_anonymous,
631+
u.instance_id,
632+
coalesce((select json_agg(json_build_object(
633+
'identity_id', i.id,
634+
'id', i.provider_id,
635+
'user_id', i.user_id,
636+
'identity_data', i.identity_data,
637+
'provider', i.provider,
638+
'last_sign_in_at', i.last_sign_in_at,
639+
'created_at', i.created_at,
640+
'updated_at', i.updated_at,
641+
'email', i.email
642+
)) from ` + Identity{}.TableName() + ` i where i.user_id = u.id), '[]') as identities_json,
643+
coalesce((select json_agg(json_build_object(
644+
'id', f.id,
645+
'user_id', f.user_id,
646+
'created_at', f.created_at,
647+
'updated_at', f.updated_at,
648+
'status', f.status,
649+
'friendly_name', f.friendly_name,
650+
'factor_type', f.factor_type,
651+
'secret', f.secret,
652+
'phone', f.phone,
653+
'last_challenged_at', f.last_challenged_at,
654+
'web_authn_credential', f.web_authn_credential
655+
)) from ` + Factor{}.TableName() + ` f where f.user_id = u.id), '[]') as factors_json
656+
from ` + User{}.TableName() + ` u
657+
where ` + query
658+
659+
if err := tx.RawQuery(sqlQuery, args...).First(&result); err != nil {
609660
if errors.Cause(err) == sql.ErrNoRows {
610661
return nil, UserNotFoundError{}
611662
}
612663
return nil, errors.Wrap(err, "error finding user")
613664
}
614665

615-
return obj, nil
666+
// Deserialize identities and factors from JSON
667+
if err := json.Unmarshal(result.IdentitiesJSON, &result.User.Identities); err != nil {
668+
return nil, errors.Wrap(err, "error unmarshaling identities")
669+
}
670+
671+
if err := json.Unmarshal(result.FactorsJSON, &result.User.Factors); err != nil {
672+
return nil, errors.Wrap(err, "error unmarshaling factors")
673+
}
674+
675+
return &result.User, nil
616676
}
617677

618678
// FindUserByEmailAndAudience finds a user with the matching email and audience.

0 commit comments

Comments
 (0)