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

SQL: Найти первую и последнюю покупку каждого клиента

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

Условие

У вас есть таблица purchases:

  • purchase_id (integer)
  • customer_id (integer)
  • product_name (varchar)
  • purchase_date (timestamp)
  • amount (decimal)

Для каждого клиента найдите его первую и последнюю покупку.

Ожидаемый результат:

Таблица с колонками:

  • customer_id
  • first_purchase_date
  • first_purchase_product
  • last_purchase_date
  • last_purchase_product
  • total_purchases (общее количество покупок)

Источник: типовая задача на собеседованиях

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

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

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

Решение: Первая и последняя покупка клиента

SQL-запрос (базовый вариант)

SELECT
  customer_id,
  COUNT(*) as total_purchases,
  MIN(purchase_date) as first_purchase_date,
  MAX(purchase_date) as last_purchase_date
FROM purchases
GROUP BY customer_id
ORDER BY customer_id;

Это найдёт даты, но без информации о продуктах.

Вариант со страницей (с использованием оконных функций)

WITH purchases_ranked AS (
  -- Ранжируем покупки каждого клиента по дате
  SELECT
    purchase_id,
    customer_id,
    product_name,
    purchase_date,
    amount,
    ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY purchase_date ASC) as first_rn,
    ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY purchase_date DESC) as last_rn
  FROM purchases
),
first_purchase AS (
  SELECT
    customer_id,
    product_name as first_product,
    purchase_date as first_date
  FROM purchases_ranked
  WHERE first_rn = 1
),
last_purchase AS (
  SELECT
    customer_id,
    product_name as last_product,
    purchase_date as last_date
  FROM purchases_ranked
  WHERE last_rn = 1
)
SELECT
  p.customer_id,
  fp.first_date as first_purchase_date,
  fp.first_product as first_purchase_product,
  lp.last_date as last_purchase_date,
  lp.last_product as last_purchase_product,
  COUNT(DISTINCT p.purchase_id) as total_purchases
FROM purchases p
LEFT JOIN first_purchase fp ON p.customer_id = fp.customer_id
LEFT JOIN last_purchase lp ON p.customer_id = lp.customer_id
GROUP BY p.customer_id, fp.first_date, fp.first_product, lp.last_date, lp.last_product
ORDER BY p.customer_id;

Оптимизированный вариант (более простой)

WITH purchase_dates AS (
  SELECT
    customer_id,
    COUNT(*) as total_purchases,
    MIN(purchase_date) as first_purchase_date,
    MAX(purchase_date) as last_purchase_date
  FROM purchases
  GROUP BY customer_id
)
SELECT
  pd.customer_id,
  pd.first_purchase_date,
  fp.product_name as first_purchase_product,
  pd.last_purchase_date,
  lp.product_name as last_purchase_product,
  pd.total_purchases
FROM purchase_dates pd
LEFT JOIN purchases fp ON pd.customer_id = fp.customer_id 
  AND pd.first_purchase_date = fp.purchase_date
LEFT JOIN purchases lp ON pd.customer_id = lp.customer_id 
  AND pd.last_purchase_date = lp.purchase_date
-- Берём только первую строку каждого типа (в случае нескольких покупок в один день)
QUALIFY ROW_NUMBER() OVER (PARTITION BY pd.customer_id, pd.first_purchase_date ORDER BY fp.purchase_id) = 1
  OR fp.purchase_id IS NULL
AND ROW_NUMBER() OVER (PARTITION BY pd.customer_id, pd.last_purchase_date ORDER BY lp.purchase_id DESC) = 1
  OR lp.purchase_id IS NULL
ORDER BY pd.customer_id;

Самый простой вариант (с DISTINCT ON)

-- Для PostgreSQL
SELECT DISTINCT ON (customer_id)
  customer_id,
  purchase_date as first_purchase_date,
  product_name as first_purchase_product
FROM purchases
ORDER BY customer_id, purchase_date ASC;

-- Для последней покупки
SELECT DISTINCT ON (customer_id)
  customer_id,
  purchase_date as last_purchase_date,
  product_name as last_purchase_product
FROM purchases
ORDER BY customer_id, purchase_date DESC;

Полное решение (работает везде)

