Skip to content

Instantly share code, notes, and snippets.

@MIKEk8
Last active July 22, 2026 12:30
Show Gist options
  • Select an option

  • Save MIKEk8/c8e40e2b4666ec3ab2f1ea62b85eb4c4 to your computer and use it in GitHub Desktop.

Select an option

Save MIKEk8/c8e40e2b4666ec3ab2f1ea62b85eb4c4 to your computer and use it in GitHub Desktop.

Ответ на тестовое задание

Оговорка про данные: в sku нет суррогатного ключа, поэтому если у товара окажутся две записи с одинаковым last_price_datetime, однозначно определить актуальную невозможно — запрос вернёт одну строку, но какую именно, не гарантируется, и в разных прогонах это может быть то одна, то вторая цена. Ниже, в предложениях по структуре, предлагаю, как это закрыть.

WITH unique_barcodes AS (
    SELECT DISTINCT
        sku_code,
        barcode,
        chain_id
    FROM barcode
    WHERE chain_id IN (1, 2, 3)
),
last_prices AS (
    SELECT
        sku_code,
        chain_id,
        sku_name,
        last_price,
        ROW_NUMBER() OVER (
            PARTITION BY sku_code, chain_id
            ORDER BY last_price_datetime DESC
        ) AS rn
    FROM sku
    WHERE chain_id IN (1, 2, 3)
      AND last_price_datetime >= '2026-01-01'
      AND last_price_datetime <  '2026-02-01'
)
SELECT
    lp.sku_name,
    lp.sku_code,
    ub.barcode,
    lp.last_price
FROM last_prices lp
JOIN unique_barcodes ub
  ON ub.sku_code = lp.sku_code
 AND ub.chain_id = lp.chain_id
WHERE lp.rn = 1;

Несколько моментов, которые хочу пояснить

Во-первых, sku и barcode соединяю не только по sku_code, но и по chain_id. Один и тот же артикул вполне может существовать сразу в нескольких сетях, поэтому join только по артикулу легко даёт лишние строки. Если в данных гарантируется, что артикул глобально уникален между сетями, chain_id из условия соединения можно убрать — но по схеме такой гарантии не видно.

Во-вторых, последнюю цену получаю через ROW_NUMBER(). Вариант с MAX(last_price_datetime) тоже рабочий, но есть нюанс: если две записи имеют одинаковое время обновления, после обратного join можно получить две строки. С ROW_NUMBER() строка всегда одна — хотя какая именно из двух одинаковых по времени, без дополнительного критерия сортировки не гарантируется (та самая оговорка из начала).

Про период

Как я понял, нужна выборка за январь, поэтому использовал диапазон >= '2026-01-01' AND < '2026-02-01'. При этом если подразумевалось "какие цены были в январе", то нижняя граница не нужна: у цен нет "срока годности" или даты деактивации, поэтому декабрьские действовали бы и в январе. На реальной задаче я бы просто уточнил этот момент. Здесь оставил вариант, который, как мне показалось, ближе к формулировке задания.

Почему не MAX() + GROUP BY

Такой вариант я тоже рассматривал. Сначала получить максимальную дату:

SELECT sku_code, chain_id, MAX(last_price_datetime)
FROM sku
GROUP BY sku_code, chain_id;

Потом присоединить исходную таблицу обратно. Это нормальное решение, и на некоторых данных оно может оказаться даже быстрее. Но заранее этого не узнать. Здесь уже всё зависит от индексов, объёма данных и плана выполнения. Я бы просто сравнил оба варианта через EXPLAIN ANALYZE. Ещё один минус этого паттерна: фильтры по периоду и сетям приходится держать синхронными в двух местах — в агрегирующем подзапросе и во внешней выборке. Если фильтр по дате остаётся только снаружи, подзапрос считает максимум за всё время, и товар молча выпадает из результата.

Что станет самым дорогим на 500 млн строк?

Скорее всего, окно с ROW_NUMBER(). Чтобы его посчитать, базе придётся отсортировать большой объём данных. Если сортировка перестанет помещаться в память, начнут использоваться временные файлы на диске, и запрос заметно замедлится. Следом идёт DISTINCT по таблице штрихкодов и join двух больших наборов данных. Если подходящих индексов нет, то узким местом вообще станет чтение таблиц.

Что бы я изменил

Первое — индекс на sku по (chain_id, sku_code, last_price_datetime DESC). Он помогает и фильтрации, и поиску последних записей. Если история цен только пополняется и старые записи не меняются, я бы ещё добавил id и использовал его как дополнительный критерий сортировки:

ORDER BY last_price_datetime DESC, id DESC

Это закрывает ту самую неоднозначность, о которой я говорил в начале: две записи с одинаковым временем изменения перестают быть неразличимыми. Если при этом порядок вставки всегда совпадает с порядком изменений, можно вообще ориентироваться на id. На barcode я бы добавил уникальное ограничение по (chain_id, sku_code, barcode). Если одинаковый штрихкод для одного товара в одной сети невозможен, лучше не допускать такие записи при сохранении. Тогда DISTINCT из запроса можно убрать. Если история быстро растёт, имеет смысл подумать о партиционировании по дате. Тогда запросы за конкретный период будут читать только нужные партиции, а не всю таблицу. Если же "последняя цена" нужна постоянно, я бы вообще не считал её каждый раз. Проще поддерживать небольшую таблицу с актуальными ценами и читать уже её.

Если запрос выполняется тысячи раз в час

В этот момент проблема уже не в SQL. Нет большого смысла заново вычислять один и тот же результат тысячи раз. Гораздо дешевле один раз подготовить его и отдавать из витрины данных, материализованного представления или кэша. История цен меняется намного реже, чем её читают. Если запись и чтение сильно нагружены одновременно, тяжёлые запросы можно вынести на реплику.

Что бы я не стал делать

Не стал бы использовать коррелированный подзапрос для поиска максимальной даты — на больших объёмах он масштабируется плохо. Не стал бы закрывать проблему дублей DISTINCT после join. Если дубли появились, лучше разобраться, почему они вообще возникли, а не скрывать это в запросе. И точно не стал бы создавать отдельный индекс на каждую колонку. Для такого запроса намного полезнее один составной индекс, чем несколько одиночных.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment