У меня есть шаблон запроса, который должен быть очень распространенным, но я не знаю, как написать эффективный запрос для него. Я хочу посмотреть строки таблицы, которые соответствуют «самой последней дате, а не после» строк другой таблицы.
У меня есть таблица, inventory
скажем, которая представляет инвентарь, который я держу в определенный день.
date | good | quantity
------------------------------
2013-08-09 | egg | 5
2013-08-09 | pear | 7
2013-08-02 | egg | 1
2013-08-02 | pear | 2
и таблица, скажем, «цена», которая содержит цену товара в данный день
date | good | price
--------------------------
2013-08-07 | egg | 120
2013-08-06 | pear | 200
2013-08-01 | egg | 110
2013-07-30 | pear | 220
Как я могу эффективно получить «самую последнюю» цену для каждой строки таблицы инвентаря, т.е.
date | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07 | egg | 5 | 120
2013-08-09 | 2013-08-06 | pear | 7 | 200
2013-08-02 | 2013-08-01 | egg | 1 | 110
2013-08-02 | 2013-07-30 | pear | 2 | 220
Я знаю один способ сделать это:
select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good
и затем присоедините этот запрос снова к инвентарю. Для больших таблиц даже выполнение первого запроса (без повторного присоединения к инвентарю) выполняется очень медленно. Тем не менее, та же проблема быстро решается, если я просто использую свой язык программирования для выдачи одного max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1
запроса на каждый date_of_interest
из таблицы инвентаризации, поэтому я знаю, что нет никаких вычислительных затруднений. Однако я предпочел бы решить всю проблему с помощью одного запроса SQL, поскольку это позволило бы мне выполнить дальнейшую обработку SQL по результату запроса.
Есть ли стандартный способ сделать это эффективно? Такое чувство, что оно должно появляться часто и что должен быть способ написать быстрый запрос.
Я использую Postgres, но был бы признателен общий ответ на SQL.
\d tbl
в psql), ваша версия Postgres и мин. / Макс. количество цен за товар.Ответы:
Это очень зависит от обстоятельств и точных требований. Рассмотрим мой комментарий к вопросу .
Простое решение
С
DISTINCT ON
в Postgres:Заказанный результат.
Или
NOT EXISTS
в стандартном SQL (работает с каждой знакомой мне СУБД):Тот же результат, но с произвольным порядком сортировки - если только вы не добавите
ORDER BY
.В зависимости от распределения данных, точных требований и показателей любой из них может быть быстрее.
Как правило,
DISTINCT ON
это победитель, и вы получаете отсортированный результат поверх него. Но в некоторых случаях другие методы запросов (намного) еще быстрее. Смотри ниже.Решения с подзапросами для вычисления максимальных / минимальных значений обычно медленнее. Варианты с CTE, как правило, медленнее, но все же.
Простые представления (например, предложенные в другом ответе) совсем не помогают производительности в Postgres.
SQL Fiddle.
Правильное решение
Строки и сопоставление
Прежде всего, вы страдаете от неоптимального расположения таблицы. Это может показаться тривиальным, но нормализация вашей схемы может иметь большое значение.
Сортировка по типам символов (
text
,varchar
, ...) должно быть сделано в соответствии с локалью - в COLLATION в частности. Скорее всего, ваша БД использует некоторый локальный набор правил (например, в моем случае:)de_AT.UTF-8
. Узнайте с помощью:Это замедляет сортировку и поиск по индексу . Чем дольше ваши строки (названия товаров), тем хуже. Если вы на самом деле не заботитесь о правилах сортировки в выходных данных (или о порядке сортировки вообще), это может быть быстрее, если вы добавите
COLLATE "C"
:Обратите внимание, как я добавил сопоставление в двух местах.
В два раза быстрее в моем тесте с 20 тыс. Строк в каждой и очень простыми именами ('good123').
Индекс
Если ваш запрос должен использовать индекс, столбцы с символьными данными должны использовать сопоставление (
good
в примере):Обязательно прочитайте последние две главы этого связанного ответа на SO:
Вы можете даже иметь несколько индексов с разными параметрами сортировки в одних и тех же столбцах - если вам также нужны товары, отсортированные согласно другому (или стандартному) параметру сортировки в других запросах.
Нормализовать
Избыточные строки (имя хорошо) также раздувают ваши таблицы и индексы, что делает все еще медленнее. При правильном расположении таблицы вы можете избежать большинства проблем с самого начала. Может выглядеть так:
Первичные ключи автоматически предоставляют (почти) все нужные нам индексы.
В зависимости от отсутствующих деталей, многоколоночный индекс в
price
порядке убывания во втором столбце может повысить производительность:Опять же, сопоставление должно соответствовать вашему запросу (см. Выше).
В Postgres 9.2 или более поздних версиях «индексы покрытия» для сканирования только по индексу могут помочь еще больше - особенно если в ваших таблицах содержатся дополнительные столбцы, что делает таблицу значительно больше, чем индекс покрытия.
Эти результирующие запросы выполняются намного быстрее:
НЕ СУЩЕСТВУЕТ
ОТЛИЧАЕТСЯ НА
SQL Fiddle.
Более быстрые решения
Если это все еще не достаточно быстро, могут быть более быстрые решения.
Рекурсивный CTE /
JOIN LATERAL
/ коррелированный подзапросСпециально для распространения данных со многими ценами за товар :
Материализованный вид
Если вам нужно выполнить это часто и быстро, я предлагаю вам создать материализованное представление. Я думаю, можно с уверенностью предположить, что цены и запасы за прошедшие даты редко меняются. Вычислите результат один раз и сохраните снимок как материализованное представление.
Postgres 9.3+ имеет автоматическую поддержку материализованных представлений. Вы можете легко реализовать базовую версию в более старых версиях.
источник
price_good_date_desc_idx
Индекс вы рекомендуете значительно улучшили производительность аналогичного запроса шахты. Мой план запроса изменился от стоимости42374.01..42374.86
до0.00..37.12
!К вашему сведению, я использовал mssql 2008, поэтому у Postgres не будет индекса «включить». Однако использование базовой индексации, показанной ниже, изменится с хеш-соединений на слияния в Postgres: http://explain.depesz.com/s/eF6 (без индекса) http://explain.depesz.com/s/j9x ( с индексом по критериям объединения)
Я предлагаю разбить ваш запрос на две части. Во-первых, представление (не предназначенное для повышения производительности), которое можно использовать в различных других контекстах, представляющих взаимосвязь дат инвентаризации и дат ценообразования.
Тогда ваш запрос может стать более простым и легким для манипулирования другими видами, если запрос (например, использование левых соединений для поиска ресурсов без последних ценовых дат):
Это дает следующий план выполнения: http://sqlfiddle.com/#!3/24f23/1
... Все сканы с полной сортировкой. Обратите внимание, что затраты производительности на хеш-совпадения занимают большую часть общей стоимости ... и мы знаем, что сканирование и сортировка таблицы выполняются медленно (по сравнению с целью: поиск индекса).
Теперь добавьте базовые индексы, чтобы помочь критериям, используемым в вашем объединении (я не утверждаю, что это оптимальные индексы, но они иллюстрируют суть): http://sqlfiddle.com/#!3/5ec75/1
Это показывает улучшение. Операции с вложенным циклом (внутренним объединением) больше не требуют значительных общих затрат для запроса. Остальные затраты теперь распределяются между поисками индекса (сканирование инвентаря, потому что мы тянем каждую строку инвентаря). Но мы можем сделать еще лучше, потому что запрос тянет количество и цену. Чтобы получить эти данные, после оценки критерия соединения необходимо выполнить поиск.
Последняя итерация использует «include» в индексах, чтобы план мог легко скользить и получать дополнительно запрашиваемые данные прямо из самого индекса. Итак, поиск пропал: http://sqlfiddle.com/#!3/5f143/1
Теперь у нас есть план запроса, в котором общая стоимость запроса равномерно распределяется между очень быстрыми операциями поиска по индексу. Это будет близко к тому, как хорошо. Конечно, другие эксперты могут улучшить это дальше, но решение устраняет пару основных проблем:
источник
Если у вас есть PostgreSQL 9.3 (выпущен сегодня), то вы можете использовать LATERAL JOIN.
У меня нет способа проверить это, и я никогда не использовал его раньше, но из того, что я могу сказать из документации, синтаксис будет выглядеть примерно так:
Это в основном эквивалентно приложению SQL-Server APPLY , и для демонстрационных целей есть работающий пример этого на SQL-Fiddle .
источник
Как отмечали Эрвин и другие, эффективный запрос зависит от множества переменных, и PostgreSQL очень старается оптимизировать выполнение запроса на основе этих переменных. Как правило, сначала вы хотите написать для ясности, а затем изменить производительность после выявления узких мест.
Кроме того, в PostgreSQL есть много хитростей, которые вы можете использовать, чтобы сделать вещи немного более эффективными (частичные индексы для одного), поэтому, в зависимости от нагрузки чтения / записи, вы сможете оптимизировать это очень далеко за счет тщательного индексирования.
Первое, что нужно попробовать, это просто сделать вид и присоединиться к нему:
Это должно хорошо работать при выполнении чего-то вроде:
Тогда вы можете присоединиться к этому. Запрос в конечном итоге объединит представление с базовой таблицей, но при условии, что у вас есть уникальный индекс (дата, хорошая в этом порядке ), вам нужно идти (так как это будет простой поиск в кэше). Это будет очень хорошо работать с несколькими просматриваемыми строками, но будет очень неэффективно, если вы пытаетесь переварить миллионы цен на товары.
Второе, что вы можете сделать, это добавить в таблицу инвентаризации столбец most_recent bool и
Затем вы захотите использовать триггеры, чтобы установить значение Most_recent равным false, когда была вставлена новая строка для товара. Это добавляет больше сложности и больше шансов для ошибок, но это полезно.
Опять же, многое зависит от наличия соответствующих индексов. Для самых последних запросов к дате у вас, вероятно, должен быть индекс даты, и, возможно, многостолбцовый, начиная с даты и включающий критерии объединения.
Обновите комментарий Эрвина ниже, похоже, я неправильно это понял. Перечитывая вопрос, я совсем не уверен, что задают. Я хочу упомянуть в обновлении, что является потенциальной проблемой, которую я вижу, и почему это оставляет неясным.
Предложенный дизайн базы данных не имеет реального использования IME с ERP и системами учета. Это будет работать в гипотетической идеальной модели ценообразования, где все, что продается в данный день данного продукта, имеет одинаковую цену. Тем не менее, это не всегда так. Это даже не относится к таким вещам, как обмен валюты (хотя некоторые модели делают вид, что это так). Если это надуманный пример, неясно. Если это реальный пример, есть большие проблемы с дизайном на уровне данных. Я собираюсь предположить здесь, что это реальный пример.
Вы не можете предполагать, что одна дата указывает цену на данный товар. Цены в любом бизнесе могут быть согласованы на контрагента и даже иногда на транзакцию. По этой причине вы действительно должны хранить цену в таблице, которая фактически обрабатывает входящий или исходящий инвентарь (таблица инвентаризации). В таком случае ваша таблица дат / товаров / цен просто указывает базовую цену, которая может быть изменена на основе переговоров. В таком случае эта проблема переходит от проблемы отчетности к проблеме, которая является транзакционной и работает по одной строке из каждой таблицы за раз. Например, вы можете посмотреть цену по умолчанию для данного товара в данный день как:
С индексом цен (товар, дата) это будет хорошо работать.
Если это надуманный пример, возможно, что-то более близкое к тому, над чем вы работаете, поможет.
источник
most_recent
Подход должен хорошо работать на самую последнюю цену абсолютно . Казалось бы, ОП требует самой последней цены относительно каждой даты инвентаризации.Другим способом было бы использовать оконную функцию,
lead()
чтобы получить диапазон дат для каждой строки в таблице цен, а затем использоватьbetween
при присоединении к инвентарю. Я действительно использовал это в реальной жизни, но главным образом потому, что это была моя первая идея, как решить эту проблему.SqlFiddle
источник
Используйте объединение из инвентаря в цену с условиями соединения, которые ограничивают записи из таблицы цен только теми, которые находятся на или до даты инвентаризации, затем извлекают максимальную дату, и где дата является самой высокой датой из этого подмножества
Итак, для вашей инвентарной цены:
Если цена какого-либо указанного товара изменялась более одного раза в один и тот же день, и у вас действительно есть только даты, а не время в этих столбцах, вам может потребоваться применить дополнительные ограничения к объединениям, чтобы выбрать только одну из записей об изменении цены.
источник