Skip to content

Commit 734ff2b

Browse files
committed
use window functions for efficiency
1 parent ce1bda5 commit 734ff2b

File tree

3 files changed

+127
-66
lines changed

3 files changed

+127
-66
lines changed

coderd/database/querier_test.go

+31-26
Original file line numberDiff line numberDiff line change
@@ -2258,6 +2258,10 @@ func TestGroupRemovalTrigger(t *testing.T) {
22582258
func TestGetUserStatusChanges(t *testing.T) {
22592259
t.Parallel()
22602260

2261+
now := dbtime.Now()
2262+
createdAt := now.Add(-5 * 24 * time.Hour) // 5 days ago
2263+
firstTransitionTime := createdAt.Add(2 * 24 * time.Hour) // 3 days ago
2264+
secondTransitionTime := firstTransitionTime.Add(2 * 24 * time.Hour) // 1 days ago
22612265
t.Run("No Users", func(t *testing.T) {
22622266
t.Parallel()
22632267
db, _ := dbtestutil.NewDB(t)
@@ -2307,8 +2311,6 @@ func TestGetUserStatusChanges(t *testing.T) {
23072311
ctx := testutil.Context(t, testutil.WaitShort)
23082312

23092313
// Create a user that's been in the specified status for the past 30 days
2310-
now := dbtime.Now()
2311-
createdAt := now.Add(-29 * 24 * time.Hour)
23122314
dbgen.User(t, db, database.User{
23132315
Status: tc.status,
23142316
CreatedAt: createdAt,
@@ -2324,8 +2326,10 @@ func TestGetUserStatusChanges(t *testing.T) {
23242326
require.NotEmpty(t, userStatusChanges, "should return results")
23252327

23262328
// We should have an entry for each status change
2327-
require.Len(t, userStatusChanges, 1, "should have 1 status change")
2328-
require.Equal(t, userStatusChanges[0].NewStatus, tc.status, "should have the correct status")
2329+
require.Len(t, userStatusChanges, 5, "should have 1 user * 5 days = 5 rows")
2330+
for _, row := range userStatusChanges {
2331+
require.Equal(t, row.NewStatus, tc.status, "should have the correct status")
2332+
}
23292333
})
23302334
}
23312335
})
@@ -2393,20 +2397,17 @@ func TestGetUserStatusChanges(t *testing.T) {
23932397
ctx := testutil.Context(t, testutil.WaitShort)
23942398

23952399
// Create a user that starts with initial status
2396-
now := dbtime.Now()
2397-
createdAt := now.Add(-5 * 24 * time.Hour) // 5 days ago
23982400
user := dbgen.User(t, db, database.User{
23992401
Status: tc.initialStatus,
24002402
CreatedAt: createdAt,
24012403
UpdatedAt: createdAt,
24022404
})
24032405

24042406
// After 2 days, change status to target status
2405-
statusChangeTime := createdAt.Add(2 * 24 * time.Hour)
24062407
user, err := db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{
24072408
ID: user.ID,
24082409
Status: tc.targetStatus,
2409-
UpdatedAt: statusChangeTime,
2410+
UpdatedAt: firstTransitionTime,
24102411
})
24112412
require.NoError(t, err)
24122413

@@ -2418,10 +2419,15 @@ func TestGetUserStatusChanges(t *testing.T) {
24182419
require.NoError(t, err)
24192420
require.NotEmpty(t, userStatusChanges, "should return results")
24202421

2421-
// We should have an entry for each status change, including the initial status
2422-
require.Len(t, userStatusChanges, 2, "should have 2 status changes")
2423-
require.Equal(t, userStatusChanges[0].NewStatus, tc.initialStatus, "should have the initial status")
2424-
require.Equal(t, userStatusChanges[1].NewStatus, tc.targetStatus, "should have the target status")
2422+
// We should have an entry for each status (active, dormant, suspended) for each day
2423+
require.Len(t, userStatusChanges, 5, "should have 1 user * 5 days = 5 rows")
2424+
for _, row := range userStatusChanges {
2425+
if row.ChangedAt.Before(firstTransitionTime) {
2426+
require.Equal(t, row.NewStatus, tc.initialStatus, "should have the initial status")
2427+
} else {
2428+
require.Equal(t, row.NewStatus, tc.targetStatus, "should have the target status")
2429+
}
2430+
}
24252431
})
24262432
}
24272433
})
@@ -2606,20 +2612,18 @@ func TestGetUserStatusChanges(t *testing.T) {
26062612
})
26072613

26082614
// First transition at 2 days
2609-
user1TransitionTime := createdAt.Add(2 * 24 * time.Hour)
26102615
user1, err := db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{
26112616
ID: user1.ID,
26122617
Status: tc.user1Transition.to,
2613-
UpdatedAt: user1TransitionTime,
2618+
UpdatedAt: firstTransitionTime,
26142619
})
26152620
require.NoError(t, err)
26162621

26172622
// Second transition at 4 days
2618-
user2TransitionTime := createdAt.Add(4 * 24 * time.Hour)
26192623
user2, err = db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{
26202624
ID: user2.ID,
26212625
Status: tc.user2Transition.to,
2622-
UpdatedAt: user2TransitionTime,
2626+
UpdatedAt: secondTransitionTime,
26232627
})
26242628
require.NoError(t, err)
26252629

