[Oracle DB] Thủ tục xoay vòng dữ liệu trên bảng online

"Bảng NGHIEPVU online có dữ liệu lịch sử dẫn đến dung lượng lớn (Vài trăm GB) gây chậm cho các câu lệnh DML, yêu cầu của nghiệp vụ chỉ cần lưu các bản ghi trong vòng 3 ngày. Vậy làm thế nào để bảng này có cơ chế xoay vòng, chỉ lưu bản ghi của 3 ngày gần nhất, các bản ghi cũ hơn sẽ định kì insert vào bảng khác để lưu trữ"

## Bài toán này cần được chia nhỏ ra thành 2 bài toán con như sau:

  • Cắt lấy dữ liệu của 3 ngày gần nhất lưu lại bảng NGHIEPVU để giảm dung lượng cho bảng online, các dữ liệu cũ sẽ được lưu ở bảng khác.
    • Bảng có dung lượng lớn, nếu sử dụng phương án DELETE dữ liệu cũ sẽ gây tăng sinh Redo log, Archives log dẫn đến tăng I/O có thể gây down hệ thống (Trường hợp treo disk vì xóa số lượng lớn bản ghi bằng cách delete) -> Cần cắt chuyển bảng bằng cách tạo bảng mới từ dữ liệu gần nhất, sau đó bảng hiện hành sẽ trở thành bảng lưu trữ, bảng mới trở thành bảng hiện hành phục vụ nghiệp vụ.
  • Khi bảng chỉ còn dữ liệu của 3 ngày, thiết lập cơ chế định kì xóa các bản ghi cũ hơn 3 ngày, các bản  ghi thuộc diện "cũ" này sẽ được insert sang bảng khác có chức năng lưu trữ.
    • Khi này dữ liệu bảng đã nhỏ, có thể tiến hành select, insert bản ghi cũ sang bảng lưu trữ, delete bản ghi cũ ở bảng hiện hành.

## Lưu trình:

  • Cắt dữ liệu lần 1:
    • Select dữ liệu trong vòng 3 ngày để tạo thành bảng mới NGHIEPVU_NEW.
    • Đổi tên bảng hiện hành thành NGHIEPVU_OLD.
    • Đổi tên bảng NGHIEPVU_NEW thành NGHIEPVU.
  • Cắt dữ liệu định kì từ lần 2 trở đi:
    • Select dữ liệu cũ, insert vào bảng NGHIEPVU_OLD.
    • Delete dữ liệu cũ tại bảng NGHIEPVU.
## Giả lập dữ liệu:
--Tạo tablespace, tạo bảng:
CREATE TABLESPACE EMPLOYEE DATAFILE '/u01/app/oracle/oradata/CDB1/pdb1/EMPLOYEE.DBF' SIZE 250M AUTOEXTEND ON NEXT 250M MAXSIZE 1000M;
CREATE TABLE OAZ21.NGHIEPVU
	(
	  ID          NUMBER,
	  NAME        VARCHAR2(255 BYTE),
	  DATETIME    TIMESTAMP(6),
	  SALARY      NUMBER(10,2),
	  CREDITCARD  CHAR(12 BYTE)
	)
	TABLESPACE EMPLOYEE
	NOLOGGING 
	NOCOMPRESS 
	NOCACHE
;

