Оптимизация соединения запросов Oracle для диапазона значений (объединение по n между x и y)

У меня есть две большие (около 1 млн строк каждая) таблицы Oracle, которые мне нужно объединить в диапазоне целых чисел. Например,

Range_Descriptors
-----------------
range_id
from_value number(38)
to_value number(38)
descriptor

Values
------
value_id 
value number(38)
...

Итак, в этом случае мне нужно получить Range_Descriptors.descriptor для каждой записи в значениях, где value находится между from_value и to_value.

В Range_Descriptors диапазоны не перекрываются, и очень большой процент диапазонов представляет собой только одно или два значения (т. е. from_value = to_value или to_value+1), но даже исключение этих особых случаев помогает лишь в некоторой степени.

Самый очевидный запрос действительно возвращает ожидаемые результаты; однако он работает очень медленно (~ 20 минут):

select v.value_id, r.descriptor
from Values v join Range_Descriptors r on v.value between r.from_value and r.to_value;

Я пробовал разные варианты индексации, но не нашел ничего, что значительно улучшало бы производительность. В настоящее время существуют индексы Range_Descriptors(from_value, to_value) и Values(value). Предложения?

Какую версию Oracle вы используете? Если у вас версия 12.2 или выше, надеюсь, Oracle будет использовать присоединение к группе . Индексы здесь, скорее всего, не помогут, поскольку обычно они полезны только для выбора небольшого процента данных из таблицы, а вы выбираете все строки. Можете ли вы создать отчет SQL Monitor, чтобы показать план выполнения? Найдите SQL_ID в GV$SQL, запустите select dbms_sqltune.report_sql_monitor('your SQL_ID here') from dual; и отредактируйте результат в своем вопросе.

Jon Heller 21.06.2024 00:34

У вас действительно 1 миллион диапазонов? Они перекрываются?

The Impaler 21.06.2024 06:09

Спасибо, @Джон Хеллер; Я согласен, что присоединение к группе решило бы проблему здесь, но по какой-то причине я не смог заставить его использовать ее даже с подсказкой оптимизатора. Однако подход Пола В. сработал.

verbalclay 21.06.2024 22:24
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
1
3
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это неприятная проблема с ключами диапазона (даты начала/окончания, значения от/до и т. д.). Хотя Oracle может использовать сканирование индекса для исключения одной стороны (больше или меньше) вашего диапазона, это оставляет другую сторону открытой, и в этом проблема.

В этой ситуации я использую два подхода в зависимости от объема данных.

  1. Для небольших таблиц (1 млн строк на самом деле довольно мало) или для тех случаев, когда вы собираетесь выполнять лишь несколько поисков определенных значений, просто создайте объединенный индекс (из двух столбцов) на (from_value,to_value) и убедитесь, что ваши запросы выполняют сканирование диапазона. на нем (подсказка, если это не так). Это позволит избежать попадания в таблицу любой строки, выходящей за пределы диапазона. Тем не менее, по-прежнему требуется сканирование одного блока связанного списка конечных узлов индекса для всех старых диапазонов, которые предшествуют желаемой дате - это потому, что двоичный поиск, выполняемый в первом столбце, будет удовлетворяться самым старым значением и продолжать удовлетворяться. до тех пор, пока не превысит целевое значение. Таким образом, вы в конечном итоге выполняете множество операций по получению индексных блоков.

  2. Для больших таблиц (сотни миллионов или миллиарды строк) и когда вы собираетесь обрабатывать большую часть значений (например, в процессе ETL, материализованном представлении или какой-либо повседневной работе), вам может понадобиться другая стратегия. Что для меня хорошо работает, так это группировать таблицу диапазонов на основе размера диапазона, чтобы вы могли использовать предикат равенства в своих соединениях для большинства строк и минимизировать количество строк, в которых необходимо использовать неравенство. Есть несколько способов сделать это. Вот пример из системы, которая историзирует данные с использованием идентификаторов начала/конца (которые в других местах привязаны к временным меткам). Мне часто нужно получить строку, которая была активна на момент определенного идентификатора снимка. Когда объем действительно огромен, требуется некоторый творческий подход:

       SELECT /*+ NO_MERGE USE_HASH(dt rt) */
              gd.driving_start_snapshot_id,
              rt.*
         FROM drivingtable dt,
              rangetable rt
        WHERE FLOOR(rt.composite_start_snapshot_id / 1000000) = FLOOR(rt.composite_end_snapshot_id / 1000000)
          AND FLOOR(dt.driving_start_snapshot_id / 1000000) = FLOOR(rt.composite_start_snapshot_id / 1000000)
          AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
          AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id
       UNION ALL
       SELECT /*+ NO_MERGE USE_HASH(dt rt) */
              dt.driving_start_snapshot_id,
              rt.*
         FROM drivingtable dt,
              rangetable rt
        WHERE FLOOR(rt.composite_start_snapshot_id / 1000000) != FLOOR(rt.composite_end_snapshot_id / 1000000)
          AND FLOOR(rt.composite_start_snapshot_id / 10000000) = FLOOR(rt.composite_end_snapshot_id / 10000000)
          AND FLOOR(dt.driving_start_snapshot_id / 10000000) = FLOOR(rt.composite_start_snapshot_id / 10000000)
          AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
          AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id
       UNION ALL
       SELECT /*+ NO_MERGE USE_HASH(dt rt) */
              dt.driving_start_snapshot_id,
              rt.*
         FROM drivingtable dt,
              rangetable rt
        WHERE FLOOR(rt.composite_start_snapshot_id / 10000000) != FLOOR(rt.composite_end_snapshot_id / 10000000)
          AND FLOOR(rt.composite_start_snapshot_id / 20000000) = FLOOR(rt.composite_end_snapshot_id / 20000000)
          AND FLOOR(dt.driving_start_snapshot_id / 20000000) = FLOOR(rt.composite_start_snapshot_id / 20000000)
          AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
          AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id
       UNION ALL
       SELECT /*+ NO_MERGE USE_HASH(dt rt) */
              dt.driving_start_snapshot_id,
              rt.*
         FROM drivingtable dt,
              rangetable rt
        WHERE FLOOR(rt.composite_start_snapshot_id / 20000000) != FLOOR(rt.composite_end_snapshot_id / 20000000)
          AND FLOOR(rt.composite_start_snapshot_id / 100000000) = FLOOR(rt.composite_end_snapshot_id / 100000000)
          AND FLOOR(dt.driving_start_snapshot_id / 100000000) = FLOOR(rt.composite_start_snapshot_id / 100000000)
          AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
          AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id
       UNION ALL
       SELECT /*+ NO_MERGE USE_MERGE(dt rt) */
              dt.driving_start_snapshot_id,
              rt.*
         FROM drivingtable dt,
              rangetable rt
        WHERE FLOOR(rt.composite_start_snapshot_id / 100000000) != FLOOR(rt.composite_end_snapshot_id / 100000000)
          AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
          AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id
    

