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

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

```sql
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');
```

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

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

<table border="0" cellpadding="0" cellspacing="0" id="bkmrk-count%281%29-order_date-" style="border-collapse: collapse; width: 192pt;" width="254"><colgroup><col span="2" style="mso-width-source: userset; mso-width-alt: 4445; width: 96pt;" width="127"></col> </colgroup><tbody><tr style="height: 14.5pt;"><td height="19" style="height: 14.5pt; width: 96pt;" width="127">COUNT(1)</td><td style="width: 96pt;" width="127">ORDER\_DATE</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">14281</td><td align="right">2013.12</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">16223</td><td align="right">2014.01</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">15018</td><td align="right">2014.02</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">14926</td><td align="right">2014.03</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">22395</td><td align="right">2014.04</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">18620</td><td align="right">2014.05</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">18111</td><td align="right">2014.06</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">27140</td><td align="right">2014.07</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">234983</td><td align="right">2014.08</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">492579</td><td align="right">2014.09</td></tr><tr style="height: 14.5pt;"><td height="19" style="height: 14.5pt;">…</td><td>…</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">436314181</td><td align="right">2025.12</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">570153932</td><td align="right">2026.01</td></tr><tr style="height: 14.5pt;"><td align="right" height="19" style="height: 14.5pt;">346233850</td><td align="right">2026.02</td></tr></tbody></table>

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

```sql
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...

```sql
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_2013'' 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_2013'' 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_2013: все заказы до 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_201401: декабрь 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_201402: январь 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_201403: февраль 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 P_201312 COMPRESS FOR QUERY HIGH PARALLEL 16;
ALTER TABLE ORDERS MOVE PARTITION P_201401 COMPRESS FOR QUERY HIGH PARALLEL 16;
ALTER TABLE ORDERS MOVE PARTITION P_201402 COMPRESS FOR QUERY HIGH PARALLEL 16;
.... и так далее
```

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

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

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

```bash
--Наш исходный репликат, потребляет трэйл ./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

```sql
SELECT CURRENT_SCN FROM V$DATABASE;
```

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

```bash
--Посмотрим где у нас остановлен основной репликат
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 Запустим репликат, который мы создали:

```bash
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;

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

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

```bash
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
```

Итого, у нас теперь есть партиционированная таблица, с пожатыми историческими партициями и общее время простоя репликации - несколько минут.