Денормализация деревьев

в 12:04, , рубрики: oracle, деревья, Проектирование и рефакторинг, метки: ,

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

CREATE TABLE object (
  id NUMBER(11),
  parent_id NUMBER(11),
  type VARCHAR2(16) NOT NULL,
  name VARCHAR2(255) NOT NULL,
  CONSTRAINT pk_object PRIMARY KEY (id),
  CONSTRAINT fk_object_parent FOREIGN KEY (parent_id) REFERENCES object (id) ON DELETE CASCADE ENABLE
);

Наполним её какими-нибудь данными:

id  |  parent_id  |  type     |  name
------------------------------------------------------
1   |  NULL       |  country  |  Россия
2   |  1          |  region   |  Московская область
3   |  1          |  region   |  Новосибирская область
4   |  2          |  city     |  Москва
5   |  3          |  city     |  Новосибирск

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

-- Выбрать все города России
SELECT *
  FROM object
    WHERE type = 'city'
    START WITH id = 1 CONNECT BY PRIOR id = parent_id;

-- Выбрать страну, в которой находится Новосибирск
SELECT *
  FROM object
    WHERE type = 'country'
    START WITH id = 5 CONNECT BY PRIOR parent_id = id;

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

Основная идея заключается в использовании материализованного представления, которое хранит связи в более удобном для запросов виде. Для начала добавим пакет objects с функцией get_object_fast_table, которая будет возвращать нам денормализованные связи:

CREATE OR REPLACE
PACKAGE objects AS
    
  TYPE object_fast_type
    IS RECORD (
      object_id object.id%TYPE,
      object_name object.name%TYPE,
      object_type object.type%TYPE,
      parent_id object.id%TYPE,
      parent_name object.name%TYPE,
      parent_type object.type%TYPE,
      nesting_level NUMBER(20)
    );

  TYPE object_fast_table
    IS TABLE OF objects.object_fast_type;
  
  FUNCTION get_object_fast_table
    RETURN objects.object_fast_table PIPELINED;
    
END objects;
/

CREATE OR REPLACE
PACKAGE BODY objects AS

  FUNCTION get_object_fast_table
    RETURN objects.object_fast_table PIPELINED IS

    CURSOR objs IS
      SELECT id
        FROM object;
    
    CURSOR parents(obid object.id%TYPE) IS
      SELECT
        obid  object_id,
        obj.name object_name,
        obj.type object_type,
        par.id parent_id,
        par.name parent_name,
        par.type parent_type,
        level - 1 nesting_level
      FROM object par
      LEFT JOIN object obj ON obj.id = obid 
        START WITH par.id = obid  CONNECT BY PRIOR par.parent_id = par.id;

  BEGIN

    FOR obj IN objs LOOP
      FOR object_fast IN parents(obj.id) LOOP
        PIPE ROW (object_fast);
      END LOOP;
    END LOOP;

    RETURN;
  END get_object_fast_table;

END objects;
/

Теперь мы можем использовать денормализованную таблицу в запросах:

SELECT * FROM TABLE(objects.get_object_fast_table());
object_id | object_name           | object_type | parent_id |  parent_name           | parent_type | nesting_level
------------------------------------------------------------------------------------------------------------------
1         | Россия                | country     | 1         |  Россия                | country     | 0
2         | Московская область    | region      | 2         |  Московская область    | region      | 0
2         | Московская область    | region      | 1         |  Россия                | country     | 1
3         | Новосибирская область | region      | 3         |  Новосибирская область | region      | 0
3         | Новосибирская область | region      | 1         |  Россия                | country     | 1
4         | Москва                | city        | 4         |  Москва                | city        | 0
4         | Москва                | city        | 2         |  Московская область    | region      | 1
4         | Москва                | city        | 1         |  Россия                | country     | 2
5         | Новосибирск           | city        | 5         |  Новосибирск           | city        | 0
5         | Новосибирск           | city        | 3         |  Новосибирская область | region      | 1
5         | Новосибирск           | city        | 1         |  Россия                | country     | 2

Как можно увидеть, в таблице есть связи каждого объекта со всеми его родителями, а nesting_level — это число уровней до родителя. Чтоб постоянно не дёргать эту функцию, сохраним связи в материализованное представление:

CREATE MATERIALIZED VIEW object_fast
  REFRESH COMPLETE ON DEMAND
  START WITH trunc(sysdate)+4/24 NEXT (trunc(sysdate)+1)+4/24
  AS SELECT rownum id, objs.* FROM TABLE(objects.get_object_fast_table()) objs;

ALTER TABLE object_fast ADD CONSTRAINT pk_object_fast PRIMARY KEY (id);

Теперь вышеупомянутые запросы будут выглядеть так:

-- Выбрать все города России
SELECT *
  FROM object_fast
    WHERE parent_id = 1 AND object_type = 'city';

-- Выбрать страну, в которой находится Новосибирск
SELECT *
  FROM object_fast
    WHERE object_id = 5 AND parent_type = 'country';

Ну и, по желанию, можно добавить индексы:

CREATE INDEX object_fast_obj_id ON object_fast (object_id);
CREATE INDEX object_fast_par_id ON object_fast (parent_id);
CREATE INDEX object_fast_obj_type ON object_fast (object_type);
CREATE INDEX object_fast_par_type ON object_fast (parent_type);
CREATE INDEX object_fast_nesting ON object_fast (nesting_level);

Вот и всё. От себя скажу, что на нашем проекте этот способ дал прирост скорости запросов примерно в 60 раз. Используйте с умом и не забывайте, что полученные данные будут не всегда актуальными. Рекомендую применять этот способ только к редко добавляющимся и удаляющимся объектам. Ну или тогда стоит реализовать оперативное обновление материализованного представления. Нет предела полёту фантазии…

Автор: Webtoucher

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js