← Назад к вопросам

Как реализовать логику подсчета количества уникальных пользователей?

2.0 Middle🔥 161 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

Как реализовать логику подсчета количества уникальных пользователей

Подсчёт уникальных пользователей - важная задача для аналитики. Есть несколько подходов с разными компромиссами между точностью и производительностью.

1. Простой способ - Set в памяти

@Service
public class UserCountingService {
    
    private Set<Long> uniqueUserIds = Collections.synchronizedSet(new HashSet<>());
    
    public void trackUser(Long userId) {
        uniqueUserIds.add(userId);
    }
    
    public long getUniqueUserCount() {
        return uniqueUserIds.size();
    }
}

Проблемы: теряется при перезагрузке, потребление памяти.

2. БД подход - COUNT(DISTINCT)

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT COUNT(DISTINCT u.id) FROM User u WHERE u.createdAt >= :startDate")
    long countUniqueUsersSince(@Param("startDate") LocalDateTime startDate);
    
    // По периоду
    @Query("SELECT COUNT(DISTINCT u.id) FROM User u " +
           "WHERE u.createdAt BETWEEN :start AND :end")
    long countUniqueUsersBetween(
        @Param("start") LocalDateTime start,
        @Param("end") LocalDateTime end
    );
}

@Service
public class UserAnalyticsService {
    
    @Autowired
    private UserRepository userRepository;
    
    public void printDailyActiveUsers() {
        LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
        LocalDateTime endOfDay = startOfDay.plusDays(1);
        
        long count = userRepository.countUniqueUsersBetween(startOfDay, endOfDay);
        System.out.println("Daily unique users: " + count);
    }
}

3. Таблица событий с индексом

// Отслеживание каждого события пользователя
@Entity
@Table(name = "user_events", indexes = {
    @Index(name = "idx_user_date", columnList = "user_id, event_date")
})
public class UserEvent {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long userId;
    private LocalDate eventDate;
    private String eventType;  // login, purchase, view
    
    // getters/setters
}

@Repository
public interface UserEventRepository extends JpaRepository<UserEvent, Long> {
    
    @Query("SELECT COUNT(DISTINCT ue.userId) FROM UserEvent ue " +
           "WHERE ue.eventDate = :date AND ue.eventType = :type")
    long countUniqueUsersByTypeOnDate(
        @Param("date") LocalDate date,
        @Param("type") String eventType
    );
}

4. HyperLogLog - приблизительный подсчёт

Для очень больших объёмов данных без точного результата:

@Service
public class ApproximateCountingService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public void trackUserViaPFAdd(Long userId) {
        // Добавить в HyperLogLog
        redisTemplate.opsForHyperLogLog()
            .add("daily_users:" + LocalDate.now(), userId.toString());
    }
    
    public long getApproximateUniqueCount() {
        // Получить приблизительное количество
        return redisTemplate.opsForHyperLogLog()
            .size("daily_users:" + LocalDate.now());
    }
}

Преимущества HyperLogLog:

  • Очень мало памяти (100 байт для 1M элементов)
  • O(1) операции
  • Точность ~2%

5. Redis Set для сессионных данных

@Service
public class SessionCountingService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public void registerUserSession(Long userId) {
        String key = "online_users:" + LocalDate.now();
        // Добавить в set
        redisTemplate.opsForSet().add(key, userId.toString());
        // Удалить соединение через 1 час
        redisTemplate.expire(key, 1, TimeUnit.HOURS);
    }
    
    public long getOnlineUserCount() {
        String key = "online_users:" + LocalDate.now();
        return redisTemplate.opsForSet().size(key);
    }
}

6. Bitmap (битовая карта)

Для ID пользователей в диапазоне 0-1M:

@Service
public class BitmapCountingService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public void markUserActive(Long userId) {
        String key = "active_users:" + LocalDate.now();
        // Set бит в позиции userId
        redisTemplate.opsForValue().setBit(key, userId, true);
    }
    
    public long countActiveUsers() {
        String key = "active_users:" + LocalDate.now();
        // Получить количество установленных бит
        return redisTemplate.opsForValue().getBit(key, 0L) != null ? 1 : 0;
        // Реальная реализация сложнее - использовать BITCOUNT
    }
}

7. Batch обработка - эффективнее

@Service
public class BatchUserCountingService {
    
    @Autowired
    private UserEventRepository userEventRepository;
    
    @Scheduled(cron = "0 0 * * * *")  // Каждый час
    public void calculateDailyUniques() {
        LocalDate today = LocalDate.now();
        
        // Получить максимум один раз в день
        long uniqueCount = userEventRepository.findDistinctUserIdsByDate(today)
            .stream()
            .distinct()
            .count();
        
        // Сохранить результат
        DailyStats stats = new DailyStats();
        stats.setDate(today);
        stats.setUniqueUserCount(uniqueCount);
        dailyStatsRepository.save(stats);
    }
}

8. Materialized View для больших данных

-- PostgreSQL materialized view для быстрого подсчёта
CREATE MATERIALIZED VIEW daily_unique_users AS
SELECT 
    DATE(created_at) as date,
    COUNT(DISTINCT user_id) as unique_users
FROM user_events
GROUP BY DATE(created_at)
ORDER BY date DESC;

-- Индекс на дату для быстрого поиска
CREATE INDEX idx_daily_unique_users_date ON daily_unique_users(date);

-- Обновлять периодически
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_unique_users;

9. Java реализация для большого набора

@Service
public class ScalableUserCountingService {
    
    @Autowired
    private UserEventRepository userEventRepository;
    
    public long countUniqueUsersEfficiently(LocalDate date) {
        // Использовать stream для больших данных
        return userEventRepository.findByEventDate(date)
            .stream()
            .map(UserEvent::getUserId)
            .distinct()
            .count();
    }
    
    // Для ОЧЕНЬ больших наборов
    public long countUniqueUsersInChunks(LocalDate date, int chunkSize) {
        Set<Long> uniqueIds = ConcurrentHashMap.newKeySet();
        
        int offset = 0;
        boolean hasMore = true;
        
        while (hasMore) {
            List<UserEvent> chunk = userEventRepository
                .findByEventDate(date, PageRequest.of(offset / chunkSize, chunkSize))
                .getContent();
            
            if (chunk.isEmpty()) {
                hasMore = false;
            } else {
                chunk.stream()
                    .map(UserEvent::getUserId)
                    .forEach(uniqueIds::add);
                offset += chunkSize;
            }
        }
        
        return uniqueIds.size();
    }
}

10. Сравнение методов

МетодПамятьСкоростьТочностьЛучше для
Set в памятиВысокаяO(1)100%Маленькие наборы
COUNT DISTINCTСредняяO(n)100%Запросы к БД
HyperLogLogНизкаяO(1)99%Очень большие наборы
Redis SetСредняяO(1)100%Сессионные данные
BitmapНизкаяO(1)100%Компактные ID
Materialized ViewСредняяO(1)*100%Сложные аналитики

Best Practices

  1. Для точности - используй COUNT DISTINCT или Set
  2. Для масштабируемости - HyperLogLog или bitmap
  3. Для реального времени - Redis
  4. Для аналитики - materialized views
  5. Всегда кэшируй результаты - пересчитывай только когда нужно
  6. Индексируй user_id - важно для COUNT DISTINCT
  7. Батчируй операции - обновляй раз в час/день

Кратко: Используй COUNT DISTINCT для точности на небольших наборах, HyperLogLog для приблизительного подсчёта на больших наборах, Redis для реального времени.

Как реализовать логику подсчета количества уникальных пользователей? | PrepBro