Cara terbaik untuk menghapus recordset yang sangat besar di Oracle

18

Saya mengelola aplikasi yang sangat besar (hampir 1TB data dengan lebih dari 500 juta baris dalam satu tabel) database Oracle back end. Basis data tidak benar-benar melakukan apa-apa (tidak ada SProcs, tidak ada pemicu atau apa pun) itu hanya menyimpan data.

Setiap bulan kita diharuskan untuk membersihkan catatan dari dua tabel utama. Kriteria untuk pembersihan bervariasi dan merupakan kombinasi umur baris dan beberapa bidang status. Kami biasanya membersihkan antara 10 dan 50 juta baris per bulan (kami menambahkan sekitar 3-5 juta baris per minggu melalui impor).

Saat ini kami harus melakukan penghapusan ini dalam batch sekitar 50.000 baris (mis. Hapus 50000, komit, hapus 50000, komit, ulangi). Mencoba menghapus seluruh kumpulan sekaligus, membuat basis data tidak responsif selama sekitar satu jam (tergantung pada # baris). Menghapus baris dalam batch seperti ini sangat kasar pada sistem dan kami biasanya harus melakukannya "sesuai waktu" selama satu minggu; membiarkan skrip berjalan terus menerus dapat mengakibatkan penurunan kinerja yang tidak dapat diterima pengguna.

Saya percaya bahwa penghapusan batch seperti ini juga menurunkan kinerja indeks dan memiliki dampak lain yang akhirnya menyebabkan kinerja database menurun. Ada 34 indeks hanya dalam satu tabel, dan ukuran data indeks sebenarnya lebih besar dari data itu sendiri.

Berikut ini skrip yang digunakan oleh salah satu staf TI kami untuk melakukan pembersihan ini:

BEGIN
LOOP

delete FROM tbl_raw 
  where dist_event_date < to_date('[date]','mm/dd/yyyy') and rownum < 50000;

  exit when SQL%rowcount < 49999;

  commit;

END LOOP;

commit;

END;

Basis data ini harus mencapai 99,99999% dan kami hanya memiliki jendela pemeliharaan 2 hari setahun sekali.

Saya mencari metode yang lebih baik untuk menghapus catatan ini, tetapi saya belum menemukan. Ada saran?

Coding Gorilla
sumber
Perhatikan juga ada 30+ indeks yang dimainkan di sini
jcolebrand

Jawaban:

17

Logika dengan 'A' dan 'B' mungkin "disembunyikan" di belakang kolom virtual tempat Anda dapat melakukan partisi:

alter session set nls_date_format = 'yyyy-mm-dd';
drop   table tq84_partitioned_table;

create table tq84_partitioned_table (
  status varchar2(1)          not null check (status in ('A', 'B')),
  date_a          date        not null,
  date_b          date        not null,
  date_too_old    date as
                       (  case status
                                 when 'A' then add_months(date_a, -7*12)
                                 when 'B' then            date_b
                                 end
                        ) virtual,
  data            varchar2(100) 
)
partition   by range  (date_too_old) 
( 
  partition p_before_2000_10 values less than (date '2000-10-01'),
  partition p_before_2000_11 values less than (date '2000-11-01'),
  partition p_before_2000_12 values less than (date '2000-12-01'),
  --
  partition p_before_2001_01 values less than (date '2001-01-01'),
  partition p_before_2001_02 values less than (date '2001-02-01'),
  partition p_before_2001_03 values less than (date '2001-03-01'),
  partition p_before_2001_04 values less than (date '2001-04-01'),
  partition p_before_2001_05 values less than (date '2001-05-01'),
  partition p_before_2001_06 values less than (date '2001-06-01'),
  -- and so on and so forth..
  partition p_ values less than (maxvalue)
);

insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '2008-04-14', date '2000-05-17', 
 'B and 2000-05-17 is older than 10 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('B', date '1999-09-19', date '2004-02-12', 
 'B and 2004-02-12 is younger than 10 yrs, must be kept');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2000-06-16', date '2010-01-01', 
 'A and 2000-06-16 is older than 3 yrs, must be deleted');


insert into tq84_partitioned_table (status, date_a, date_b, data) values 
('A', date '2009-06-09', date '1999-08-28', 
 'A and 2009-06-09 is younger than 3 yrs, must be kept');

select * from tq84_partitioned_table order by date_too_old;

-- drop partitions older than 10 or 3 years, respectively:

alter table tq84_partitioned_table drop partition p_before_2000_10;
alter table tq84_partitioned_table drop partition p_before_2000_11;
alter table tq84_partitioned_table drop partition p2000_12;

