Tighten artwork hourly snapshot window queries.
Reduce snapshot work to the active hour window so metrics aggregation stays cheaper on the hot path.
This commit is contained in:
@@ -102,42 +102,48 @@ final class ArtworkHourlySnapshotWindow
|
||||
*/
|
||||
public function artworkPeriodDeltas(CarbonInterface|\DateTimeInterface $start, ?int $userId = null): Builder
|
||||
{
|
||||
$inWindow = DB::table('artwork_metric_snapshots_hourly as snapshots');
|
||||
$this->constrainOwner($inWindow, $userId);
|
||||
$inWindow
|
||||
->where('snapshots.bucket_hour', '>=', $start)
|
||||
->groupBy('snapshots.artwork_id')
|
||||
->select('snapshots.artwork_id')
|
||||
->selectRaw('MAX(snapshots.views_count) as views_latest')
|
||||
->selectRaw('MIN(snapshots.views_count) as views_min')
|
||||
->selectRaw('MAX(snapshots.downloads_count) as downloads_latest')
|
||||
->selectRaw('MIN(snapshots.downloads_count) as downloads_min')
|
||||
->selectRaw('MAX(snapshots.favourites_count) as favourites_latest')
|
||||
->selectRaw('MIN(snapshots.favourites_count) as favourites_min')
|
||||
->selectRaw('MAX(snapshots.comments_count) as comments_latest')
|
||||
->selectRaw('MIN(snapshots.comments_count) as comments_min')
|
||||
->selectRaw('MAX(snapshots.shares_count) as shares_latest')
|
||||
->selectRaw('MIN(snapshots.shares_count) as shares_min');
|
||||
$start = \Carbon\Carbon::parse($start);
|
||||
$baselineFrom = (clone $start)->subHours(self::BASELINE_LOOKBACK_HOURS);
|
||||
|
||||
$baselineFrom = \Carbon\Carbon::parse($start)->subHours(self::BASELINE_LOOKBACK_HOURS);
|
||||
$baseline = DB::table('artwork_metric_snapshots_hourly as snapshots');
|
||||
$this->constrainOwner($baseline, $userId);
|
||||
$baseline
|
||||
->where('snapshots.bucket_hour', '<', $start)
|
||||
->where('snapshots.bucket_hour', '>=', $baselineFrom)
|
||||
->groupBy('snapshots.artwork_id')
|
||||
->select('snapshots.artwork_id')
|
||||
->selectRaw('MAX(snapshots.views_count) as views_baseline')
|
||||
->selectRaw('MAX(snapshots.downloads_count) as downloads_baseline')
|
||||
->selectRaw('MAX(snapshots.favourites_count) as favourites_baseline')
|
||||
->selectRaw('MAX(snapshots.comments_count) as comments_baseline')
|
||||
->selectRaw('MAX(snapshots.shares_count) as shares_baseline');
|
||||
// Resolve the three global hourly buckets first. These are cheap indexed
|
||||
// lookups and avoid aggregating millions of cumulative snapshot rows.
|
||||
$latestBucket = DB::table('artwork_metric_snapshots_hourly')
|
||||
->where('bucket_hour', '>=', $start)
|
||||
->max('bucket_hour');
|
||||
|
||||
$firstBucket = DB::table('artwork_metric_snapshots_hourly')
|
||||
->where('bucket_hour', '>=', $start)
|
||||
->min('bucket_hour');
|
||||
|
||||
$baselineBucket = DB::table('artwork_metric_snapshots_hourly')
|
||||
->where('bucket_hour', '<', $start)
|
||||
->where('bucket_hour', '>=', $baselineFrom)
|
||||
->max('bucket_hour');
|
||||
|
||||
$latest = $this->exactBucketSnapshot(
|
||||
$latestBucket,
|
||||
$userId,
|
||||
'latest',
|
||||
);
|
||||
|
||||
$first = $this->exactBucketSnapshot(
|
||||
$firstBucket,
|
||||
$userId,
|
||||
'min',
|
||||
);
|
||||
|
||||
$baseline = $this->exactBucketSnapshot(
|
||||
$baselineBucket,
|
||||
$userId,
|
||||
'baseline',
|
||||
);
|
||||
|
||||
$bornSql = $this->quotedTimestamp($start);
|
||||
|
||||
return DB::query()
|
||||
->fromSub($inWindow, 'win')
|
||||
->fromSub($latest, 'win')
|
||||
->leftJoinSub($baseline, 'base', 'base.artwork_id', '=', 'win.artwork_id')
|
||||
->leftJoinSub($first, 'first', 'first.artwork_id', '=', 'win.artwork_id')
|
||||
->join('artworks', 'artworks.id', '=', 'win.artwork_id')
|
||||
->select('win.artwork_id')
|
||||
->selectRaw($this->deltaExpression('views', $bornSql) . ' as views_delta')
|
||||
@@ -147,6 +153,7 @@ final class ArtworkHourlySnapshotWindow
|
||||
->selectRaw($this->deltaExpression('shares', $bornSql) . ' as shares_delta');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array{oldest_bucket: ?string, newest_bucket: ?string, coverage_days: float, window_complete: bool, requested_days: int}
|
||||
*/
|
||||
@@ -194,6 +201,34 @@ final class ArtworkHourlySnapshotWindow
|
||||
->whereNull('owner_artworks.deleted_at');
|
||||
}
|
||||
|
||||
private function exactBucketSnapshot(
|
||||
?string $bucket,
|
||||
?int $userId,
|
||||
string $suffix,
|
||||
): Builder {
|
||||
$query = DB::table('artwork_metric_snapshots_hourly as snapshots');
|
||||
|
||||
if (DB::connection()->getDriverName() === 'mysql') {
|
||||
$query->forceIndex('idx_bucket_artwork');
|
||||
}
|
||||
|
||||
$this->constrainOwner($query, $userId);
|
||||
|
||||
if ($bucket === null) {
|
||||
$query->whereRaw('1 = 0');
|
||||
} else {
|
||||
$query->where('snapshots.bucket_hour', $bucket);
|
||||
}
|
||||
|
||||
return $query
|
||||
->select('snapshots.artwork_id')
|
||||
->selectRaw("snapshots.views_count as views_{$suffix}")
|
||||
->selectRaw("snapshots.downloads_count as downloads_{$suffix}")
|
||||
->selectRaw("snapshots.favourites_count as favourites_{$suffix}")
|
||||
->selectRaw("snapshots.comments_count as comments_{$suffix}")
|
||||
->selectRaw("snapshots.shares_count as shares_{$suffix}");
|
||||
}
|
||||
|
||||
private function quotedTimestamp(CarbonInterface|\DateTimeInterface $start): string
|
||||
{
|
||||
return DB::getPdo()->quote(\Carbon\Carbon::parse($start)->toDateTimeString());
|
||||
@@ -202,7 +237,7 @@ final class ArtworkHourlySnapshotWindow
|
||||
private function deltaExpression(string $metric, string $bornSql): string
|
||||
{
|
||||
$latest = "COALESCE(win.{$metric}_latest, 0)";
|
||||
$min = "COALESCE(win.{$metric}_min, 0)";
|
||||
$min = "COALESCE(first.{$metric}_min, 0)";
|
||||
$base = "base.{$metric}_baseline";
|
||||
$bornInWindow = "COALESCE(artworks.published_at, artworks.created_at) >= {$bornSql}";
|
||||
$observed = $this->clampNonNegative("{$latest} - {$min}");
|
||||
|
||||
Reference in New Issue
Block a user