Namun bagaimana jika tipe yang tidak menyenangkan tersebut (pemuatan OLAP jangka panjang pada database OLTP) masih ada? Bagaimana membersihkan meja ganti secara aktif dikelilingi oleh pertanyaan panjang dan tidak menginjak penggaruk?
Meletakkan penggaruk
Pertama, mari kita tentukan masalah apa yang ingin kita pecahkan dan bagaimana masalah itu bisa timbul.
Biasanya situasi ini terjadi di atas meja yang relatif kecil, tapi di mana hal itu terjadi banyak perubahan. Biasanya ini atau berbeda meter/agregat/peringkat, di mana UPDATE sering dijalankan, atau antrian penyangga untuk memproses beberapa aliran peristiwa yang terus-menerus berlangsung, catatannya terus-menerus INSERT/DELETE.
Mari kita coba mereproduksi opsi dengan peringkat:
CREATE TABLE tbl(k text PRIMARY KEY, v integer);
CREATE INDEX ON tbl(v DESC); -- по этому индексу будем строить рейтинг
INSERT INTO
tbl
SELECT
chr(ascii('a'::text) + i) k
, 0 v
FROM
generate_series(0, 25) i;
Dan secara paralel, di koneksi lain, permintaan yang sangat panjang dimulai, mengumpulkan beberapa statistik rumit, tapi tidak mempengaruhi meja kami:
SELECT pg_sleep(10000);
Sekarang kami memperbarui nilai salah satu penghitung berkali-kali. Demi kemurnian eksperimen, mari kita lakukan ini
DO $$
DECLARE
i integer;
tsb timestamp;
tse timestamp;
d double precision;
BEGIN
PERFORM dblink_connect('dbname=' || current_database() || ' port=' || current_setting('port'));
FOR i IN 1..10000 LOOP
tsb = clock_timestamp();
PERFORM dblink($e$UPDATE tbl SET v = v + 1 WHERE k = 'a';$e$);
tse = clock_timestamp();
IF i % 1000 = 0 THEN
d = (extract('epoch' from tse) - extract('epoch' from tsb)) * 1000;
RAISE NOTICE 'i = %, exectime = %', lpad(i::text, 5), lpad(d::text, 5);
END IF;
END LOOP;
PERFORM dblink_disconnect();
END;
$$ LANGUAGE plpgsql;
NOTICE: i = 1000, exectime = 0.524
NOTICE: i = 2000, exectime = 0.739
NOTICE: i = 3000, exectime = 1.188
NOTICE: i = 4000, exectime = 2.508
NOTICE: i = 5000, exectime = 1.791
NOTICE: i = 6000, exectime = 2.658
NOTICE: i = 7000, exectime = 2.318
NOTICE: i = 8000, exectime = 2.572
NOTICE: i = 9000, exectime = 2.929
NOTICE: i = 10000, exectime = 3.808
Apa yang telah terjadi? Mengapa bahkan untuk UPDATE yang paling sederhana dari satu catatan waktu eksekusi menurun 7 kali — dari 0.524 md hingga 3.808 md? Dan peringkat kami meningkat semakin lambat.
Itu semua salah MVCC.
Semua tentang
VACUUM VERBOSE tbl;
INFO: vacuuming "public.tbl"
INFO: "tbl": found 0 removable, 10026 nonremovable row versions in 45 out of 45 pages
DETAIL: 10000 dead row versions cannot be removed yet, oldest xmin: 597439602
Oh, tidak ada yang perlu dibersihkan! Paralel Permintaan yang berjalan mengganggu kami - lagi pula, suatu hari nanti dia mungkin ingin beralih ke versi ini (bagaimana jika?), dan versi tersebut harus tersedia untuknya. Dan oleh karena itu bahkan VACUUM FULL tidak akan membantu kita.
“Meruntuhkan” meja
Namun kami tahu pasti bahwa kueri itu tidak memerlukan tabel kami. Oleh karena itu, kami akan tetap mencoba mengembalikan kinerja sistem ke batas yang memadai dengan menghilangkan semua yang tidak perlu dari tabel - setidaknya “secara manual”, karena VACUUM menyerah.
Agar lebih jelas mari kita lihat contoh kasus tabel buffer. Artinya, ada aliran INSERT/DELETE yang besar, dan terkadang tabel benar-benar kosong. Tapi kalau tidak kosong, kita harus menyimpan isinya saat ini.
#0: Menilai situasi
Jelas bahwa Anda dapat mencoba melakukan sesuatu dengan tabel bahkan setelah setiap operasi, tetapi ini tidak masuk akal - overhead pemeliharaan jelas akan lebih besar daripada throughput kueri target.
Mari kita rumuskan kriterianya - “saatnya bertindak” jika:
- VACUUM diluncurkan cukup lama
Kami mengharapkan beban yang berat, biarlah 60 detik sejak VAKUM [otomatis] terakhir. - ukuran tabel fisik lebih besar dari target
Mari kita definisikan sebagai dua kali jumlah halaman (blok 8KB) dibandingkan dengan ukuran minimum - 1 blk untuk heap + 1 blk untuk setiap indeks - untuk tabel yang berpotensi kosong. Jika kita berharap bahwa sejumlah data tertentu akan selalu berada di buffer “secara normal”, maka masuk akal untuk mengubah rumus ini.
Permintaan verifikasi
SELECT
relpages
, ((
SELECT
count(*)
FROM
pg_index
WHERE
indrelid = cl.oid
) + 1) << 13 size_norm -- тут правильнее делать * current_setting('block_size')::bigint, но кто меняет размер блока?..
, pg_total_relation_size(oid) size
, coalesce(extract('epoch' from (now() - greatest(
pg_stat_get_last_vacuum_time(oid)
, pg_stat_get_last_autovacuum_time(oid)
))), 1 << 30) vaclag
FROM
pg_class cl
WHERE
oid = $1::regclass -- tbl
LIMIT 1;
relpages | size_norm | size | vaclag
-------------------------------------------
0 | 24576 | 1105920 | 3392.484835
#1: Masih VAKUM
Kami tidak dapat mengetahui sebelumnya apakah kueri paralel mengganggu kami secara signifikan - tepatnya berapa banyak catatan yang menjadi "kedaluwarsa" sejak kueri tersebut dimulai. Oleh karena itu, ketika kita memutuskan untuk memproses tabel, bagaimanapun juga, kita harus mengeksekusinya terlebih dahulu KEKOSONGAN - tidak seperti VACUUM FULL, ini tidak mengganggu proses paralel yang bekerja dengan data baca-tulis.
Pada saat yang sama, ia dapat segera membersihkan sebagian besar dari apa yang ingin kita hapus. Ya, dan pertanyaan selanjutnya pada tabel ini akan dikirimkan kepada kami dengan "cache panas", yang akan mengurangi durasinya - dan, oleh karena itu, total waktu pemblokiran orang lain oleh transaksi layanan kami.
#2: Apakah ada orang di rumah?
Mari kita periksa apakah ada sesuatu di tabel:
TABLE tbl LIMIT 1;
Jika tidak ada satu catatan pun yang tersisa, maka kita dapat menghemat banyak pemrosesan hanya dengan melakukan
Tindakannya sama seperti perintah DELETE tanpa syarat untuk setiap tabel, namun jauh lebih cepat karena tidak benar-benar memindai tabel. Terlebih lagi, ini segera mengosongkan ruang disk, sehingga tidak perlu melakukan operasi VACUUM setelahnya.
Apakah Anda perlu mengatur ulang penghitung urutan tabel (RESTART IDENTITY) terserah Anda.
#3: Semuanya - bergiliran!
Karena kami bekerja di lingkungan yang sangat kompetitif, sementara kami di sini memeriksa bahwa tidak ada entri dalam tabel, seseorang mungkin sudah menulis sesuatu di sana. Kita tidak boleh kehilangan informasi ini, lalu kenapa? Benar sekali, kita perlu memastikan tidak ada orang yang bisa menuliskannya secara pasti.
Untuk melakukan ini kita perlu mengaktifkan SERIALISASI-isolasi untuk transaksi kita (ya, di sini kita memulai transaksi) dan mengunci meja “rapat”:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
Tingkat pemblokiran ini ditentukan oleh operasi yang ingin kita lakukan padanya.
#4: Konflik kepentingan
Kami datang ke sini dan ingin “mengunci” tanda tersebut - bagaimana jika seseorang sedang aktif pada tanda tersebut pada saat itu, misalnya, membacanya? Kami akan "menggantung" menunggu blok ini dilepaskan, dan orang lain yang ingin membaca akan menemui kami...
Untuk mencegah hal ini terjadi, kami akan "mengorbankan diri kami sendiri" - jika kami tidak dapat memperoleh kunci dalam waktu tertentu (yang cukup singkat), maka kami akan menerima pengecualian dari pangkalan, tetapi setidaknya kami tidak akan terlalu banyak ikut campur dalam hal ini. yang lain.
Untuk melakukan ini, atur variabel sesi
SET statement_timeout = ...;LOCK TABLE ...;
Agar tidak harus berurusan dengan pemulihan nilai variabel yang "lama" nanti, kami menggunakan formulir ATUR LOKAL, yang membatasi cakupan pengaturan pada transaksi saat ini.
Kita ingat bahwa statement_timeout berlaku untuk semua permintaan berikutnya sehingga transaksi tidak dapat mencapai nilai yang tidak dapat diterima jika ada banyak data dalam tabel.
#5: Salin data
Jika tabel tidak sepenuhnya kosong, data harus disimpan ulang menggunakan tabel sementara tambahan:
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
Tanda tangan PADA KOMITMEN DROP berarti bahwa pada saat transaksi berakhir, tabel sementara tidak akan ada lagi, dan tidak perlu menghapusnya secara manual dalam konteks koneksi.
Karena kami berasumsi bahwa tidak banyak data "langsung", operasi ini akan dilakukan dengan cukup cepat.
Ya, itu saja! Jangan lupa setelah selesai transaksi
Menyusun naskah akhir
Kami menggunakan "python semu" ini:
# собираем статистику с таблицы
stat <-
SELECT
relpages
, ((
SELECT
count(*)
FROM
pg_index
WHERE
indrelid = cl.oid
) + 1) << 13 size_norm
, pg_total_relation_size(oid) size
, coalesce(extract('epoch' from (now() - greatest(
pg_stat_get_last_vacuum_time(oid)
, pg_stat_get_last_autovacuum_time(oid)
))), 1 << 30) vaclag
FROM
pg_class cl
WHERE
oid = $1::regclass -- table_name
LIMIT 1;
# таблица больше целевого размера и VACUUM был давно
if stat.size > 2 * stat.size_norm and stat.vaclag is None or stat.vaclag > 60:
-> VACUUM %table;
try:
-> BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# пытаемся захватить монопольную блокировку с предельным временем ожидания 1s
-> SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s';
-> LOCK TABLE %table IN ACCESS EXCLUSIVE MODE;
# надо убедиться в пустоте таблицы внутри транзакции с блокировкой
row <- TABLE %table LIMIT 1;
# если в таблице нет ни одной "живой" записи - очищаем ее полностью, в противном случае - "перевставляем" все записи через временную таблицу
if row is None:
-> TRUNCATE TABLE %table RESTART IDENTITY;
else:
# создаем временную таблицу с данными таблицы-оригинала
-> CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE %table;
# очищаем оригинал без сброса последовательности
-> TRUNCATE TABLE %table;
# вставляем все сохраненные во временной таблице данные обратно
-> INSERT INTO %table TABLE _tmp_swap;
-> COMMIT;
except Exception as e:
# если мы получили ошибку, но соединение все еще "живо" - словили таймаут
if not isinstance(e, InterfaceError):
-> ROLLBACK;
Apakah mungkin untuk tidak menyalin data untuk kedua kalinya?Pada prinsipnya dimungkinkan jika oid tabel itu sendiri tidak terikat dengan kegiatan lain dari pihak BL atau FK dari pihak DB:
CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;
Mari jalankan skrip pada tabel sumber dan periksa metriknya:
VACUUM tbl;
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s';
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
TRUNCATE TABLE tbl;
INSERT INTO tbl TABLE _tmp_swap;
COMMIT;
relpages | size_norm | size | vaclag
-------------------------------------------
0 | 24576 | 49152 | 32.705771
Semuanya berhasil! Tabel telah menyusut 50 kali lipat dan semua UPDATE berjalan cepat kembali.
Sumber: www.habr.com