Merge pull request #7037 from pixelfed/perf/seasonal-sql-aggregation

Compute Year-in-Review averages in SQL instead of in PHP
pull/7039/head
Shlee 3 weeks ago committed by GitHub
commit 7ad550d2f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,6 +13,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class SeasonalController extends Controller
{
@ -50,21 +51,25 @@ class SeasonalController extends Controller
$shared = Cache::remember($siteKey, $siteTtl, function () use ($epochStart, $epochEnd) {
return [
'average' => [
'posts' => round(Status::selectRaw('*, count(profile_id) as count')
->whereNull('uri')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
'likes' => round(Like::selectRaw('*, count(profile_id) as count')
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->pluck('count')
->avg()),
'posts' => round((float) DB::query()->fromSub(
Status::query()
->whereNull('uri')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->selectRaw('count(*) as count'),
'per_profile'
)->avg('count')),
'likes' => round((float) DB::query()->fromSub(
Like::query()
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->selectRaw('count(*) as count'),
'per_profile'
)->avg('count')),
],
'popular' => [

@ -0,0 +1,92 @@
<?php
namespace Tests\Feature;
use App\Models\Status;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/*
|--------------------------------------------------------------------------
| Seasonal "Year in Review" aggregation
|--------------------------------------------------------------------------
|
| SeasonalController::getData previously computed the average number of
| posts per profile by loading every grouped row into PHP and calling
| ->pluck('count')->avg(). This verifies the SQL-side replacement:
| AVG over a subquery of per-profile counts returns the same value
| without materialising every group in memory.
|
*/
class SeasonalAggregationTest extends TestCase
{
use RefreshDatabase;
/**
* Mirrors the aggregation used in SeasonalController::getData for
* average posts per profile.
*/
private function averagePostsPerProfile(string $epochStart, string $epochEnd): float
{
return (float) DB::query()->fromSub(
Status::query()
->whereNull('uri')
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->where('created_at', '>', $epochStart)
->where('created_at', '<', $epochEnd)
->groupBy('profile_id')
->selectRaw('count(*) as count'),
'per_profile'
)->avg('count');
}
#[Test]
public function it_averages_matching_posts_per_profile_in_sql()
{
$epochStart = '2020-01-01 00:00:00';
$epochEnd = '2020-12-31 23:59:59';
$inRange = '2020-06-01 12:00:00';
// Profile 1: 2 matching photos. Profile 2: 4 matching photos.
// Average per profile = 3.
Status::factory()->count(2)->photo()->create([
'profile_id' => 1001,
'created_at' => $inRange,
]);
Status::factory()->count(4)->photo()->create([
'profile_id' => 1002,
'created_at' => $inRange,
]);
// Noise that must be excluded from the average:
// remote (uri set), wrong type, and out-of-range date.
Status::factory()->photo()->create([
'profile_id' => 1003,
'uri' => 'https://remote.example/statuses/1',
'created_at' => $inRange,
]);
Status::factory()->create([
'profile_id' => 1003,
'type' => 'text',
'created_at' => $inRange,
]);
Status::factory()->photo()->create([
'profile_id' => 1004,
'created_at' => '2019-01-01 00:00:00',
]);
$this->assertSame(3.0, $this->averagePostsPerProfile($epochStart, $epochEnd));
}
#[Test]
public function it_returns_zero_when_no_posts_match()
{
$average = $this->averagePostsPerProfile('2020-01-01 00:00:00', '2020-12-31 23:59:59');
// No rows -> AVG returns null -> cast to 0.0
$this->assertSame(0.0, $average);
}
}
Loading…
Cancel
Save