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

@ -46,7 +46,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
}
queryStr := r.URL.Query().Get("q")
filter, errs := searchquery.AuditLogs(ctx, api.Database, queryStr)
filter, countFilter, errs := searchquery.AuditLogs(ctx, api.Database, queryStr)
if len(errs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid audit search query.",
@ -62,6 +62,27 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
if filter.Username == "me" {
filter.UserID = apiKey.UserID
filter.Username = ""
countFilter.UserID = apiKey.UserID
countFilter.Username = ""
}
// Use the same filters to count the number of audit logs
count, err := api.Database.CountAuditLogs(ctx, countFilter)
if dbauthz.IsNotAuthorizedError(err) {
httpapi.Forbidden(rw)
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
// If count is 0, then we don't need to query audit logs
if count == 0 {
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AuditLogResponse{
AuditLogs: []codersdk.AuditLog{},
Count: 0,
})
return
}
dblogs, err := api.Database.GetAuditLogsOffset(ctx, filter)
@ -73,19 +94,10 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
httpapi.InternalServerError(rw, err)
return
}
// GetAuditLogsOffset does not return ErrNoRows because it uses a window function to get the count.
// So we need to check if the dblogs is empty and return an empty array if so.
if len(dblogs) == 0 {
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AuditLogResponse{
AuditLogs: []codersdk.AuditLog{},
Count: 0,
})
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AuditLogResponse{
AuditLogs: api.convertAuditLogs(ctx, dblogs),
Count: dblogs[0].Count,
Count: count,
})
}