Skip to main content

Партиционируем и жмем огромную таблицу, с работающей репликацией

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

SELECT /*+ PARALLEL(t, 32),  INDEX(t, ORDERS_DATE_IDX) */
       COUNT(1),
       TO_CHAR(t.order_date, 'rrrr.mm')
FROM   orders t
GROUP BY TO_CHAR(t.order_date, 'rrrr.mm');

(у нас есть индекс по полю, которое будет впоследствии ключом партиционирования, и это хорошо)

Получим что-то вроде такого результата:

COUNT(1) ORDER_DATE
14281 2013.12
16223 2014.01
15018 2014.02
14926 2014.03
22395 2014.04
18620 2014.05
18111 2014.06
27140 2014.07
234983 2014.08
492579 2014.09
436314181 2025.12
570153932 2026.01
346233850 2026.02

Теперь, создадим таблицу, в которой есть все партиции, которые нам нужны. Атрибут компрессии мы выставлять не будем:

CREATE TABLE orders_parts (
    order_id      NUMBER PRIMARY KEY,
    order_date    DATE,
    customer_id   NUMBER,
    order_status  VARCHAR2(20),
    ...
)
PARTITION BY RANGE (order_date) INTERVAL (INTERVAL '1' MONTH)
(
  PARTITION P_BEFORE_2013 VALUES LESS THAN (DATE '2013-12-01'),
  PARTITION P_201312 VALUES LESS THAN (DATE '2014-01-01') TABLESPACE CUST_DATA,
  PARTITION P_201401 VALUES LESS THAN (DATE '2014-02-01') TABLESPACE CUST_DATA,
  PARTITION P_201402 VALUES LESS THAN (DATE '2014-03-01') TABLESPACE CUST_DATA,
  ........
  PARTITION P_202511 VALUES LESS THAN (DATE '2025-12-01') TABLESPACE CUST_DATA,
  PARTITION P_202512 VALUES LESS THAN (DATE '2026-01-01') TABLESPACE CUST_DATA,
  PARTITION P_202601 VALUES LESS THAN (DATE '2026-02-01') TABLESPACE CUST_DATA,
  PARTITION P_202602 VALUES LESS THAN (DATE '2026-03-01') TABLESPACE CUST_DATA
);

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

Для этого, используем DBMS_PARALLEL_EXECUTE...

SET SERVEROUTPUT ON SIZE UNLIMITED
SET LINESIZE 200
SET PAGESIZE 0
SET FEEDBACK OFF
SET VERIFY OFF

ALTER SESSION ENABLE PARALLEL DML;

-- 1. Создаём задачу
BEGIN
  DBMS_PARALLEL_EXECUTE.CREATE_TASK('LOAD_ORDERS_PARTS');
END;
/

-- 2. Создаём чанки (диапазоны дат) для всех партиций, кроме двух последних
BEGIN
  DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_SQL(
    task_name    => 'LOAD_ORDERS_PARTS',
    sql_stmt     => 
      'WITH part_info AS (
         SELECT partition_name,
                CASE 
                  WHEN partition_name = ''P_BEFORE_2023'' THEN DATE ''1900-01-01''
                  ELSE TO_DATE(SUBSTR(partition_name, 3, 6) || ''01'', ''YYYYMMDD'')
                END AS start_date,
                CASE 
                  WHEN partition_name = ''P_BEFORE_2023'' THEN DATE ''2013-12-01''
                  ELSE ADD_MONTHS(TO_DATE(SUBSTR(partition_name, 3, 6) || ''01'', ''YYYYMMDD''), 1)
                END AS end_date,
                partition_position
         FROM user_tab_partitions
         WHERE table_name = ''ORDERS_PARTS''
       )
       SELECT start_date, end_date
       FROM part_info
       WHERE partition_position <= (SELECT MAX(partition_position)-2 FROM part_info)
       ORDER BY partition_position',
    by_rowid => FALSE
  );
END;
/