select * from tq84_partitioned_table order by date_too_old;
René Nyffenegger
sumber
Saya mungkin telah menyederhanakan logika di balik bagaimana catatan untuk dibersihkan ditentukan, tetapi ini adalah ide yang sangat menarik. Namun, satu hal yang harus diperhatikan adalah kinerja sehari-hari. Membersihkan adalah "masalah kita", klien tidak akan menerima kinerja terdegradasi hanya untuk menyelesaikannya. Kedengarannya, dari beberapa komentar dan jawaban Gary bahwa ini mungkin masalah dengan partisi?
Coding Gorilla
Saya tidak yakin apakah ini jawaban yang kami cari, tetapi ini jelas merupakan pendekatan yang sangat menarik yang akan kami selidiki.
Pengkodean Gorilla
14

Solusi klasik untuk ini adalah untuk mempartisi tabel Anda, misalnya berdasarkan bulan atau minggu. Jika Anda belum pernah melihatnya, tabel partisi seperti beberapa tabel terstruktur identik dengan implisit UNIONsaat memilih, dan Oracle akan secara otomatis menyimpan baris di partisi yang sesuai saat memasukkannya berdasarkan kriteria partisi. Anda menyebutkan indeks - baik setiap partisi mendapatkan indeks dipartisi sendiri juga. Ini adalah operasi yang sangat murah di Oracle untuk menjatuhkan partisi (analog dengan aTRUNCATEdalam hal memuat karena itulah yang benar-benar Anda lakukan - memotong atau menjatuhkan salah satu dari sub-tabel yang tidak terlihat ini). Ini akan menjadi jumlah yang signifikan dari pemrosesan untuk dipartisi "setelah fakta", tetapi tidak ada gunanya menangisi susu yang tumpah - keuntungan untuk melakukannya sejauh ini lebih besar daripada biayanya. Setiap bulan Anda akan membagi partisi atas untuk membuat partisi baru untuk data bulan berikutnya (Anda dapat dengan mudah mengotomatisasikannya dengan a DBMS_JOB).

Dan dengan partisi Anda juga dapat mengeksploitasi kueri paralel dan penghapusan partisi , yang seharusnya membuat pengguna Anda sangat senang ...

Gayus
sumber
FWIW kami menggunakan teknik ini di situs saya pada basis data 30Tb +
Gayus
Masalah dengan mempartisi adalah tidak ada cara yang jelas untuk mempartisi data. Dalam salah satu dari dua tabel (bukan yang ditunjukkan di bawah) kriteria yang digunakan untuk melakukan pembersihan didasarkan pada dua bidang tanggal yang berbeda (dan berbeda), dan bidang status. Misalnya, jika statusnya Amaka jika DateAlebih dari 3 tahun, maka akan dihapus. Jika Status adalah Bdan DateBlebih tua dari 10 tahun, hal itu akan dibersihkan. Jika pemahaman saya tentang partisi benar, maka partisi tidak akan berguna dalam situasi seperti ini (setidaknya sejauh menyangkut pembersihan).
Pengkodean Gorilla
Anda dapat mempartisi berdasarkan status dan subpartisi berdasarkan rentang tanggal. Tetapi jika status (atau tanggal) berubah, itu benar-benar menghapus dari satu sub-partisi dan memasukkan ke yang lain. Singkatnya Anda bisa mendapatkan hit pada proses sehari-hari Anda untuk menghemat waktu pada pembersihan Anda.
Gary
6
Atau Anda bisa membuat kolom virtual yang menampilkan DateA ketika status A dan DateB ketika status B dan kemudian partisi pada kolom virtual. Migrasi partisi yang sama akan terjadi, tetapi itu akan membantu membersihkan Anda. Sepertinya ini sudah diposting sebagai jawaban.
Leigh Riffel
4

Satu aspek yang perlu dipertimbangkan adalah berapa banyak hasil kinerja penghapusan dari indeks dan berapa banyak dari tabel mentah. Setiap catatan yang dihapus dari tabel membutuhkan penghapusan baris yang sama dari setiap indeks btree. Jika Anda memiliki indeks 30+ btree, saya menduga sebagian besar waktu Anda dihabiskan untuk pemeliharaan indeks.

Ini berdampak pada kegunaan partisi. Katakanlah Anda memiliki indeks atas nama. Indeks Btree standar, semua dalam satu segmen, mungkin harus melakukan empat lompatan untuk mendapatkan dari blok root ke blok daun dan yang kelima membaca untuk mendapatkan baris. Jika indeks itu dipartisi menjadi 50 segmen dan Anda tidak memiliki kunci partisi sebagai bagian dari kueri, maka masing-masing dari 50 segmen tersebut perlu diperiksa. Setiap segmen akan lebih kecil, jadi Anda mungkin hanya harus melakukan 2 lompatan tetapi Anda mungkin masih akan selesai membaca 100 kali daripada yang sebelumnya 5.