WITH first_last_dates AS (
  SELECT
    customer_id,
    COUNT(*) as total_purchases,
    MIN(purchase_date) as first_purchase_date,
    MAX(purchase_date) as last_purchase_date
  FROM purchases
  GROUP BY customer_id
),
first_purchases AS (
  SELECT
    p.customer_id,
    p.product_name as first_purchase_product,
    p.purchase_date as first_purchase_date,
    ROW_NUMBER() OVER (PARTITION BY p.customer_id ORDER BY p.purchase_id) as rn
  FROM purchases p
  JOIN first_last_dates fld ON p.customer_id = fld.customer_id 
    AND p.purchase_date = fld.first_purchase_date
),
last_purchases AS (
  SELECT
    p.customer_id,
    p.product_name as last_purchase_product,
    p.purchase_date as last_purchase_date,
    ROW_NUMBER() OVER (PARTITION BY p.customer_id ORDER BY p.purchase_id DESC) as rn
  FROM purchases p
  JOIN first_last_dates fld ON p.customer_id = fld.customer_id 
    AND p.purchase_date = fld.last_purchase_date
)
SELECT
  fld.customer_id,
  fp.first_purchase_date,
  fp.first_purchase_product,
  lp.last_purchase_date,
  lp.last_purchase_product,
  fld.total_purchases
FROM first_last_dates fld
LEFT JOIN first_purchases fp ON fld.customer_id = fp.customer_id AND fp.rn = 1
LEFT JOIN last_purchases lp ON fld.customer_id = lp.customer_id AND lp.rn = 1
ORDER BY fld.customer_id;

Пример вывода

customer_id | first_purchase_date | first_purchase_product | last_purchase_date | last_purchase_product | total_purchases
------------|---------------------|------------------------|--------------------|-----------------------|---
1           | 2024-01-15 10:30    | Laptop                 | 2024-03-20 15:45   | Mouse                 | 5
2           | 2024-02-01 08:00    | Phone                  | 2024-02-01 08:00   | Phone                 | 1
3           | 2024-01-10 12:00    | Keyboard               | 2024-03-25 18:30   | Monitor               | 8

Дополнительные метрики

WITH customer_summary AS (
  SELECT
    customer_id,
    COUNT(*) as total_purchases,
    COUNT(DISTINCT DATE(purchase_date)) as purchase_days,
    MIN(purchase_date) as first_purchase_date,
    MAX(purchase_date) as last_purchase_date,
    SUM(amount) as total_spent,
    AVG(amount) as avg_purchase_amount
  FROM purchases
  GROUP BY customer_id
)
SELECT
  customer_id,
  total_purchases,
  purchase_days,
  first_purchase_date,
  last_purchase_date,
  EXTRACT(DAY FROM (last_purchase_date - first_purchase_date)) as days_since_first_purchase,
  ROUND(total_spent, 2) as total_spent,
  ROUND(avg_purchase_amount, 2) as avg_purchase_amount
FROM customer_summary
ORDER BY total_spent DESC;

Это дополнительно покажет:

  • Сколько дней между первой и последней покупкой
  • Общую сумму, потраченную клиентом
  • Среднюю сумму одной покупки

Вариант с информацией о периоде активности

WITH customer_activity AS (
  SELECT
    customer_id,
    COUNT(*) as total_purchases,
    MIN(purchase_date) as first_purchase_date,
    MAX(purchase_date) as last_purchase_date,
    DATE_PART('day', MAX(purchase_date) - MIN(purchase_date)) as days_active,
    CASE
      WHEN MAX(purchase_date) >= CURRENT_DATE - INTERVAL '30 days' THEN 'Active'
      WHEN MAX(purchase_date) >= CURRENT_DATE - INTERVAL '90 days' THEN 'At Risk'
      ELSE 'Inactive'
    END as customer_status
  FROM purchases
  GROUP BY customer_id
)
SELECT
  customer_id,
  total_purchases,
  first_purchase_date,
  last_purchase_date,
  days_active,
  ROUND(total_purchases :: NUMERIC / GREATEST(days_active, 1), 2) as avg_purchases_per_day,
  customer_status
FROM customer_activity
ORDER BY last_purchase_date DESC;

Это поможет сегментировать клиентов по активности.

SQL: Найти первую и последнюю покупку каждого клиента | PrepBro