Применяем весь курс на практике: от простых SELECT до оконных функций и CTE
| Таблица | Ключевые поля | Описание |
|---|---|---|
| users | id, name, age, gender, city | Пользователи |
| communities | id, name, rating | Сообщества |
| posts | id, community_id, title, score | Публикации |
| likes | id, user_id, post_id, liked_at | Лайки к постам |
| views | id, user_id, community_id, viewed_at | Просмотры сообществ |
Первый блок дашборда — сводные цифры: сколько пользователей, лайков, просмотров и каков средний score постов.
SELECT
(SELECT COUNT(*) FROM users) AS total_users,
(SELECT COUNT(*) FROM communities) AS total_communities,
(SELECT COUNT(*) FROM posts) AS total_posts,
(SELECT COUNT(*) FROM likes) AS total_likes,
(SELECT COUNT(*) FROM views) AS total_views,
(SELECT ROUND(AVG(score), 1) FROM posts) AS avg_score;
| total_users | total_communities | total_posts | total_likes | total_views | avg_score |
|---|---|---|---|---|---|
| 10 | 6 | 13 | 18 | 18 | 934.6 |
Какие публикации вызвали наибольший отклик аудитории? Объединяем posts и likes.
SELECT
p.title,
c.name AS community,
p.score,
COUNT(l.id) AS likes_count
FROM posts p
JOIN communities c ON c.id = p.community_id
LEFT JOIN likes l ON l.post_id = p.id
GROUP BY p.id, p.title, c.name, p.score
ORDER BY likes_count DESC, p.score DESC, p.title
LIMIT 5;
| title | community | score | likes_count |
|---|---|---|---|
| review Elden Ring DLC | GamersHub | 800 | 3 |
| tutorial homemade pasta | FoodLovers | 1000 | 2 |
| tutorial portrait lighting | PhotoWorld | 1000 | 2 |
| review dev tools 2026 | TechTalks | 900 | 2 |
| review Lightroom vs Capture | PhotoWorld | 800 | 2 |
p.title база вправе каждый раз отбрасывать разные посты с равными лайками и score. С LIMIT порядок должен быть однозначным — иначе дашборд «мигает» между обновлениями.Самые активные пользователи — те, кто поставил больше всего лайков. Считаем и добавляем их город.
SELECT
u.name,
u.city,
COUNT(l.id) AS likes_count
FROM users u
LEFT JOIN likes l ON l.user_id = u.id
GROUP BY u.id, u.name, u.city
ORDER BY likes_count DESC, u.name
LIMIT 5;
| name | city | likes_count |
|---|---|---|
| Andrey | Moscow | 2 |
| Anna | Moscow | 2 |
| Denis | Kazan | 2 |
| Dmitriy | Kazan | 2 |
| Elvira | Kazan | 2 |
По два лайка у восьми пользователей из десяти — топ-5 здесь мало что говорит, это особенность маленькой учебной базы. На живых данных распределение неравномерное, и такой отчёт показывает ядро аудитории.
Для каждого сообщества: рейтинг, количество постов, средний score постов, количество лайков и просмотров.
LEFT JOIN и посчитать COUNT(DISTINCT …). Счётчики так получатся верными, а AVG(p.score) — нет: каждый пост повторится столько раз, сколько у него лайков, умноженных на просмотры сообщества, и популярные посты перевесят. У GamersHub вышло бы 875 вместо 950. Поэтому каждую метрику считаем отдельно и присоединяем уже готовые итоги.SELECT
c.name,
c.rating,
p.posts_count,
p.avg_score,
COALESCE(l.likes_count, 0) AS likes_count,
COALESCE(v.views_count, 0) AS views_count
FROM communities c
LEFT JOIN (SELECT community_id, COUNT(*) AS posts_count, ROUND(AVG(score), 1) AS avg_score
FROM posts GROUP BY community_id) p ON p.community_id = c.id
LEFT JOIN (SELECT p.community_id, COUNT(*) AS likes_count
FROM likes l JOIN posts p ON p.id = l.post_id
GROUP BY p.community_id) l ON l.community_id = c.id
LEFT JOIN (SELECT community_id, COUNT(*) AS views_count
FROM views GROUP BY community_id) v ON v.community_id = c.id
ORDER BY likes_count DESC, c.name;
| name | rating | posts_count | avg_score | likes_count | views_count |
|---|---|---|---|---|---|
| GamersHub | 4.3 | 2 | 950.0 | 4 | 3 |
| PhotoWorld | 3.8 | 2 | 900.0 | 4 | 5 |
| TechTalks | 4.6 | 2 | 750.0 | 4 | 5 |
| FoodLovers | 4.1 | 2 | 1050.0 | 3 | 2 |
| SportLife | 3.5 | 3 | 1066.7 | 2 | 1 |
| TravelDiaries | 4.2 | 2 | 825.0 | 1 | 2 |
«Новый» пользователь — тот, кто поставил первый лайк на данной неделе. Смотрим, как растёт аудитория. В учебной базе все лайки поставлены за 12 дней января, поэтому группируем по неделям: по месяцам получилась бы одна строка.
WITH first_like AS (
SELECT
user_id,
MIN(liked_at) AS first_at,
DATE_TRUNC('week', MIN(liked_at))::date AS cohort_week
FROM likes
GROUP BY user_id
)
SELECT
cohort_week,
COUNT(*) AS new_users
FROM first_like
GROUP BY cohort_week
ORDER BY cohort_week;
| cohort_week | new_users |
|---|---|
| 2025-12-29 | 3 |
| 2026-01-05 | 6 |
| 2026-01-12 | 1 |
DATE_TRUNC('week', …) возвращает понедельник недели, поэтому первая когорта датирована 29 декабря 2025, хотя лайки в ней — 1–4 января. За первую неделю пришли 3 пользователя, за вторую — 6, за третью — 1.
Оконные функции (RANK, DENSE_RANK, ROW_NUMBER) позволяют ранжировать строки внутри групп, не теряя детализацию.
-- Ранг пользователей по лайкам внутри каждого города
SELECT
u.name,
u.city,
COUNT(l.id) AS likes_count,
RANK() OVER (PARTITION BY u.city ORDER BY COUNT(l.id) DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY u.city ORDER BY COUNT(l.id) DESC) AS dense_rnk
FROM users u
LEFT JOIN likes l ON l.user_id = u.id
GROUP BY u.id, u.name, u.city
ORDER BY u.city, rnk, u.name;
| name | city | likes_count | rnk | dense_rnk |
|---|---|---|---|---|
| Denis | Kazan | 2 | 1 | 1 |
| Dmitriy | Kazan | 2 | 1 | 1 |
| Elvira | Kazan | 2 | 1 | 1 |
| Andrey | Moscow | 2 | 1 | 1 |
| Anna | Moscow | 2 | 1 | 1 |
| Peter | Moscow | 2 | 1 | 1 |
| Maria | Moscow | 1 | 4 | 2 |
| Kate | Novosibirsk | 2 | 1 | 1 |
| Natalie | Novosibirsk | 2 | 1 | 1 |
| Irina | Saint-Petersburg | 1 | 1 | 1 |
Разница между функциями видна на Москве: три пользователя делят первое место, и RANK отдаёт Maria четвёртое — пропуская 2 и 3, а DENSE_RANK — второе, без пропусков.
-- Накопительный итог лайков по датам
SELECT
liked_at,
COUNT(*) AS daily_likes,
SUM(COUNT(*)) OVER (ORDER BY liked_at) AS cumulative_likes
FROM likes
GROUP BY liked_at
ORDER BY liked_at;
| liked_at | daily_likes | cumulative_likes |
|---|---|---|
| 2026-01-01 | 1 | 1 |
| 2026-01-02 | 1 | 2 |
| 2026-01-03 | 2 | 4 |
| 2026-01-04 | 1 | 5 |
| 2026-01-05 | 1 | 6 |
| 2026-01-06 | 1 | 7 |
| 2026-01-07 | 1 | 8 |
| 2026-01-08 | 2 | 10 |
| 2026-01-09 | 2 | 12 |
| 2026-01-10 | 2 | 14 |
| 2026-01-11 | 2 | 16 |
| 2026-01-12 | 2 | 18 |
SUM(COUNT(*)) OVER (ORDER BY liked_at) — оконная функция поверх агрегата. COUNT(*) агрегирует строки в группе, а SUM … OVER считает накопительный итог по всем группам в порядке даты.За эти девять модулей вы освоили:
Следующий шаг — практика на реальных данных. Попробуйте Kaggle, public datasets PostgreSQL или откройте свою базу данных в pgAdmin и задайте ей правильные вопросы.