Как сравнить диапазон целочисленных значений в PL / SQL? - PullRequest
0 голосов
/ 18 ноября 2018

Я пытаюсь сравнить диапазон целочисленных значений между тестовой таблицей и справочной таблицей.Если какой-либо диапазон значений из тестовой таблицы перекрывается с доступными диапазонами в справочной таблице, его следует удалить.

Извините, если неясно, но приведен пример данных:

TEST_TABLE:

MIN          MAX
10           121
122          648
1200         1599

REFERENCE_TABLE:

MIN          MAX
50           106
200          1400
1450         1500

MODIFIED TEST_TABLE: (ожидаемый результат после запуска PL / SQL)

MIN          MAX
10           49
107          121
122          199
1401        1449
1501        1599

В первом ряду из приведенного выше примера 10-121 был разделен на две строки: 10-49 и 107-121, потому что значения 50, 51, ..., 106включен в первый ряд reference_table (50-106);и так далее ..

Вот что я написал до сих пор с вложенными циклами.Я создал две дополнительные временные таблицы, которые будут хранить все значения, которые будут найдены в справочной таблице.Затем он создаст новые наборы диапазонов для вставки в test_table.

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

CREATE TABLE new_table (num_value NUMBER);
CREATE TABLE new_table_next (num_value NUMBER, next_value NUMBER);

- начало PL / SQL

DECLARE
  l_count NUMBER;
  l_now_min NUMBER;
  l_now_max NUMBER;

  l_final_min NUMBER;
  l_final_max NUMBER;

BEGIN

  FOR now IN (SELECT min_num, max_num FROM test_table) LOOP
    l_now_min:=now.min_num;
    l_now_max:=now.max_num;
    WHILE (l_now_min < l_now_max) LOOP

      SELECT COUNT(*) -- to check if number is found in reference table
      INTO l_count
      FROM reference_table refr
      WHERE l_now_min  >= refr.min_num
      AND l_now_min   <= refr.max_num;

      IF l_count        > 0 THEN

        INSERT INTO new_table (num_value) VALUES (l_now_min);
        COMMIT;

      END IF;

      l_now_min:=l_now_min+1;

    END LOOP;

    INSERT INTO new_table_next (num_value, next_value)
    VALUES (SELECT num_value, (SELECT MIN (num_value) FROM new_table t2 WHERE t2.num_value > t.num_value) AS next_value FROM new_table t);

    DELETE FROM test_table t
    WHERE now.min_num   = t.min_num
    AND now.max_num   = t.max_num;
    COMMIT;

    SELECT (num_value + 1) INTO l_final_min FROM new_table_next;
    SELECT (next_value - num_value - 2) INTO l_final_max FROM new_table_next;

    INSERT INTO test_table (min_num, max_num)
        VALUES (l_final_min, l_final_max);
    COMMIT;

    DELETE FROM new_table;
    DELETE FROM new_table_next;

    COMMIT;
  END LOOP;
END;
/

Пожалуйста, помогите, я застрял.:)

Ответы [ 3 ]

0 голосов
/ 18 ноября 2018

Это изменение от аналогичной задачи (с использованием дат вместо чисел), которую я сделал для Teradata, она основана на тех же базовых данных, что и данные Гордона (все начальные / конечные значения объединены в одном списке), но использует более простую логику:

WITH minmax AS 
 ( -- create a list of all existing start/end values (possible to simplify using Unpivot or Cross Apply)
   SELECT Min AS val, -1 AS prio, 1 AS flag  -- main table, range start
   FROM test_table
   UNION ALL
   SELECT Max+1,   -1, -1                      -- main table, range end
   FROM test_table
   UNION ALL 
   SELECT Min, 1,  1                      -- reference table, adjusted range start
   FROM reference_table
   UNION ALL
   SELECT Max+1, 1, -1                      -- reference table, adjusted range end
   FROM reference_table
 )
