Poznajemy group by i agregowanie po grupach w SQL. Kontynuacja lekcji poprzednich. Do dzieła!
Ok, małe przypomnienie tego i owego. Przypomnijmy sobie tę komendę:
Artisan::command('review-min-rating-2', function () {
$min = Review::min('rating');
$numberOfMinReviews = Review::where('rating', $min)->count();
$this->comment("Minimum rating: {$min}");
$this->comment("Number of reviews with minimal rating: {$numberOfMinReviews }");
});
Czyli nasz pierwszy min na całej tabeli. Odpowiednik:
SELECT min(rating)
from `reviews`;
Warto zwrócić uwagę, że tutaj dostajemy jedną odpowiedź. Ok, nasz pierwszy sub-select:
SELECT COUNT(rating)
from `reviews`
where rating = (SELECT min(rating) from `reviews`);
Oraz podejście ze zmiennymi SQL:
SELECT @min_rating := min(rating) from `reviews`;
SELECT count(*)
from `reviews`
where rating = @min_rating;
Nasz pierwszy max (Eloquent oraz fasada DB):
Artisan::command('review-max-rating', function () {
$max = Review::max('rating');
$this->comment("Max rating: {$max}");
});
Artisan::command('review-max-rating-db', function () {
$max = DB::table('reviews')->max('rating');
$this->comment("Max rating: {$max}");
});
A tutaj funkcje with (nie wszystkie):
Artisan::command('movies-with-minmax', function () {
$movies = Movie::withSum('reviews', 'rating')
->withMin('reviews','rating')
->withMax('reviews', 'rating')
->get();
foreach($movies as $movie){
$this->comment("Movie title: {$movie->title}");
$this->comment("Reviews - min rating: {$movie->reviews_min_rating}");
$this->comment("Reviews - max rating: {$movie->reviews_max_rating}");
$this->comment("Reviews - sum of all ratings: {$movie->reviews_sum_rating}");
}
});
A tutaj select, który zaciąga wszystko z movies i sub select, który liczy średnią z recenzji, ale tylko dla każdego filmu i dla każdego filmu daje kolumnę avg_rating:
select m.*,
(select avg(r.rating)
from `reviews` as r
where m.id = r.movie_id) as `reviews_avg_rating`
from `movies` as m;
I jeszcze raz przypominam – to jest O(n^2), to jest pętla w pętli, dla każdego movie leci osobny sub-select, z innym m.id, które jest porównywane z r.movie_id i tylko te są liczone, tak ta magia działa, że dostajemy do każdego filmu reviews_avg_rating.
Tam można dać sum, min, max, count(*), dopiero withExists wymaga odrobinę więcej myślenia i znajomości SQL:
select m.*,
exists(
select *
from `reviews` as r
where m.id = r.movie_id
) as `reviews_exists`
from `movies` as m;
Ok, ale teraz podejdziemy troszkę od innej strony. Od strony tabeli reviews i spróbujemy wyliczyć średnią:
SELECT avg(rating) from `reviews`;
Ok, fajnie, zrobiliśmy:
$avg = Review::avg('rating');
No, niby co to ma być? Nic wielkiego, umiemy to. Ok, to teraz taki kod:
SELECT avg(rating), movie_id from `reviews`;
Ok, myślałem, że będzie błąd, dostaliśmy jeden rekord, id 1, jakaś średnia (pewnie dla wszystkich rekordów). Sprawdźmy to raz jeszcze:
SELECT avg(rating), movie_id from `reviews`;
SELECT avg(rating) from `reviews` WHERE movie_id = 1;
Ok, dostajemy dwa result sety, drugi pokazuje prawdziwą średnią dla recenzji o id 1, pierwszy jest nieco popsuty, pokazuje średnią dla wszystkich ocen wszystkich filmów plus id 1.
Generalnie pomyślmy o co nam chodzi w pierwszym przypadku. Chodzi nam o to, aby podać średnią ocen dla każdego filmu. A zatem musimy jakoś te poagregowane dane… pogrupować.
Tak:
SELECT avg(rating), movie_id
from `reviews`
GROUP BY movie_id;
Teraz już wszystko działa jak należy. Dostajemy średnią ocen dla każdego filmu z osobna.
Możemy też policzyć sumę wszystkich ocen dla każdego z filmów:
SELECT sum(rating), movie_id
from `reviews`
GROUP BY movie_id;
Możemy też więcej wyciągnąć funkcji agregujących, ważne aby grupowanie się zgadzało:
SELECT sum(rating),
avg(rating),
min(rating),
max(rating),
count(*),
movie_id
from `reviews`
GROUP BY movie_id;
Możemy to sobie poaliasować, tak czy inaczej wyciągnęliśmy dla każdego filmu średnią ocen, minimalną ocenę, maksymalną ocenę, sumę ocen oraz ilość recenzji.
A jak to robić w Laravelu? Ciężko jest. Eloquent odpada, bo tutaj nie pracujemy na modelu, tylko dziwnie poagregowanych danych. No odpada, pomyślmy chwilę, to sami do tego dojdziemy.
Fasada DB? Tak, ale z DB::raw:
$user_info = DB::table('usermetas')
->select('browser', DB::raw('count(*) as total'))
->groupBy('browser')
->get();
Oczywiście dla chcącego nic trudnego i DB raw możemy się pozbyć z pewną paczką:
composer require tpetry/laravel-query-expressions
Nie będziemy tego teraz tłumaczyć, jeszcze to sobie zrobimy, ale dobra znajomość SQL była tu priorytetem. Mam nadzieję, że pewne rzeczy się rozjaśniają, zwłaszcza że SQL to temat rzeka i w dodatku bardzo ciekawy i tylko te początki są trudne, jak już to konceptualnie ogarniemy to później jest magia.