Men vad händer om en sådan obehaglig typ (långvarig OLAP-belastning på en OLTP-databas) fortfarande existerar? Hur rent aktivt skötbord omgiven av långa frågor och inte trampa på en rake?
Lägger ut rakan
Låt oss först avgöra vad problemet vi vill lösa är och hur det kan uppstå.
Vanligtvis inträffar denna situation på ett relativt litet bord, men där det förekommer många förändringar. Vanligtvis detta eller annorlunda mätare/aggregat/klasser, där UPDATE ofta körs, eller buffertkö att bearbeta någon ständigt pågående ström av händelser, vars register ständigt INSERT/DELETE.
Låt oss försöka återskapa alternativet med betyg:
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;
Och parallellt, i ett annat sammanhang, startar en lång, lång förfrågan, som samlar in lite komplex statistik, men påverkar inte vårt bord:
SELECT pg_sleep(10000);
Nu uppdaterar vi värdet på en av räknarna många, många gånger. För experimentets renhet, låt oss göra detta
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
Vad hände? Varför ens för den enklaste UPPDATERING av en enda post exekveringstiden minskade med 7 gånger — från 0.524ms till 3.808ms? Och vårt betyg bygger allt långsammare.
Allt är MVCC:s fel.
Allt handlar om
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
Åh, det finns inget att rengöra! Parallell Den löpande begäran stör oss – trots allt kanske han någon gång vill vända sig till dessa versioner (tänk om?), och de borde vara tillgängliga för honom. Och därför kommer inte ens VAKUUM FULL hjälpa oss.
"Komprimerar" bordet
Men vi vet med säkerhet att den frågan inte behöver vår tabell. Därför kommer vi fortfarande att försöka återställa systemets prestanda till adekvata gränser genom att eliminera allt onödigt från tabellen - åtminstone "manuellt", eftersom VACUUM ger efter.
För att göra det mer tydligt, låt oss titta på exemplet med fallet med en bufferttabell. Det vill säga att det är ett stort flöde av INSERT/DELETE, och ibland är tabellen helt tom. Men om det inte är tomt så måste vi spara dess nuvarande innehåll.
#0: Bedömer situationen
Det är uppenbart att du kan försöka göra något med tabellen även efter varje operation, men detta är inte så vettigt - underhållskostnaderna kommer helt klart att vara större än genomströmningen av målfrågorna.
Låt oss formulera kriterierna - "det är dags att agera" om:
- VACUUM lanserades för ganska länge sedan
Vi förväntar oss en tung belastning, så låt det vara 60 sekunder sedan senaste [auto]VAKUUM. - fysisk tabellstorlek är större än målet
Låt oss definiera det som dubbelt så många sidor (8KB block) i förhållande till minimistorleken - 1 blk för hög + 1 blk för varje index - för ett potentiellt tomt bord. Om vi förväntar oss att en viss mängd data alltid kommer att finnas kvar i bufferten "normalt", är det rimligt att justera denna formel.
Begäran om verifiering
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: Fortfarande VAKUUM
Vi kan inte i förväg veta om en parallell fråga stör oss avsevärt - exakt hur många poster som har blivit "inaktuella" sedan den började. Därför, när vi bestämmer oss för att på något sätt bearbeta tabellen, bör vi i alla fall först köra på den VAKUUM - till skillnad från VACUUM FULL stör den inte parallella processer som arbetar med läs-skrivdata.
Samtidigt kan den direkt rensa bort det mesta vi skulle vilja ta bort. Ja, och efterföljande frågor i den här tabellen kommer att gå till oss av "hot cache", vilket kommer att minska deras varaktighet - och därmed den totala tiden för att blockera andra av vår servicetransaktion.
#2: Är någon hemma?
Låt oss kolla om det överhuvudtaget finns något i tabellen:
TABLE tbl LIMIT 1;
Om det inte finns en enda post kvar kan vi spara mycket på bearbetningen genom att helt enkelt göra
Det fungerar på samma sätt som ett ovillkorligt DELETE-kommando för varje tabell, men är mycket snabbare eftersom det faktiskt inte skannar tabellerna. Dessutom frigör det omedelbart diskutrymme, så det finns inget behov av att utföra en VAKUUM-operation efteråt.
Om du behöver återställa tabellsekvensräknaren (RESTART IDENTITY) är upp till dig att bestämma.
#3: Alla – turas om!
Eftersom vi arbetar i en mycket konkurrensutsatt miljö, medan vi är här och kontrollerar att det inte finns några poster i tabellen, kan någon redan ha skrivit något där. Vi bör inte förlora denna information, så vad? Det stämmer, vi måste se till att ingen säkert kan skriva ner det.
För att göra detta måste vi aktivera SERIALISERBAR-isolering för vår transaktion (ja, här startar vi en transaktion) och lås bordet "tätt":
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE;
Denna nivå av blockering bestäms av de operationer som vi vill utföra på den.
#4: Intressekonflikt
Vi kommer hit och vill "låsa" skylten - tänk om någon var aktiv på den i det ögonblicket, till exempel läste från den? Vi kommer att "hänga" och vänta på att detta block ska släppas, och andra som vill läsa kommer att stöta på oss...
För att förhindra att detta händer kommer vi att "offra oss själva" - om vi inte kunde få ett lås inom en viss (acceptabelt kort) tid, kommer vi att få ett undantag från basen, men vi kommer åtminstone inte att störa för mycket med andra.
För att göra detta, ställ in sessionsvariabeln
SET statement_timeout = ...;LOCK TABLE ...;
För att inte behöva ta itu med att återställa det "gamla" värdet på variabeln senare använder vi formuläret STÄLL LOKALT, vilket begränsar omfattningen av inställningen till den aktuella transaktionen.
Vi kommer ihåg att statement_timeout gäller för alla efterföljande förfrågningar så att transaktionen inte kan sträcka sig till oacceptabla värden om det finns mycket data i tabellen.
#5: Kopiera data
Om tabellen inte är helt tom måste data sparas på nytt med hjälp av en tillfällig extra tabell:
CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl;
Signatur PÅ ÅTAGANDE DROP betyder att i det ögonblick som transaktionen avslutas kommer den temporära tabellen att upphöra att existera, och det finns inget behov av att manuellt radera den i anslutningssammanhanget.
Eftersom vi antar att det inte finns mycket "live" data, bör denna operation ske ganska snabbt.
Tja, det är allt! Glöm inte efter att transaktionen har slutförts
Att sätta ihop det sista manuset
Vi använder denna "pseudo-python":
# собираем статистику с таблицы
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;
Är det möjligt att inte kopiera uppgifterna en andra gång?I princip är det möjligt om själva bordets oid inte är bunden till några andra aktiviteter från BL-sidan eller FK från DB-sidan:
CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL);
INSERT INTO _swap_%table TABLE %table;
DROP TABLE %table;
ALTER TABLE _swap_%table RENAME TO %table;
Låt oss köra skriptet på källtabellen och kontrollera mätvärdena:
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
Allt löste sig! Tabellen har krympt med 50 gånger och alla UPPDATERINGAR går snabbt igen.
Källa: will.com