, all_ranges AS 
 ( -- create all ranges from current to next row
   SELECT minmax.*,
     Lead(val) Over (ORDER BY val, prio desc, flag) AS next_val,  -- next value = end of range
     Sum(flag) Over (ORDER BY val, prio desc, flag ROWS Unbounded Preceding) AS Cnt -- how many overlapping periods exist
   FROM minmax
 )
SELECT val, next_val-1
FROM all_ranges
WHERE Cnt = 1           -- 1st level only
  AND prio + flag =  0  -- either (prio -1 and flag  1) = range start in base table
                        --     or (prio  1 and flag -1) = range end in ref table
ORDER BY 1

См. db-fiddle

0 голосов
/ 19 ноября 2018

Вот один из способов сделать это. Я помещаю тестовые данные в предложение WITH, а не создаю таблицы (я считаю, что это проще для целей тестирования). Я использовал ваши имена столбцов (MIN и MAX); Это очень плохой выбор, поскольку MIN и MAX являются ключевыми словами Oracle. Они наверняка вызовут путаницу и могут привести к ошибкам в запросах.

Стратегия проста - сначала возьмите КОМПЛЕМЕНТ из диапазонов в REFERENCE_TABLE, который также будет объединением интервалов (используя NULL в качестве маркера для минус бесконечности и плюс бесконечности); затем возьмите пересечение каждого интервала в TEST_TABLE с каждым интервалом в дополнении к REFERENCE_TABLE. Как это сделать, показано в последнем (внешнем) запросе в приведенном ниже решении.

with
  test_table (min, max) as (
    select   10,  121 from dual union all
    select  122,  648 from dual union all
    select 1200, 1599 from dual
  )
, reference_table (min, max) as (
    select   50,  106 from dual union all
    select  200, 1400 from dual union all
    select 1450, 1500 from dual
  )
, 
  prep (min, max) as (
    select lag(max) over (order by max) + 1 as min
         , min - 1                          as max
      from ( select min, max   from reference_table
             union  all
             select null, null from dual
           )
  )
select   greatest(t.min, nvl(p.min, t.min)) as min
     ,   least   (t.max, nvl(p.max, t.max)) as max
from     test_table t inner join prep p
                      on  t.min <= nvl(p.max, t.max)
                      and t.max >= nvl(p.min, t.min)
order by min
;

       MIN        MAX
---------- ----------
        10         49
       107        121
       122        199
      1401       1449
      1501       1599
0 голосов
/ 18 ноября 2018

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

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

Вот рабочая версия:

with vals as (
      select min as x, 1 as inc, 0 as is_ref
      from test_table
      union all
      select max + 1, -1 as inc, 0 as is_ref
      from test_table
      union all
      select min as x, 0, 1 as is_ref
      from reference_table
      union all
      select max + 1 as x, 0, -1 as is_ref
      from reference_table
     )
select min, max
from (select refgrp, incgrp, ref, inc2, min(x) as min, (lead(min(x), 1, max(x) + 1) over (order by min(x))  - 1) as max
      from (select v.*,
                  row_number() over (order by x) - row_number() over (partition by ref order by x) as refgrp,
                  row_number() over (order by x) - row_number() over (partition by inc2 order by x) as incgrp
            from (select v.*, sum(is_ref) over (order by x, inc) as ref,
                         sum(inc) over (order by x, inc) as inc2
                  from vals v
                 ) v
           ) v
      group by refgrp, incgrp, ref, inc2
     ) v
where ref = 0 and inc2 = 1 and min < max
order by min;

И здесь - это дБ <> скрипка.

Обратная задача получения перекрытий намного проще. Для этого может быть целесообразно «инвертировать» справочную таблицу.

select greatest(tt.min, rt.min), least(tt.max, rt.max)
from test_table tt join
     reference_table rt
     on tt.min < rt.max and tt.max > rt.min  -- is there an overlap?
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...