-- 3. Запускаем задачу с параллельностью 16
BEGIN
  DBMS_PARALLEL_EXECUTE.RUN_TASK(
    task_name      => 'LOAD_ORDERS_PARTS',
    sql_stmt       => 'INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
    SELECT /*+ PARALLEL(orders,16) */ * FROM orders
    WHERE order_date >= :start_id AND order_date < :end_id',
    language_flag  => DBMS_SQL.NATIVE,
    parallel_level => 16
  );
END;
/

-- 4. Зачистка задач
exec DBMS_PARALLEL_EXECUTE.DROP_TASK('LOAD_ORDERS_PARTS');

-- 5. Посмотреть, что там с обработкой чанков
SELECT
       task_name,
       status
FROM
       user_parallel_execute_tasks;

SELECT
         chunk_id,
         status,
         start_rowid,
         end_rowid
FROM
         user_parallel_execute_chunks
WHERE
         task_name = 'LOAD_ORDERS_PARTS'
ORDER BY
         chunk_id;

Или, если не хочется по каким то причинам использовать DBMS_PARALLEL_EXECUTE, все тоже самое, сделаем вручную:

-- Партиция P_BEFORE_2023: все заказы до 2013-12-01
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date < DATE '2013-12-01';
COMMIT;

-- Партиция P_202302: декабрь 2013 (2013-12-01 <= order_date < 2014-01-01)
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date >= DATE '2013-12-01' AND order_date < DATE '2014-01-01';
COMMIT;

-- Партиция P_202303: январь 2014
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date >= DATE '2014-01-01' AND order_date < DATE '2014-02-01';
COMMIT;

-- Партиция P_202304: февраль 2014
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date >= DATE '2014-02-01' AND order_date < DATE '2014-03-01';
COMMIT;

-- ... аналогично для всех промежуточных месяцев до 2025 ...

-- Партиция P_202511: ноябрь 2025
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date >= DATE '2025-11-01' AND order_date < DATE '2025-12-01';
COMMIT;

-- Партиция P_202512: декабрь 2025
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders
WHERE order_date >= DATE '2025-12-01' AND order_date < DATE '2026-01-01';
COMMIT;

-- Две последнии партиции не трогаем

Вариант с ручным переносом, может быть полезен, в случае, если места у нас осталось впритык и как только, мы перенесли одну партицию, мы ее сразу же компрессируем.

Теперь, собственно мы берем наши партиции и добавляем в них компрессию:

ALTER TABLE ORDERS MOVE PARTITION P_BEFORE_2013 COMPRESS FOR QUERY HIGH PARALLEL 16;
ALTER TABLE ORDERS MOVE PARTITION PARTITION P_201312 COMPRESS FOR QUERY HIGH PARALLEL 16;
ALTER TABLE ORDERS MOVE PARTITION PARTITION P_201401 COMPRESS FOR QUERY HIGH PARALLEL 16;
ALTER TABLE ORDERS MOVE PARTITION PARTITION P_201402 COMPRESS FOR QUERY HIGH PARALLEL 16;
.... и так далее

После того, как все пожато, нам осталось долить данные, в партиции, которые будут активно изменяться в текущем периоде. И тут, у нас есть 2 подхода. Если мы можем позволить себе небольшой даунтайм (отключение записи в таблицу) на время, пока мы переливаем данные в последние партиции, и строим нужные индексы то, в принципе, следующий этап совсем прост. Нам нужно остановить репликат, по аналогии с тем, как мы заливали предыдущие данные, долить оставшиеся, построить локальный индекс, сделать переименование таблицы (возможно, придется откомпилить зависимые объекты) и снова запустить репликат.

В случае, если даунтайм, должен быть минимальный (минуты) мы сделаем по другому:

1 У нас, уже есть репликат, который пишет данные в исходную таблицу и нам, нужно засечь SCN, начиная с которого, мы будем заполнять нашу новую таблицу. Для этого, мы создадим новый репликат, с папингом, аналогичным исходному, например так:

--Наш исходный репликат, потребляет трэйл ./dirdat/ad
ADD REPLICAT RORDPART, EXTTRAIL ./dirdat/ad checkpointtable GGATE.CHECKPOINTS

