From 1aa9ec17859298f03466a4ed4af46820cee0f18c Mon Sep 17 00:00:00 2001 From: DariusIII Date: Wed, 1 Apr 2026 15:05:43 +0200 Subject: [PATCH] Improve speed of user list --- .../Controllers/Admin/AdminUserController.php | 5 -- app/Models/User.php | 58 ++++++--------- ...01_120000_add_user_sort_column_indexes.php | 72 +++++++++++++++++++ resources/views/admin/users/index.blade.php | 5 +- 4 files changed, 95 insertions(+), 45 deletions(-) create mode 100644 database/migrations/2026_04_01_120000_add_user_sort_column_indexes.php diff --git a/app/Http/Controllers/Admin/AdminUserController.php b/app/Http/Controllers/Admin/AdminUserController.php index 1b856cb3a..7d53eedb5 100644 --- a/app/Http/Controllers/Admin/AdminUserController.php +++ b/app/Http/Controllers/Admin/AdminUserController.php @@ -52,17 +52,12 @@ class AdminUserController extends BasePageController $variables['email'], $variables['host'], $variables['role'], - true, $variables['created_from'], $variables['created_to'] ); $results = $this->paginate($result, User::getCount($variables['role'], $variables['username'], $variables['host'], $variables['email'], $variables['created_from'], $variables['created_to']), config('nntmux.items_per_page'), $page, $request->url(), $request->query()); - // Note: API request counts are already included via the getRange query when $apiRequests = true - // Country lookups and additional counts removed to improve performance on large datasets - // These can be added back via individual user profile pages or AJAX calls if needed - // Build order by URLs $orderByUrls = []; foreach ($ordering as $orderType) { diff --git a/app/Models/User.php b/app/Models/User.php index e02ca552d..1cf0e2b75 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1293,8 +1293,6 @@ final class User extends Authenticatable implements MustVerifyEmailContract * Get paginated user list with filters. * * @return Collection - * - * @throws \Throwable */ public static function getRange( int|false $start, @@ -1304,47 +1302,33 @@ final class User extends Authenticatable implements MustVerifyEmailContract ?string $email = '', ?string $host = '', ?string $role = '', - bool $apiRequests = false, ?string $createdFrom = '', ?string $createdTo = '', ): Collection { $order = self::getBrowseOrder($orderBy); - if ($apiRequests) { - UserRequest::clearApiRequests(false); - - $query = ' - SELECT users.*, roles.name AS rolename, COUNT(user_requests.id) AS apirequests - FROM users - INNER JOIN roles ON roles.id = users.roles_id - LEFT JOIN user_requests ON user_requests.users_id = users.id - WHERE users.id != 0 %s %s %s %s %s %s - AND email != \'sharing@nZEDb.com\' - GROUP BY users.id - ORDER BY %s %s %s'; - } else { - $query = ' - SELECT users.*, roles.name AS rolename - FROM users - INNER JOIN roles ON roles.id = users.roles_id - WHERE 1=1 %s %s %s %s %s %s - ORDER BY %s %s %s'; - } - - return static::fromQuery( - sprintf( - $query, - $userName ? 'AND users.username LIKE '.escapeString("%{$userName}%") : '', - $email ? 'AND users.email LIKE '.escapeString("%{$email}%") : '', - $host ? 'AND users.host LIKE '.escapeString("%{$host}%") : '', - $role ? "AND users.roles_id = {$role}" : '', - $createdFrom ? 'AND users.created_at >= '.escapeString("{$createdFrom} 00:00:00") : '', - $createdTo ? 'AND users.created_at <= '.escapeString("{$createdTo} 23:59:59") : '', - $order[0], - $order[1], - $start === false ? '' : "LIMIT {$offset} OFFSET {$start}" + return self::query() + ->withTrashed() + ->select('users.*') + ->selectSub( + 'SELECT name FROM roles WHERE roles.id = users.roles_id', + 'rolename' ) - ); + ->excludeSharing() + ->when($userName, fn (Builder $q) => $q->where('username', 'like', "%{$userName}%")) + ->when($email, fn (Builder $q) => $q->where('email', 'like', "%{$email}%")) + ->when($host, fn (Builder $q) => $q->where('host', 'like', "%{$host}%")) + ->when($role, fn (Builder $q) => $q->where('roles_id', $role)) + ->when($createdFrom, fn (Builder $q) => $q->where('created_at', '>=', "{$createdFrom} 00:00:00")) + ->when($createdTo, fn (Builder $q) => $q->where('created_at', '<=', "{$createdTo} 23:59:59")) + ->withCount([ + 'requests as daily_api_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()), + 'downloads as daily_download_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()), + ]) + ->with('roles:id,name') + ->orderBy($order[0], $order[1]) + ->when($start !== false, fn (Builder $q) => $q->offset($start)->limit($offset)) + ->get(); } /** diff --git a/database/migrations/2026_04_01_120000_add_user_sort_column_indexes.php b/database/migrations/2026_04_01_120000_add_user_sort_column_indexes.php new file mode 100644 index 000000000..840af716f --- /dev/null +++ b/database/migrations/2026_04_01_120000_add_user_sort_column_indexes.php @@ -0,0 +1,72 @@ + 'username', + 'ix_users_email' => 'email', + 'ix_users_host' => 'host', + 'ix_users_lastlogin' => 'lastlogin', + 'ix_users_apiaccess' => 'apiaccess', + 'ix_users_grabs' => 'grabs', + 'ix_users_rolechangedate' => 'rolechangedate', + 'ix_users_verified' => 'verified', + ]; + + foreach ($indexes as $indexName => $column) { + if (! $this->indexExists('users', $indexName)) { + Schema::table('users', function (Blueprint $table) use ($indexName, $column) { + $table->index($column, $indexName); + }); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $indexes = [ + 'ix_users_username', + 'ix_users_email', + 'ix_users_host', + 'ix_users_lastlogin', + 'ix_users_apiaccess', + 'ix_users_grabs', + 'ix_users_rolechangedate', + 'ix_users_verified', + ]; + + foreach ($indexes as $indexName) { + if ($this->indexExists('users', $indexName)) { + Schema::table('users', function (Blueprint $table) use ($indexName) { + $table->dropIndex($indexName); + }); + } + } + } + + private function indexExists(string $table, string $indexName): bool + { + $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); + + return count($indexes) > 0; + } +}; diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index b035a410c..8a60e9292 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -215,18 +215,17 @@ - {{ $user->roles->first()->name ?? 'N/A' }} + {{ $user->rolename ?? 'N/A' }} @if(!empty($user->pending_roles_id) && !empty($user->pending_role_start_date)) @php - $pendingRole = \Spatie\Permission\Models\Role::find($user->pending_roles_id); $pendingStartDate = \Carbon\Carbon::parse($user->pending_role_start_date); @endphp
- {{ $pendingRole->name ?? 'Unknown' }} + {{ $role_names[$user->pending_roles_id] ?? 'Unknown' }} {{ $pendingStartDate->diffForHumans() }}