Jika mereka adalah indeks bitmap, persamaannya berbeda. Anda mungkin tidak menggunakan indeks untuk mengidentifikasi baris individual, melainkan mengaturnya. Jadi, alih-alih permintaan menggunakan 5 IO untuk mengembalikan satu catatan, itu menggunakan 10.000 IO. Karenanya overhead tambahan di partisi ekstra untuk indeks tidak akan menjadi masalah.

Gary
sumber
2

penghapusan 50 juta catatan per bulan dalam batch 50.000 hanya 1000 iterasi. jika Anda melakukan 1 hapus setiap 30 menit itu harus memenuhi kebutuhan Anda. tugas terjadwal untuk menjalankan kueri yang Anda poskan tetapi menghapus loop sehingga hanya dijalankan sekali seharusnya tidak menyebabkan penurunan yang nyata bagi pengguna. Kami melakukan volume rekaman yang sama di pabrik kami yang beroperasi hampir 24/7 dan memenuhi kebutuhan kami. Kami benar-benar menyebarkan 10.000 catatan lebih sedikit setiap 10 menit, yang dijalankan dalam sekitar 1 atau 2 detik berjalan pada server Oracle unix kami.

Jason Jakob
sumber
Bagaimana dengan 'undo' dan 'redo' 'delete' besar yang akan dihasilkan? Ini mencekik IO juga ... pendekatan berbasis 'hapus' pastilah TIDAK. TIDAK untuk tabel besar.
pahariayogi
1

Jika ruang disk tidak pada premium, Anda bisa dapat membuat "tabel" salinan tabel, katakanlah my_table_new, menggunakan CTAS (Buat Tabel Sebagai Pilih) dengan kriteria yang akan menghilangkan catatan yang akan dihapus. Anda dapat melakukan pernyataan buat secara paralel, dan dengan petunjuk tambahkan untuk membuatnya cepat, lalu buat semua indeks Anda. Kemudian, setelah selesai, (dan diuji), ubah nama tabel yang ada menjadi my_table_olddan ubah nama tabel "work" menjadi my_table. Setelah Anda merasa nyaman dengan semuanya drop my_table_old purgeuntuk menyingkirkan meja lama. Jika ada banyak batasan kunci asing, lihat dbms_redefinition paket PL / SQL . Ini akan mengkloning indeks Anda, kendala, dll. Saat menggunakan opsi yang sesuai. Ini adalah ringkasan dari saran Tom Kyte dari AskTomketenaran. Setelah menjalankan pertama, Anda dapat mengotomatiskan semuanya, dan tabel buat harus berjalan lebih cepat, dan dapat dilakukan saat sistem dinyalakan, dan waktu henti aplikasi akan dibatasi hingga kurang dari satu menit untuk melakukan penggantian nama tabel. Menggunakan CTAS akan jauh lebih cepat daripada melakukan beberapa penghapusan batch. Pendekatan ini bisa sangat berguna jika Anda tidak memiliki partisi berlisensi.

Sampel CTAS, menjaga baris dengan data dari 365 hari terakhir dan flag_inactive = 'N':

create /*+ append */ table my_table_new 
   tablespace data as
   select /*+ parallel */ * from my_table 
       where some_date >= sysdate -365 
       and flag_inactive = 'N';

-- test out my_table_new. then if all is well:

alter table my_table rename to my_table_old;
alter table my_table_new rename to my_table;
-- test some more
drop table my_table_old purge;
Mark Stewart
sumber
1
Ini dapat dipertimbangkan jika (a) membersihkan adalah tugas satu kali. (B) jika Anda lebih sedikit baris untuk mempertahankan dan sebagian besar data untuk menghapus ...
pahariayogi
0

ketika menjatuhkan partisi, Anda meninggalkan indeks global tidak dapat digunakan, yang perlu dibangun kembali, pembangunan kembali indeks global akan menjadi masalah besar, karena jika Anda melakukannya secara online, itu akan sangat lambat, jika tidak, Anda perlu downtime. dalam kedua kasus, tidak dapat memenuhi persyaratan.

"Kami biasanya membersihkan antara 10 dan 50 juta baris per bulan"

Saya akan merekomendasikan menggunakan PL / SQL batch delete, beberapa jam ok saya pikir.

iceburge5
sumber
1
Jika Anda memiliki kunci utama, menjatuhkan partisi seharusnya tidak membuat indeks global tidak dapat digunakan. Tetapi jika OP memiliki banyak indeks global akan ada biaya tinggi untuk menjatuhkan partisi. Dalam kasus ideal ketika seseorang mempartisi tabel, partisi tersebut didasarkan pada kunci utama dan mereka tidak memerlukan indeks global. Bahwa setiap kueri dapat memanfaatkan pemangkasan partisi.
Gandolf989
@ Gandolf989 menjatuhkan partisi, akan selalu membuat indeks global tidak dapat digunakan
miracle173