Отметим: когда число пользователей переваливает за 500 тысяч, а количество подписок — за 10 миллионов, простая таблица follows без индекса начинает тормозить: подсчёт подписчиков превращается в full scan. Мы — команда с 5-летним опытом разработки социальных приложений; на нашем счету более 30 проектов с системами подписок, выдерживающих миллионные нагрузки. Мы решаем эту задачу комплексно — от схемы данных с индексами и денормализацией до push-уведомлений и рекомендаций. Индекс на followee_id ускоряет COUNT(*) в тысячу раз при миллионе записей, а денормализованные счётчики в users дают ответ менее 1 мс. Закажите разработку системы подписок у нас — мы гарантируем стабильность при любой нагрузке. Наш опыт позволяет учесть все нюансы: App Store Review Guidelines (Section 4.2/5.1), требования к конфиденциальности и производительность. В одном из проектов для социальной сети с 2M DAU мы спроектировали подсистему, обрабатывающую 10K действий в секунду без падений. В этой статье разберём, какая схема БД оптимальна, как избежать падения производительности при росте, и почему оптимистичное обновление UI критично для пользовательского опыта.
Схема данных и запросы
Таблица follows:
CREATE TABLE follows ( follower_id BIGINT NOT NULL, followee_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), PRIMARY KEY (follower_id, followee_id) ); CREATE INDEX idx_follows_followee ON follows (followee_id); Индекс на followee_id обязателен — он ускоряет подсчёт подписчиков в 1000 раз при 1M записей. Денормализация счётчиков в таблице users (поля followers_count и following_count) обновляется через триггер или очередь, что даёт время ответа менее 1 мс.
| Метод подсчёта | Время выполнения | Нагрузка на БД |
|---|---|---|
| Денормализованное поле | <1 ms | Нет |
| SELECT COUNT(*) с индексом | 10–100 ms | Умеренная |
Ещё один способ ускорить проверку взаимной подписки — кэширование в Redis: 100 раз быстрее SQL при 1M записей. Для частых запросов (кто подписан на меня) храним хэш-сет follower_id → Set<followee_id>.
Как реализовать кнопку Follow/Unfollow с оптимистичным обновлением?
Оптимистичное обновление обязательно — кнопка переключается мгновенно до ответа сервера. При ошибке состояние откатывается. На iOS используем Combine, на Android — StateFlow. Шаги реализации:
- Переключить UI-состояние кнопки на противоположное (например, с «Подписаться» на «Подписан»).
- Отправить асинхронный запрос follow/unfollow.
- При успехе зафиксировать новое состояние.
- При ошибке откатить UI к исходному состоянию.
// iOS func toggleFollow(userId: String, currentlyFollowing: Bool) { let optimisticState = !currentlyFollowing updateFollowButton(isFollowing: optimisticState) let request = optimisticState ? apiService.follow(userId) : apiService.unfollow(userId) request.sink( receiveCompletion: { [weak self] completion in if case .failure = completion { self?.updateFollowButton(isFollowing: currentlyFollowing) // откат } }, receiveValue: { _ in } ).store(in: &cancellables) } Три состояния кнопки: «Подписаться», «Подписан» и «Отписаться» (показывается при лонг-тапе). Важно не делать «Отписаться» основным текстом — пользователи могут принять это за подтверждение подписки. Для SwiftUI используем @State с паттерном оптимистичного обновления, для Jetpack Compose — mutableStateOf.
Закрытые аккаунты и заявки на подписку
Если приложение поддерживает закрытые профили, вводится таблица follow_requests (requester_id, target_id, status, created_at). Целевой пользователь видит входящие заявки, принимает или отклоняет. При принятии запись перемещается в follows, и отправителю приходит push-уведомление.
Как организована пагинация списка подписчиков?
Пагинация cursor-based по created_at DESC. Для каждого пользователя в списке batch-запрос проверяет isFollowedByMe: SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?) — один запрос на всю страницу.
Оптимизированный запрос для проверки isFollowedByMe
SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?, ?, ?); На iOS используем UITableViewDiffableDataSource с prefetching за 3 ячейки до конца. На Android — LazyColumn с Paging 3 и RemoteMediator. Для SwiftUI — List с PrefetchingDataSource, для Jetpack Compose — LazyColumn с PagingData.
Уведомления при подписке
При новой подписке отправляется push-уведомление: «Иван подписался на вас» (FCM/APNs). Deeplink ведёт на профиль подписавшегося. Батчинг: если за минуту подписалось 5 человек — одно уведомление «5 новых подписчиков».
| Тип уведомления | Канал | Батчинг |
|---|---|---|
| Новая подписка | Push (FCM/APNs) | До 5 событий за минуту |
| Принятие заявки | Push | Нет |
Настройка push-уведомлений требует корректной работы с сертификатами APNs и ключами FCM. Мы подготавливаем provisioning profile для iOS и google-services.json для Android.
Рекомендации «Кого подписаться»
Простая эвристика — друзья друзей. SQL:
SELECT DISTINCT f2.followee_id FROM follows f1 JOIN follows f2 ON f1.followee_id = f2.follower_id WHERE f1.follower_id = :me AND f2.followee_id != :me AND NOT EXISTS (SELECT 1 FROM follows WHERE follower_id = :me AND followee_id = f2.followee_id) LIMIT 20; Для больших графов предрасчёт через воркер в Redis сокращает время ответа до 5 мс.
Как мы разрабатываем модуль подписок: этапы работы
| Этап | Длительность | Результат |
|---|---|---|
| Анализ | 1 день | Определение нагрузки и требований |
| Проектирование | 1 день | Схема БД, индексы, кэш |
| Реализация API | 1–2 дня | Эндпоинты follow/unfollow, список, рекомендации |
| Интеграция клиента | 1–2 дня | iOS (Swift/SwiftUI), Android (Kotlin/Jetpack Compose) |
| Тестирование | 1 день | Нагрузочное тестирование до 1 млн подписчиков |
| Деплой и мониторинг | 0.5 дня | Инструкция по развертыванию |
Что входит в работу
- Схема базы данных с индексами и денормализацией
- Серверные API-эндпоинты (REST или GraphQL)
- Клиентский код на iOS (Swift/SwiftUI) и Android (Kotlin/Jetpack Compose)
- Push-уведомления с батчингом
- Документация по API и интеграции
- Инструкция по деплою и мониторингу
Сроки
Базовая система (follow/unfollow, счётчики, список) — 1‑2 дня. С закрытыми аккаунтами, уведомлениями и рекомендациями — 3‑5 дней. Стоимость рассчитывается индивидуально. Получите консультацию — оценим ваш проект за один день. Свяжитесь с нами, чтобы обсудить детали и заказать разработку. Мы также помогаем с публикацией в App Store и Google Play, учитывая требования App Store Review Guidelines и Google Play Console.