-- Thủ tục chèn 10 triệu bản ghi vào bảng với trường thời gian từ 2024-01-01 00:00:01 đến 2024-12-31 23:59:59

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'INSERT01',
    job_type           => 'PLSQL_BLOCK',
    job_action         => q'[
        DECLARE
		  v_start_date TIMESTAMP := TO_TIMESTAMP('2024-01-01 00:00:01', 'YYYY-MM-DD HH24:MI:SS');
		  v_end_date TIMESTAMP := TO_TIMESTAMP('2024-12-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS');
		  v_diff_seconds NUMBER;
		  v_name VARCHAR2(255);
		  v_creditcard CHAR(12);
		  v_datetime TIMESTAMP;
		BEGIN
		  -- Calculate the difference in seconds between the start and end dates
		  SELECT (EXTRACT(DAY FROM (v_end_date - v_start_date)) * 86400 +
				  EXTRACT(HOUR FROM (v_end_date - v_start_date)) * 3600 +
				  EXTRACT(MINUTE FROM (v_end_date - v_start_date)) * 60 +
				  EXTRACT(SECOND FROM (v_end_date - v_start_date)))
		  INTO v_diff_seconds
		  FROM dual;

		  FOR i IN 1..10000000 LOOP
			v_name := 'Employee ' || TO_CHAR(i);
			v_creditcard := LPAD(TRUNC(DBMS_RANDOM.VALUE(100000000000, 999999999999)), 12, '0');
			-- Add a random interval, up to the difference calculated, to the start date
			v_datetime := v_start_date + NUMTODSINTERVAL(TRUNC(DBMS_RANDOM.VALUE(0, v_diff_seconds)), 'SECOND');

			INSERT /*+ APPEND*/ INTO NGHIEPVU (ID, Name, Datetime, Salary, CreditCard)
			VALUES (i, v_name, v_datetime, DBMS_RANDOM.VALUE(10000, 20000), v_creditcard);

			IF MOD(i, 500000) = 0 THEN
			  COMMIT;
			  DBMS_LOCK.SLEEP(5); -- Pause for 5 seconds after each commit to reduce load
			END IF;
		  END LOOP;
		  COMMIT; -- Ensure the final commit
		END;
    ]',
    start_date         => SYSTIMESTAMP,
    repeat_interval    => NULL,
    end_date           => NULL,
    enabled            => TRUE,
    auto_drop          => TRUE,
    comments           => ''
  );
END;
/

I. Cắt chuyển lần 1

-- Select, chuyển dữ liệu, đổi tên bảng. Để phù hợp với ví dụ thì tôi lấy mốc cụ thể là 2024-12-31 (Thực tế sẽ lấy theo thời gian thực lúc chạy job):

-- Tạo bảng mới với cấu trúc giống bảng cũ
CREATE TABLE NGHIEPVU_NEW 
TABLESPACE EMPLOYEE
NOLOGGING
NOCACHE
AS SELECT * FROM NGHIEPVU WHERE 0=1;

-- Chèn dữ liệu vào bảng mới từ bảng NGHIEPVU trong vòng 3 ngày
INSERT /*+ APPEND*/ INTO NGHIEPVU_NEW SELECT * FROM NGHIEPVU WHERE datetime >= to_date('2024-12-31','yyyy-mm-dd') - 3
order by datetime asc;

RENAME NGHIEPVU TO NGHIEPVU_OLD;

RENAME NGHIEPVU_NEW TO NGHIEPVU;

-- Như thường lệ, tôi sẽ không chạy câu lệnh riêng lẻ mà nhét chúng vào một job để không phụ thuộc vào sesion, câu lệnh được thực hiện liền mạch, hạn chế khoảng downtime. Cao cấp hơn, ta có thể cân nhắc chèn các khoảng sleep để giàn trải áp lực lên hệ thống, tránh ảnh hưởng nghiệp vụ online:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'ROTATE_04',
    job_type           => 'PLSQL_BLOCK',
    job_action         => q'[
        BEGIN
            EXECUTE IMMEDIATE '
                CREATE TABLE NGHIEPVU_NEW 
                TABLESPACE EMPLOYEE
                NOLOGGING
                NOCACHE
                AS SELECT * FROM NGHIEPVU WHERE 0=1
            ';
            EXECUTE IMMEDIATE '
                INSERT /*+ APPEND*/ INTO NGHIEPVU_NEW SELECT * FROM NGHIEPVU WHERE datetime >= to_date(''2024-12-31'',''yyyy-mm-dd'') - 3
            ';
            EXECUTE IMMEDIATE 'ALTER TABLE NGHIEPVU RENAME TO NGHIEPVU_OLD';
            EXECUTE IMMEDIATE 'ALTER TABLE NGHIEPVU_NEW RENAME TO NGHIEPVU';
        END;
    ]',
    start_date         => SYSTIMESTAMP,
    repeat_interval    => NULL,
    end_date           => NULL,
    enabled            => TRUE,
    auto_drop          => TRUE,
    comments           => 'no comment'
  );
END;
/

-- Chạy job gather để lấy thông tin thống kê của bảng NGHIEPVU:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'GATHER_11',
    job_type           => 'PLSQL_BLOCK',
    job_action         => q'[
        DECLARE
           l_table_name VARCHAR2 (30) := 'NGHIEPVU';
        BEGIN
           DBMS_STATS.GATHER_TABLE_STATS (
              ownname => 'OAZ21',
              tabname => l_table_name,
              method_opt => 'FOR ALL COLUMNS SIZE AUTO',
              estimate_percent => NULL,
              granularity => 'ALL',
              cascade => TRUE
           );
        END;
    ]',
    start_date         => SYSTIMESTAMP,
    repeat_interval    => NULL,
    end_date           => NULL,
    enabled            => TRUE,
    auto_drop          => TRUE,
    comments           => ''
  );
