Контекст: В backend/internal/services/places.go:112-136 метод GetByID делает 3 отдельных запроса к БД вместо одного JOIN. При массовом вызове (например, список избранного) это даёт N*3 запросов.
// services/places.go:112-136 — ❌ N+1
func (s *PlaceService) GetByID(ctx context.Context, id string, fetchTags bool) (*models.Place, error) {
place, err := s.placeRepo.GetByID(ctx, id) // 1-й запрос
if place == nil { return nil, nil }
if fetchTags {
tags, err := s.placeRepo.GetTags(ctx, id) // 2-й запрос
features, err := s.placeRepo.GetFeatures(ctx, id) // 3-й запрос
place.Tags = tags
place.Features = features
}
return place, nil
}
Для одного места — приемлемо (3 round-trips). Но если вызвать GetByID в цикле для 10 мест — уже 30 запросов.
GetTags и GetFeatures — отдельные методы репозитория, переиспользуемые в List с batchGetByID вызывается только при запросе конкретного места, не в спискахGET /places/{id} для каждого местаGetByID добавятся теги услуг, отзывов, бронирований — 3 превратится в 6+// ✅ один запрос с JOIN
func (s *PlaceService) GetByID(ctx context.Context, id string, fetchTags bool) (*models.Place, error) {
place, err := s.placeRepo.GetByIDWithDetails(ctx, id) // JOIN: place + tags + features
if place == nil { return nil, nil }
return place, nil
}
// repository/places.go
func (r *PlaceRepo) GetByIDWithDetails(ctx context.Context, id string) (*models.Place, error) {
query := `
SELECT p.*,
COALESCE(json_agg(DISTINCT jsonb_build_object('id', t.id, 'name', t.name))
FILTER (WHERE t.id IS NOT NULL), '[]') as tags,
COALESCE(json_agg(DISTINCT jsonb_build_object('id', f.id, 'name', f.name))
FILTER (WHERE f.id IS NOT NULL), '[]') as features
FROM places p
LEFT JOIN place_tags pt ON pt.place_id = p.id
LEFT JOIN tags t ON t.id = pt.tag_id
LEFT JOIN place_features pf ON pf.place_id = p.id
LEFT JOIN features f ON f.id = pf.feature_id
WHERE p.id = $1
GROUP BY p.id
`
// ...
}
| Подход | Плюсы | Минусы |
|---|---|---|
| JOIN + json_agg | 1 запрос, атомарно | Сложный SQL, GROUP BY может быть медленным |
| Три параллельных запроса (goroutines) | 1 round-trip по времени | Сложнее, нужно управлять goroutines |
| Оставить как есть (3 последовательных) | Просто | N+1 при массовом вызове |
Было: 2 отдельных запроса (service + GetTags)
Стало: LEFT JOIN с json_agg + FILTER — 1 запрос
// repository/services.go — GetByIDWithTags
COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name, 'category', t.category))
FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tags
Файл: repository/services.go:50-82
Code review PhotoPlaces 2026-06 (places) и 2026-07-01 (services).
#performance #database #golang #N+1 #anti-pattern