@@ -2631,16 +2635,17 @@ func TestGetUserStatusChanges(t *testing.T) {
26312635
require.NoError(t, err)
26322636
require.NotEmpty(t, userStatusChanges)
26332637

2634-
// We should have an entry with the correct status changes for each user, including the initial status
2635-
require.Len(t, userStatusChanges, 4, "should have 4 status changes")
2636-
require.Equal(t, userStatusChanges[0].UserID, user1.ID, "should have the first user")
2637-
require.Equal(t, userStatusChanges[0].NewStatus, tc.user1Transition.from, "should have the first user's initial status")
2638-
require.Equal(t, userStatusChanges[1].UserID, user1.ID, "should have the first user")
2639-
require.Equal(t, userStatusChanges[1].NewStatus, tc.user1Transition.to, "should have the first user's target status")
2640-
require.Equal(t, userStatusChanges[2].UserID, user2.ID, "should have the second user")
2641-
require.Equal(t, userStatusChanges[2].NewStatus, tc.user2Transition.from, "should have the second user's initial status")
2642-
require.Equal(t, userStatusChanges[3].UserID, user2.ID, "should have the second user")
2643-
require.Equal(t, userStatusChanges[3].NewStatus, tc.user2Transition.to, "should have the second user's target status")
2638+
// Expected counts before, between and after the transitions should match:
2639+
for _, row := range userStatusChanges {
2640+
switch {
2641+
case row.ChangedAt.Before(firstTransitionTime):
2642+
require.Equal(t, row.Count, tc.expectedCounts["initial"][row.NewStatus], "should have the correct count before the first transition")
2643+
case row.ChangedAt.Before(secondTransitionTime):
2644+
require.Equal(t, row.Count, tc.expectedCounts["between"][row.NewStatus], "should have the correct count between the transitions")
2645+
case row.ChangedAt.Before(now):
2646+
require.Equal(t, row.Count, tc.expectedCounts["final"][row.NewStatus], "should have the correct count after the second transition")
2647+
}
2648+
}
26442649
})
26452650
}
26462651
})

coderd/database/queries.sql.go

+48-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/insights.sql

+48-20
Original file line numberDiff line numberDiff line change
@@ -773,36 +773,64 @@ JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.wor
773773
GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value;
774774

775775
-- name: GetUserStatusChanges :many
776-
WITH last_status_per_day AS (
777-
-- First get the last status change for each user for each day
778-
SELECT DISTINCT ON (date_trunc('day', changed_at), user_id)
779-
date_trunc('day', changed_at)::timestamptz AS date,
776+
WITH dates AS (
777+
SELECT generate_series(
778+
date_trunc('day', @start_time::timestamptz),
779+
date_trunc('day', @end_time::timestamptz),
780+
'1 day'::interval
781+
)::timestamptz AS date
782+
),
783+
latest_status_before_range AS (
784+
-- Get the last status change for each user before our date range
785+
SELECT DISTINCT ON (user_id)
786+
user_id,
780787
new_status,
781-
user_id
788+
changed_at
782789
FROM user_status_changes
783-
WHERE changed_at >= @start_time::timestamptz
784-
AND changed_at < @end_time::timestamptz
785-
ORDER BY
786-
date_trunc('day', changed_at),
790+
WHERE changed_at < (SELECT MIN(date) FROM dates)
791+
ORDER BY user_id, changed_at DESC
792+
),
793+
all_status_changes AS (
794+
-- Combine status changes before and during our range
795+
SELECT
796+
user_id,
797+
new_status,
798+
changed_at
799+
FROM latest_status_before_range
800+
801+
UNION ALL
802+
803+
SELECT
787804
user_id,
788-
changed_at DESC -- This ensures we get the last status for each day
805+
new_status,
806+
changed_at
807+
FROM user_status_changes
808+
WHERE changed_at < @end_time::timestamptz
789809
),
790810
daily_counts AS (
791-
-- Then count unique users per status per day
792811
SELECT
793-
date,
794-
new_status,
795-
COUNT(*) AS count
796-
FROM last_status_per_day
797-
GROUP BY
798-
date,
799-
new_status
812+
d.date,
813+
asc1.new_status,
814+
-- For each date and status, count users whose most recent status change
815+
-- (up to that date) matches this status
816+
COUNT(*) FILTER (
817+
WHERE asc1.changed_at = (
818+
SELECT MAX(changed_at)
819+
FROM all_status_changes asc2
820+
WHERE asc2.user_id = asc1.user_id
821+
AND asc2.changed_at <= d.date
822+
)
823+
)::bigint AS count
824+
FROM dates d
825+
CROSS JOIN all_status_changes asc1
826+
GROUP BY d.date, asc1.new_status
800827
)
801828
SELECT
802-
date::timestamptz AS changed_at,
829+
date AS changed_at,
803830
new_status,
804-
count::bigint
831+
count
805832
FROM daily_counts
833+
WHERE count > 0
806834
ORDER BY
807835
new_status ASC,
808836
date ASC;

0 commit comments

Comments
 (0)