END;
/

-- Hoàn thành bước cắt chuyển bảng lớn thành bảng nhỏ:

-- Hoàn thành bước này, bảng NGHIEPVU giờ đã có dung lượng nhỏ, không phân mảnh, sẵn sàng phục vụ cho các nghiệp vụ OLTP yêu cầu thời gian phản hồi nhanh 24/7. Dữ liệu cũ trong bảng NGHIEPVU_OLD có thể truncate để lấy lại dung lượng hoặc thường được qui hoạch vào một tablespace có datafile nằm trên phân vùng khác phục vụ lưu trữ.

II. Xoay vòng dữ liệu từ lần 2 trở đi

-- Để minh họa việc xoay vòng dữ liệu, tôi sẽ giả lập thêm dữ liệu phát sinh từ 2025-01-01 đến 2025-01-05. Như vậy bảng NGHIEPVU sẽ có dữ liệu từ 2024-12-28 đến 2025-01-05.

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'INSERT02',
    job_type           => 'PLSQL_BLOCK',
    job_action         => q'[
        DECLARE
		  v_start_date TIMESTAMP := TO_TIMESTAMP('2025-01-01 00:00:01', 'YYYY-MM-DD HH24:MI:SS');
		  v_end_date TIMESTAMP := TO_TIMESTAMP('2025-01-05 23:59:59', 'YYYY-MM-DD HH24:MI:SS');
		  v_diff_seconds NUMBER;
		  v_name VARCHAR2(255);
		  v_creditcard CHAR(12);
		  v_datetime TIMESTAMP;
		BEGIN
		  -- Calculate the difference in seconds between the start and end dates
		  SELECT (EXTRACT(DAY FROM (v_end_date - v_start_date)) * 86400 +
				  EXTRACT(HOUR FROM (v_end_date - v_start_date)) * 3600 +
				  EXTRACT(MINUTE FROM (v_end_date - v_start_date)) * 60 +
				  EXTRACT(SECOND FROM (v_end_date - v_start_date)))
		  INTO v_diff_seconds
		  FROM dual;

		  FOR i IN 1..60000 LOOP
			v_name := 'Employee ' || TO_CHAR(i);
			v_creditcard := LPAD(TRUNC(DBMS_RANDOM.VALUE(100000000000, 999999999999)), 12, '0');
			-- Add a random interval, up to the difference calculated, to the start date
			v_datetime := v_start_date + NUMTODSINTERVAL(TRUNC(DBMS_RANDOM.VALUE(0, v_diff_seconds)), 'SECOND');

			INSERT /*+ APPEND*/ INTO NGHIEPVU (ID, Name, Datetime, Salary, CreditCard)
			VALUES (i, v_name, v_datetime, DBMS_RANDOM.VALUE(10000, 20000), v_creditcard);

			IF MOD(i, 500000) = 0 THEN
			  COMMIT;
			  DBMS_LOCK.SLEEP(5); -- Pause for 5 seconds after each commit to reduce load
			END IF;
		  END LOOP;
		  COMMIT; -- Ensure the final commit
		END;
    ]',
    start_date         => SYSTIMESTAMP,
    repeat_interval    => NULL,
    end_date           => NULL,
    enabled            => TRUE,
    auto_drop          => TRUE,
    comments           => ''
  );
END;
/

-- Bảng đã có dữ liệu đến 2025-01-05 như vậy chúng ta sẽ phải xóa bỏ dữ liệu cũ hơn 3 ngày, tức là trước 2025-01-02.

-- Lúc này bảng NGHIEPVU đã đủ nhỏ để xóa dữ liệu cũ bằng lệnh DELETE mà không làm tăng đột biến lượng Redo log, Archive log gây nguy hiểm cho hệ thống. Thủ tục xóa và insert dữ liệu cũ sang bảng lưu trữ:

CREATE OR REPLACE PROCEDURE rotate_data AS
BEGIN
  -- Chèn dữ liệu từ bảng data sang bảng data_old
  INSERT /*+ APPEND*/ INTO NGHIEPVU_OLD
  SELECT * FROM NGHIEPVU
  WHERE DATETIME < TRUNC(SYSDATE) - 3
  ORDER BY DATETIME ASC;

  -- Xóa dữ liệu quá 3 ngày từ bảng data
  DELETE FROM NGHIEPVU
  WHERE DATETIME < TRUNC(SYSDATE) - 3;

  COMMIT;
END;
/

-- Đặt job chạy theo tần suất mong muốn, ví dụ như hệ thống sẽ thực hiện rotate vào 23h đêm hàng ngày:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'rotate_data_job',
    job_type           => 'PLSQL_BLOCK',
    job_action         => 'BEGIN rotate_data; END;',
    start_date         => TRUNC(SYSDATE) + INTERVAL '23' HOUR, -- Starts today at 23:00
    repeat_interval    => 'FREQ=DAILY; BYHOUR=23', -- Runs daily at 23:00
    end_date           => NULL, -- No end date, so it continues indefinitely
    enabled            => TRUE, -- Start the job immediately
    auto_drop          => FALSE, -- Do not automatically drop the job after completion
    comments           => 'Job to rotate data from NGHIEPVU to NGHIEPVU_OLD'
  );
END;
/

-- Trong ví dụ này, tôi sẽ chạy trực tiếp thủ tục xóa từ mốc thời gian 2025-01-05 và cũ hơn 3 ngày:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name           => 'ROTATE_07',
    job_type           => 'PLSQL_BLOCK',
    job_action         => q'[
        BEGIN
            EXECUTE IMMEDIATE '
                -- Chèn dữ liệu từ bảng data sang bảng data_old
                INSERT /*+ APPEND*/ INTO NGHIEPVU_OLD
                SELECT * FROM NGHIEPVU
                WHERE DATETIME < to_date(''2025-01-05'',''yyyy-mm-dd'') - 3
                ORDER BY DATETIME ASC
            ';
            EXECUTE IMMEDIATE '
                -- Xóa dữ liệu quá 3 ngày từ bảng data
                DELETE FROM NGHIEPVU
                WHERE DATETIME < to_date(''2025-01-05'',''yyyy-mm-dd'') - 3
            ';
            COMMIT;
        END;
    ]',
    start_date         => SYSTIMESTAMP,
    repeat_interval    => NULL,
    end_date           => NULL,
    enabled            => TRUE,
    auto_drop          => TRUE,
    comments           => 'no comment'
  );
END;
/

-- Như vậy, trong bảng NGHIEPVU đã không còn dữ liệu trước 2025-01-02:

-- Còn bảng NGHIEPVU_OLD đã được bổ sung dữ liệu đến trước 2025-01-02 để phục vụ lưu trữ:

-- Đối với các hệ thống lớn như core viễn thông, ngân hàng, chứng khoán.. thì lượng log giao dịch sinh ra trong một ngày có thể lên đến hàng TB, gâp áp lực lên hiệu suất I/O và lưu trữ. Việc quay vòng dữ liệu cùng với kĩ thuật partitioning, indexing là chìa khoá giúp duy trì và đảm bảo hiệu suất của các hệ thống OLTP.

-- Câu lệnh stop job nếu cần:

--watch -n 5 'echo "SELECT JOB_NAME,SLAVE_OS_PROCESS_ID,ELAPSED_TIME,CPU_USED FROM USER_SCHEDULER_RUNNING_JOBS;" | sqlplus tuantn/*****@cdb1'
SELECT *--JOB_NAME,SLAVE_OS_PROCESS_ID,ELAPSED_TIME,CPU_USED
FROM USER_SCHEDULER_RUNNING_JOBS;

SELECT * FROM ALL_SCHEDULER_JOB_RUN_DETAILS
ORDER BY LOG_ID DESC;

--GRANT EXECUTE ON DBMS_LOCK TO TUANTN;
BEGIN
    -- Stop the job first
    DBMS_SCHEDULER.STOP_JOB(
        job_name => 'TUANTN.INSERT_EMPLOYEE_S01',
        force    => TRUE -- Use force to immediately stop the job
    );
    -- Removed the sleep delay
    -- Now drop the job
    DBMS_SCHEDULER.DROP_JOB(
        job_name => 'TUANTN.INSERT_EMPLOYEE_S01'
    );
END;
/

====

Nhận xét

Bài đăng phổ biến