В этом примере первый блок говорит, что если диапазон находится в пределах одного и того же сегмента в 1 миллион (ПЛОЩАДЬ значения / 1 миллион):

FLOOR(rt.composite_start_snapshot_id / 1000000) = FLOOR(rt.composite_end_snapshot_id / 1000000)

затем выполните эквисоединение для этого значения сегмента:

FLOOR(dt.driving_start_snapshot_id / 1000000) = FLOOR(rt.composite_start_snapshot_id / 1000000)

Внутри этого эквивалентного соединения уточните результаты с помощью фактического оператора > < или between:

AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id

Но эквисоединение выполняет большую часть работы и именно здесь достигается прирост производительности.

Для строк, охватывающих несколько блоков по 1 миллиону (!=), попробуйте использовать более широкий блок — например, 10 миллионов. Если начало/конец находится в одном и том же 10-миллионном блоке, выполните равное соединение по значению этого блока:

          FLOOR(rt.composite_start_snapshot_id / 1000000) != FLOOR(rt.composite_end_snapshot_id / 1000000)
      AND FLOOR(rt.composite_start_snapshot_id / 10000000) = FLOOR(rt.composite_end_snapshot_id / 10000000)
      AND FLOOR(dt.driving_start_snapshot_id / 10000000) = FLOOR(rt.composite_start_snapshot_id / 10000000)

Для строк, охватывающих несколько блоков по 10 миллионов, снова расширьте их... попробуйте 20 миллионов, затем 100 миллионов и т. д. Просто убедитесь, что размер каждого сегмента кратен предыдущему, чтобы избежать обработки одной и той же строки в большем количестве блоков. чем один блок. В противном случае вам придется включить весь список сегментов !=, которые предшествуют каждому блоку, чтобы гарантировать отсутствие дублирования строк.

В конце концов вы доберетесь до строк, диапазоны которых настолько широки, что отдача от продолжения этого будет уменьшаться, поскольку каждая из них требует еще одного прохода данных. Итак, в какой-то момент вы сократите свои потери и получите универсальный блок, который обрабатывает все строки, не обработанные ничем из вышеперечисленного — это последний в моем примере:

             FLOOR(rt.composite_start_snapshot_id / 100000000) != FLOOR(rt.composite_end_snapshot_id / 100000000)
         AND dt.driving_start_snapshot_id >= rt.composite_start_snapshot_id
         AND dt.driving_start_snapshot_id < rt.composite_end_snapshot_id

Это позволит выполнить соединение по неравенству без какого-либо соединения по равенству, но, надеюсь, к тому времени вы будете иметь дело с очень небольшим количеством строк, поэтому влияние на производительность будет смягчено.

Этот подход можно записать в SQL различными способами, причем вышеописанный является только одним из способов. Но общая концепция заключается в том, чтобы разделять и властвовать с помощью отдельных шагов, которые находят способ использовать предикат равенства для как можно большего числа строк. Сколько дискретных шагов и размер корзины вы выберете, зависит от ваших данных и от того, где находится точка убывающей отдачи для выполнения нескольких проходов. Но во многих случаях я обнаруживал, что 3–5 отдельных шагов действительно могут значительно повысить производительность при объединении диапазонов по сравнению с выполнением их всех одновременно.

Другие вопросы по теме