chore(coderd/database): optimize AuditLogs queries (#18600)

Closes #17689

This PR optimizes the audit logs query performance by extracting the
count operation into a separate query and replacing the OR-based
workspace_builds with conditional joins.

## Query changes
* Extracted count query to separate one
* Replaced single `workspace_builds` join with OR conditions with
separate conditional joins
* Added conditional joins
* `wb_build` for workspace_build audit logs (which is a direct lookup)
    * `wb_workspace` for workspace create audit logs (via workspace)

Optimized AuditLogsOffset query:
https://explain.dalibo.com/plan/4g1hbedg4a564bg8

New CountAuditLogs query:
https://explain.dalibo.com/plan/ga2fbcecb9efbce3
This commit is contained in:
Kacper Sawicki
2025-07-01 07:31:14 +02:00
committed by GitHub
parent 74e1953619
commit 695de6e0c0
15 changed files with 769 additions and 250 deletions

View File

@@ -33,7 +33,9 @@ import (
// - resource_type: string (enum)
// - action: string (enum)
// - build_reason: string (enum)
func AuditLogs(ctx context.Context, db database.Store, query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) {
func AuditLogs(ctx context.Context, db database.Store, query string) (database.GetAuditLogsOffsetParams,
database.CountAuditLogsParams, []codersdk.ValidationError,
) {
// Always lowercase for all searches.
query = strings.ToLower(query)
values, errors := searchTerms(query, func(term string, values url.Values) error {
@@ -41,7 +43,8 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G
return nil
})
if len(errors) > 0 {
return database.GetAuditLogsOffsetParams{}, errors
// nolint:exhaustruct // We don't need to initialize these structs because we return an error.
return database.GetAuditLogsOffsetParams{}, database.CountAuditLogsParams{}, errors
}
const dateLayout = "2006-01-02"
@@ -63,8 +66,24 @@ func AuditLogs(ctx context.Context, db database.Store, query string) (database.G
filter.DateTo = filter.DateTo.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
}
// Prepare the count filter, which uses the same parameters as the GetAuditLogsOffsetParams.
// nolint:exhaustruct // UserID is not obtained from the query parameters.
countFilter := database.CountAuditLogsParams{
RequestID: filter.RequestID,
ResourceID: filter.ResourceID,
ResourceTarget: filter.ResourceTarget,
Username: filter.Username,
Email: filter.Email,
DateFrom: filter.DateFrom,
DateTo: filter.DateTo,
OrganizationID: filter.OrganizationID,
ResourceType: filter.ResourceType,
Action: filter.Action,
BuildReason: filter.BuildReason,
}
parser.ErrorExcessParams(values)
return filter, parser.Errors
return filter, countFilter, parser.Errors
}
func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) {

View File

@@ -343,6 +343,7 @@ func TestSearchAudit(t *testing.T) {
Name string
Query string
Expected database.GetAuditLogsOffsetParams
ExpectedCountParams database.CountAuditLogsParams
ExpectedErrorContains string
}{
{
@@ -372,6 +373,9 @@ func TestSearchAudit(t *testing.T) {
Expected: database.GetAuditLogsOffsetParams{
ResourceTarget: "foo",
},
ExpectedCountParams: database.CountAuditLogsParams{
ResourceTarget: "foo",
},
},
{
Name: "RequestID",
@@ -386,7 +390,7 @@ func TestSearchAudit(t *testing.T) {
// Do not use a real database, this is only used for an
// organization lookup.
db := dbmem.New()
values, errs := searchquery.AuditLogs(context.Background(), db, c.Query)
values, countValues, errs := searchquery.AuditLogs(context.Background(), db, c.Query)
if c.ExpectedErrorContains != "" {
require.True(t, len(errs) > 0, "expect some errors")
var s strings.Builder
@@ -397,6 +401,7 @@ func TestSearchAudit(t *testing.T) {
} else {
require.Len(t, errs, 0, "expected no error")
require.Equal(t, c.Expected, values, "expected values")
require.Equal(t, c.ExpectedCountParams, countValues, "expected count values")
}
})
}