--На этом же этапе, подготовим репликаты, для синхронной остановки через  eventactions( stop )
--Создадим в базах - источнике и приемнике, таблицу:
create table GGATE.STOPACTION
(
  id        number GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  stopdate  date 
);
--Добавим логирвоане на эту таблицу:
ALTER TABLE GGATE.STOPACTION ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;

-- В экстратор, который поставляет трэйл - файлы для репликатов, пропишем:
TABLE GGATE.STOPACTION, tokens( commit_scn=@GETENV('TRANSACTION', 'CSN') );

--Перезапустим экстратор

--Добавим конструкцию в оба репликата:
MAP GGATE.STOPACTION, target GGATE.STOPACTION, colmap(usedefaults, stop_scn=@TOKEN('commit_scn')), eventactions( stop );

2 Пропишем конфиг, аналогичный исходному репликату в наш новый репликат, оставив в новом репликате, мапинг, только на одну таблицу ORDER_PARTS

3 Остановим исходный репликат, и засечем SCN

SELECT CURRENT_SCN FROM V$DATABASE;

4 Переставим наш новый репликат на тот RBA, на котором у нас остановлен основной репликат:

--Посмотрим где у нас остановлен основной репликат
GGSCI (ggate.local.net) 2> info RORDERS

Replicat   RORDERS   Last Started 2026-02-24 21:03   Status STOPPED
Checkpoint Lag       00:02:05 (updated 00:00:02 ago)
Process ID           2394620
Log Read Checkpoint  File ./dirdat/ad002328787
                     2026-02-25 12:22:12.000000  RBA 407552118

--Переставим новый репликат на нужный RBA
GGSCI (ggate.local.net) 3> ALTER REPLICAT RORDPART EXTSEQNO 2328787 EXTRBA 407552118

--Запустим основной репликат
GGSCI (ggate.local.net) 4> START RORDERS

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

5 Перенесём данные из исходной таблицы, который у нас остались:

-- Партиция P_202601: январь 2026
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders AS OF SCN (<значени SCN, которое мы засекли>)
WHERE order_date >= DATE '2026-01-01' AND order_date < DATE '2026-02-01';
COMMIT;
-- Партиция P_202602: февраль 2026
INSERT /*+ APPEND PARALLEL(16) */ INTO orders_parts
SELECT /*+ PARALLEL(orders,16) */ * FROM orders AS OF SCN (<значени SCN, которое мы засекли>)
WHERE order_date >= DATE '2026-02-01' AND order_date < DATE '2026-03-01';
COMMIT;

6 Проиндексируем таблицу:

CREATE UNIQUE INDEX orders_parts_idx on orders_parts(order_id, order_date) local parallel 16;
alter index orders_parts_idx noparallel;

7 Запустим репликат, который мы создали:

GGSCI (gghubs.local.net) 3> START RORDPART

8 После того, как репликат догонит таблицу по данным, мы остановим оба репликата синхронно. Для этого у нас есть несколько вариантов:

  • остановка пампа\экстратора, который формирует трэйл. Это самое простое, но, возможно, что экстратор, после рестарта, будет долго выполнят рекавер
  • остановка, через событие  eventactions( stop ) самый подходящий для нас вариант

9 Остановка событием. Для этого, просто, делаем инсерт в таблицу, которую мы создавали на источнике:

insert into GGATE.STOPACTION (STOPDATE) values (SYSDATE);
COMMIT;

Оба репликата, синхронно остановтся

10 Переименовываем таблицы:

--Преименуем таблицы
ALTER TABLE ORDERS RENAME TO ORDERS_BACK;
ALTER TABLE ORDERS_PARTS RENAME TO ORDERS;

--Проверим, что у нас развалились зависимые пакеты и сделаем рекмпиляцию пакетов, которые развалились

11 Мы можем запустить 'старый' репликат и удалить 'новый'

GGSCI (gghubs.local.net) 3> START RORDERS

GGSCI (gghubs.local.net) 3> STOP RORDPART

--Для того, чтобы в базе не осталось ошметков в таблице чекпоинтов, перед удалением, логинимсяв базу
GGSCI (gghubs.local.net) 3> DBLOGIN USERIDALIAS <алиас>
GGSCI (gghubs.local.net) 3> DELETE RORDPART