Оговорка про данные: в 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'.
При этом если подразумевалось "какие цены были в январе", то нижняя граница не нужна: у цен нет "срока годности" или даты деактивации, поэтому декабрьские действовали бы и в январе.
На реальной задаче я бы просто уточнил этот момент. Здесь оставил вариант, который, как мне показалось, ближе к формулировке задания.
Такой вариант я тоже рассматривал. Сначала получить максимальную дату:
SELECT sku_code, chain_id, MAX(last_price_datetime)
FROM sku
GROUP BY sku_code, chain_id;Потом присоединить исходную таблицу обратно.
Это нормальное решение, и на некоторых данных оно может оказаться даже быстрее. Но заранее этого не узнать. Здесь уже всё зависит от индексов, объёма данных и плана выполнения. Я бы просто сравнил оба варианта через EXPLAIN ANALYZE.
Ещё один минус этого паттерна: фильтры по периоду и сетям приходится держать синхронными в двух местах — в агрегирующем подзапросе и во внешней выборке. Если фильтр по дате остаётся только снаружи, подзапрос считает максимум за всё время, и товар молча выпадает из результата.
Скорее всего, окно с 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. Если дубли появились, лучше разобраться, почему они вообще возникли, а не скрывать это в запросе.
И точно не стал бы создавать отдельный индекс на каждую колонку. Для такого запроса намного полезнее один составной индекс, чем несколько одиночных.