Получение актуальной (последней) цены на товар — одна из самых частых задач при работе с базами данных‚ будь то 1С:Предприятие‚ PostgreSQL‚ MS SQL Server или MySQL․ В этой статье мы подробно разберем оптимальные способы решения этой задачи одним запросом‚ рассмотрим особенности виртуальных таблиц 1С и стандартные SQL-конструкции (оконные функции‚ подзапросы‚ TOP/LIMIT)․
Содержание
- Подход в 1С:Предприятие — Виртуальная таблица «СрезПоследних»
- Стандартные SQL-методы (PostgreSQL‚ MS SQL‚ MySQL‚ Oracle)
- Оконные функции (ROW_NUMBER) — современный стандарт
- Подзапрос с агрегацией (MAX даты)
- TOP 1 / LIMIT 1 / FETCH FIRST — для единичных проверок
- CROSS APPLY / LATERAL JOIN, для присоединения к списку товаров
- Разбор типичных сценариев из практики
- Сценарий: Таблица цен с 25 млн записей (JOIN с таблицей периодов)
- Сценарий: Получение последней цены за каждый час/день
- Сценарий: Пустой регистр/таблица (обработка NULL)
- Сравнение производительности и лучшие практики
Подход в 1С:Предприятие — Виртуальная таблица «СрезПоследних»
В платформе 1С:Предприятие 8 для регистров сведений (например‚ ЦеныНоменклатуры) существует мощный механизм — виртуальная таблица СрезПоследних․ Она позволяет получить актуальные значения реквизитов на заданный момент времени без написания сложных подзапросов․
Синтаксис
ВЫБРАТЬ
ЦеныНоменклатурыСрезПоследних․Номенклатура КАК Номенклатура‚
ЦеныНоменклатурыСрезПоследних․Цена КАК Цена‚
ЦеныНоменклатурыСрезПоследних․ВидЦены КАК ВидЦены
ИЗ
РегистрСведений․ЦеныНоменклатуры․СрезПоследних(&ДатаСреза‚ Номенклатура В (&СписокНоменклатуры) И ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних Ключевые параметры
- Период (Дата среза) — обязательный параметр (обычно передается
&ДатаСрезаилиКОНЕЦПЕРИОДА(&ДатаСреза‚ ДЕНЬ))․ Срез берется на конец этой секунды․ - Отбор (Condition) — необязательный‚ но критически важен для производительности․ Позволяет отфильтровать данные до чтения с диска (по Номенклатуре‚ Виду цен‚ Характеристикам и т․д․)․
Пример с параметрами из вашего набора данных
Если у нас три параметра: Дата‚ Номенклатура‚ ТипЦен:
ВЫБРАТЬ
ЦеныНоменклатурыСрезПоследних․Номенклатура‚
ЦеныНоменклатурыСрезПоследних․Цена
ИЗ
РегистрСведений․ЦеныНоменклатуры․СрезПоследних(&Дата‚ Номенклатура = &Номенклатура И ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних Важно: Всегда указывайте отбор по измерениям (Номенклатура‚ ТипЦен) во втором параметре
СрезПоследних․ Это позволяет платформе использовать индексы регистра эффективно‚ не сканируя всю таблицу․
Стандартные SQL-методы (Для внешних БД и прямых SQL-запросов)
Если вы работаете напрямую с СУБД (PostgreSQL‚ SQL Server‚ MySQL‚ Oracle) или используете прямые SQL-запросы в 1С (через COMConnector или внешние отчеты)‚ подходы отличаются․
Оконные функции: ROW_NUMBER — Золотой стандарт (SQL:2003+)
Это самый производительный и читаемый способ для получения последней цены для списка товаров сразу․
WITH RankedPrices AS (
SELECT
p․ProductID‚
p․Price‚
p․PriceDate‚
ROW_NUMBER OVER (PARTITION BY p․ProductID ORDER BY p․PriceDate DESC‚ p․ID DESC) as rn
FROM tb_Price p
-- WHERE p․PriceDate <= @TargetDate -- опционально: срез на дату
)
SELECT ProductID‚ Price‚ PriceDate
FROM RankedPrices
WHERE rn = 1; - PARTITION BY ProductID — разбивает данные на группы по товарам․
- ORDER BY PriceDate DESC‚ ID DESC, сортирует внутри группы: сначала по дате (новые выше)‚ затем по ID (на случай одинаковых дат)․
- WHERE rn = 1 — оставляет только самую последнюю запись для каждого товара․
Подзапрос с MAX(Дата) — Классика
Работает везде‚ но на больших таблицах может быть медленнее оконных функций из-за двойного сканирования или неэффективных планов выполнения․
SELECT p․ProductID‚ p․Price‚ p․PriceDate
FROM tb_Price p
INNER JOIN (
SELECT ProductID‚ MAX(PriceDate) as MaxDate
FROM tb_Price
GROUP BY ProductID
) latest ON p․ProductID = latest․ProductID AND p․PriceDate = latest․MaxDate
-- Важно: если даты дублируются‚ вернется несколько строк․ Нужен доп․ отбор по MAX(ID)
-- AND p․ID = (SELECT MAX(ID) FROM tb_Price WHERE ProductID = p․ProductID AND PriceDate = p․PriceDate); TOP 1 / LIMIT 1 / FETCH FIRST — Для одного товара
Идеально‚ если нужно узнать цену конкретного товара (параметризованный запрос)․
- MS SQL / Access:
SELECT TOP 1 Price FROM tb_Price WHERE ProductID = @ID ORDER BY PriceDate DESC‚ ID DESC; - PostgreSQL / MySQL / SQLite:
SELECT Price FROM tb_Price WHERE ProductID = @ID ORDER BY PriceDate DESC‚ ID DESC LIMIT 1; - Oracle 12c+ / Standard SQL:
SELECT Price FROM tb_Price WHERE ProductID = @ID ORDER BY PriceDate DESC‚ ID DESC FETCH FIRST 1 ROW ONLY;
CROSS APPLY (SQL Server) / LATERAL JOIN (PostgreSQL), JOIN к списку товаров
Когда у вас есть список товаров (например‚ из корзины или заказа) и нужно подтянуть к каждому последнюю цену — это лучший синтаксис․
-- SQL Server (CROSS APPLY)
SELECT t․ProductID‚ ca․Price‚ ca․PriceDate
FROM tb_Products t
CROSS APPLY (
SELECT TOP 1 Price‚ PriceDate
FROM tb_Price p
WHERE p․ProductID = t․ProductID
ORDER BY p․PriceDate DESC‚ p․ID DESC
) ca;
-- PostgreSQL (LATERAL JOIN)
SELECT t․ProductID‚ l․Price‚ l․PriceDate
FROM tb_Products t
LEFT JOIN LATERAL (
SELECT Price‚ PriceDate
FROM tb_Price p
WHERE p․ProductID = t․ProductID
ORDER BY p․PriceDate DESC‚ p․ID DESC
LIMIT 1
) l ON true; Разбор типичных сценариев из практики
Сценарий: Большая таблица цен (25 млн записей) + JOIN с таблицей периодов
Условие: Таблица tb․Price (25 млн строк)‚ поля: ID_Brewery‚ ProductCode‚ Price‚ PeriodID․ Таблица tb․Period: PeriodID‚ DatePeriod․ Нужно выбрать товары по последней дате с ценой․
Оптимальное решение (Оконная функция):
WITH LastPeriod AS (
-- 1․ Находим ID последнего периода (один раз)
SELECT MAX(PeriodID) as LastPeriodID FROM tb_Period
)‚
Ranked AS (
SELECT
p․ID_Brewery‚
p․ProductCode‚
p․Price‚
per․DatePeriod‚
ROW_NUMBER OVER (PARTITION BY p․ID_Brewery‚ p․ProductCode ORDER BY p․PeriodID DESC) as rn
FROM tb_Price p
INNER JOIN tb_Period per ON p․PeriodID = per․PeriodID
-- Оптимизация: сразу отсекаем старые периоды‚ если их много
WHERE p․PeriodID = (SELECT LastPeriodID FROM LastPeriod)
)
SELECT ID_Brewery‚ ProductCode‚ Price‚ DatePeriod
FROM Ranked
WHERE rn = 1; Почему так: Мы не делаем `JOIN` всей таблицы цен с периодами‚ а сразу фильтруем по `PeriodID = MAX(PeriodID)`․ Если нужен срез на конкретную дату‚ используйте `WHERE per․DatePeriod <= @TargetDate` и `ORDER BY per․DatePeriod DESC`․
Сценарий: Последняя цена за каждый час/день (Тайм-серии)
Задача: Получить по одной строке в час (последнюю цену за час) для конкретного полета/товара․
SELECT
DATE_TRUNC('hour'‚ PriceDateTime) as HourSlot‚ -- или DATEADD(hour‚ DATEDIFF(hour‚ 0‚ PriceDateTime)‚ 0) в MS SQL
(ARRAY_AGG(Price ORDER BY PriceDateTime DESC))[1] as LastPriceInHour -- PostgreSQL specific
-- Или стандартный способ через ROW_NUMBER:
FROM (
SELECT
Price‚
PriceDateTime‚
ROW_NUMBER OVER (PARTITION BY DATE_TRUNC('hour'‚ PriceDateTime) ORDER BY PriceDateTime DESC) as rn
FROM tb_Price
WHERE ProductID = 'AB12' AND PriceDateTime >= @StartDate AND PriceDateTime < @EndDate
) t
WHERE rn = 1
ORDER BY HourSlot; Сценарий: Обработка пустых таблиц/регистров (NULL)
Как отмечено в исходных данных: если в регистре нет записей‚ СрезПоследних в 1С вернет пустую выборку (не NULL)․ В SQL агрегатные функции (MAX) вернут NULL․
1С (Запрос): Результат просто пуст․ При получении значения в коде: `Цена = Результат․Цена ?? 0;`․
SQL (COALESCE):
SELECT COALESCE((
SELECT TOP 1 Price FROM tb_Price WHERE ProductID = @ID ORDER BY PriceDate DESC
)‚ 0) as Price; Сравнение производительности и лучшие практики
| Метод | СУБД / Платформа | Сложность | Производительность на больших данных | Когда использовать |
|---|---|---|---|---|
| СрезПоследних | 1С:Предприятие 8 | Низкая (1 строка) | Максимальная (использует индексы регистра нативно) | Всегда в 1С для регистров сведений․ |
| ROW_NUMBER OVER | PG‚ MS SQL‚ Oracle‚ MySQL 8+ | Средняя (CTE) | Высокая (один проход‚ параллелизм) | Стандарт для выборки "последняя N записей на группу"․ |
| CROSS APPLY / LATERAL | MS SQL / PG | Средняя | Высокая (Index Seek на каждую строку внешней таблицы) | Присоединение цены к списку товаров (Order Lines‚ Cart)․ |
| TOP 1 / LIMIT 1 | Все | Низкая | Высокая (для 1 товара) | Точечный запрос цены одного SKU․ |
| Подзапрос MAX(Date) + JOIN | Все (Legacy) | Средняя | Низкая/Средняя (2 сканирования‚ проблемы с дублями дат) | Старые СУБД без поддержки оконных функций․ |
Ключевые советы по оптимизации:
- Индексы — это закон․ Для таблицы цен идеальный покрывающий индекс:
- 1С: Автоматически по измерениям + Период․
- SQL:
CREATE INDEX IX_Price_Lookup ON tb_Price (ProductID‚ PriceDate DESC) INCLUDE (Price);
- Избегайте функций над колонками в WHERE․ Плохо: `WHERE YEAR(PriceDate) = 2026`․ Хорошо: `WHERE PriceDate >= '2026-01-01' AND PriceDate < '2027-01-01'`․
- Типизация параметров․ Всегда передавайте параметры с правильным типом данных (DateTime‚ GUID‚ String)‚ чтобы СУБД не делала неявное преобразование (Implicit Conversion)‚ сбивающее индексы․
- Параллелизм․ Оконные функции и `APPLY`/`LATERAL` отлично параллелятся современными оптимизаторами․
Получение последней цены «одним запросом» — задача решаемая элегантно и эффективно при выборе правильного инструмента:
- В 1С:Предприятие, безусловный лидер:
РегистрСведений․Имя․СрезПоследних(Дата‚ Отбор)․ Используйте отбор по измерениям для скорости․ - В стандартном SQL — используйте Оконные функции (
ROW_NUMBER) для выборок по группам товаров иCROSS APPLY / LATERAL JOINдля присоединения к документам/корзинам․ - Для точечных запросов —
TOP 1 ․․․ ORDER BY Date DESCс правильным индексом мгновенно вернет результат даже на сотнях миллионов строк․
Помните‚ что «один запрос» не всегда означает «один SELECT без CTE»․ Использование WITH (CTE) делает код читаемым‚ а оптимизатор — умнее․ Проектируйте индексы под ваши паттерны чтения‚ и проблема «последней цены» перестанет быть узким местом вашей системы․