przechowywanie historii zmian

zapewne każdy kto choć raz robił coś ciut większego w bazach danych zetknął się z problemem przechowywania zmian w tabelkach.

ci co nie mają triggerów, muszą używać kodu po stronie aplikacji klienckiej. co ma swoje wady.

z drugiej strony – w postgresie triggery są. i działają 🙂 więc można do roboty zaprząc postgresa.

załóżmy, że mamy bardzo prostą tabelkę z danymi:

CREATE TABLE objects (
id BIGSERIAL,
o_type TEXT NOT NULL DEFAULT '',
PRIMARY KEY (id)
);
chcemy zapisywać sobie modyfikacje. robimy więc tabelkę na zapis historii zmian:
CREATE TABLE history_objects (
id BIGSERIAL,
modified_on TIMESTAMP NOT NULL DEFAULT now(),
modification_type TEXT NOT NULL DEFAULT '',
modified_id INT8,
modified_o_type TEXT,
PRIMARY KEY (id)
);

rekordy w history_object zawierają:

  • modified_on – kiedy dana modyfikacja miała miejsce
  • modification_type – typ modyfikacji – insert, update czy delete.
  • modified_id – id zmodyfikowanego rekordu
  • modified_o_type – o_type w tym rekordzie

proste i miłe. tworzymy więc dwa triggery:

CREATE OR REPLACE FUNCTION trg_objects_ui() RETURNS TRIGGER AS
$BODY$
DECLARE
BEGIN
INSERT INTO history_objects (modified_id, modified_o_type, modification_type) VALUES (NEW.id, NEW.o_type, TG_OP);
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trg_objects_ui AFTER INSERT OR UPDATE ON objects FOR EACH ROW EXECUTE PROCEDURE trg_objects_ui();
 
CREATE OR REPLACE FUNCTION trg_objects_d() RETURNS TRIGGER AS
$BODY$
DECLARE
BEGIN
INSERT INTO history_objects (modified_id, modified_o_type, modification_type) VALUES (OLD.id, OLD.o_type, TG_OP);
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trg_objects_d AFTER DELETE ON objects FOR EACH ROW EXECUTE PROCEDURE trg_objects_d();i testujemy:

testujemy:

> select * from objects;
id | o_type
----+--------
(0 rows)
 
> select * from history_objects;
id | modified_on | modification_type | modified_id | modified_o_type
----+-------------+-------------------+-------------+-----------------
(0 rows)
 
> INSERT INTO objects (o_type) VALUES ('bleble');
INSERT 0 1
 
> INSERT INTO objects (o_type) VALUES ('bleble2');
INSERT 0 1
 
> UPDATE objects SET o_type = 'xxx' WHERE o_type = 'bleble';
UPDATE 1
 
> DELETE FROM objects WHERE o_type = 'bleble2';
DELETE 1
 
> select * from objects;
id | o_type
----+--------
1 | xxx
(1 row)
 
> select * from history_objects;
id |        modified_on         | modification_type | modified_id | modified_o_type
----+----------------------------+-------------------+-------------+-----------------
1 | 2006-07-15 12:14:56.138695 | INSERT            |           1 | bleble
2 | 2006-07-15 12:14:56.138695 | INSERT            |           2 | bleble2
3 | 2006-07-15 12:14:56.138695 | UPDATE            |           1 | xxx
4 | 2006-07-15 12:14:56.138695 | DELETE            |           2 | bleble2
(4 rows)

działa ślicznie.

ale jest jedno ale.

czasem chcielibyśmy logować więcej informacji. w projekcie nad którym pracują koledzy w firmy chcieli logować nazwę akcji webowej (coś jakby url) i nazwę użytkownika które spowodowały taką modyfikację.

tu pojawiają się schody – baza danych nie zna urla. baza danych nie zna nazwy użytkownika który się zalogował na www – wie tylko jaki user jest zalogowany do bazy danych, ale to jest zazwyczaj zawsze jeden i ten sam user – niezależnie od tego na kogo się użytkownik loguje via www.

hmm. trzeba więc logować z poziomu aplikacji, a nie bazy.

no tak, ale jeśli user w jednej akcji powoduje modyfikacje wielu rekordów czy wielu tabel – sprawa zaczyna się komplikować.

na szczęście jest rozwiązanie.

wystarczy założyć dodatkową tabelkę do której będziemy dodawać nowy rekord przy rozpoczynaniu procedury obsługi każdego requestu, i będziemy tam wpisywać url'a i nazwę usera. a potem tylko baza musi umieć z tego skorzystać.

aby baza mogła powiązać dane z tej nowej tabelki (nazwijmy ją sobie “actions"), musi być w stanie jakoś powiązać późniejsze insert'y, update'y i delete'y z wpisami w actions. najlepiej to zrobić używając pidu (numer identyfikacyjny procesu) backendu postgresa.

w postgresie jest funkcja która zwraca ten pid:

select pg_backend_pid();

dodajmy więc tabelkę actions, od razu z indeksem na to po czym będziemy szukać:

CREATE TABLE actions (
id BIGSERIAL,
backend_pid INT4 NOT NULL DEFAULT pg_backend_pid(),
action_on TIMESTAMP NOT NULL DEFAULT now(),
action_url TEXT,
action_user TEXT,
PRIMARY KEY (id)
);
CREATE INDEX ui_actions_bpao ON actions (backend_pid, action_on);

tabelkę history_objects modyfikujemy tak aby zawierała pola na urla i username (zamiast tego można dodać action_id, ale dodanie całych pól daje nam dodatkowe możliwości):

CREATE TABLE history_objects (
id BIGSERIAL,
modified_on TIMESTAMP NOT NULL DEFAULT now(),
modification_type TEXT NOT NULL DEFAULT '',
modification_url TEXT,
modification_user TEXT,
modified_id INT8,
modified_o_type TEXT,
PRIMARY KEY (id)
);

pozostało zmodyfikować triggery:

CREATE OR REPLACE FUNCTION trg_objects_ui() RETURNS TRIGGER AS
$BODY$
DECLARE
temprec actions%ROWTYPE;
BEGIN
SELECT * INTO temprec FROM actions WHERE backend_pid = pg_backend_pid() ORDER BY backend_pid desc, action_on desc LIMIT 1;
IF NOT FOUND THEN
temprec.action_url := 'unknown url. direct database access?';
temprec.action_user := 'database user: ' || CURRENT_USER;
END IF;
INSERT INTO history_objects (modified_id, modified_o_type, modification_type, modification_url, modification_user) VALUES (NEW.id, NEW.o_type, TG_OP, temprec.action_url, temprec.action_user);
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trg_objects_ui AFTER INSERT OR UPDATE ON objects FOR EACH ROW EXECUTE PROCEDURE trg_objects_ui();
 
CREATE OR REPLACE FUNCTION trg_objects_d() RETURNS TRIGGER AS
$BODY$
DECLARE
temprec actions%ROWTYPE;
BEGIN
SELECT * INTO temprec FROM actions WHERE backend_pid = pg_backend_pid() ORDER BY backend_pid desc, action_on desc LIMIT 1;
IF NOT FOUND THEN
temprec.action_url := 'unknown url. direct database access?';
temprec.action_user := 'database user: ' || CURRENT_USER;
END IF;
INSERT INTO history_objects (modified_id, modified_o_type, modification_type, modification_url, modification_user) VALUES (OLD.id, OLD.o_type, TG_OP, temprec.action_url, temprec.action_user);
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trg_objects_d AFTER DELETE ON objects FOR EACH ROW EXECUTE PROCEDURE trg_objects_d();

no to pora na test. najpierw zobaczymy jak to działa gdy nie zapiszemy nic do actions:

> select * from objects;
id | o_type
----+--------
(0 rows)
 
> select * from history_objects;
id | modified_on | modification_type | modification_url | modification_user | modified_id | modified_o_type
----+-------------+-------------------+------------------+-------------------+-------------+-----------------
(0 rows)
 
> select * from actions;
id | backend_pid | action_on | action_url | action_user
----+-------------+-----------+------------+-------------
(0 rows)
 
> INSERT INTO objects (o_type) VALUES ('bleble');
INSERT 0 1
 
> INSERT INTO objects (o_type) VALUES ('bleble2');
INSERT 0 1
 
> UPDATE objects SET o_type = 'xxx' WHERE o_type = 'bleble';
UPDATE 1
 
> DELETE FROM objects WHERE o_type = 'bleble2';
DELETE 1
 
> select * from objects;
id | o_type
----+--------
1 | xxx
(1 row)
 
> select * from history_objects;
id |        modified_on         | modification_type |           modification_url           |   modification_user   | modified_id | modified_o_type
----+----------------------------+-------------------+--------------------------------------+-----------------------+-------------+-----------------
1 | 2006-07-15 12:58:54.16915  | INSERT            | unknown url. direct database access? | database user: depesz |           1 | bleble
2 | 2006-07-15 12:58:58.46624  | INSERT            | unknown url. direct database access? | database user: depesz |           2 | bleble2
3 | 2006-07-15 12:59:04.454578 | UPDATE            | unknown url. direct database access? | database user: depesz |           1 | xxx
4 | 2006-07-15 12:59:09.519242 | DELETE            | unknown url. direct database access? | database user: depesz |           2 | bleble2
(4 rows)

działa ładnie. to teraz zapiszmy coś do actions i powtórzmy test:

> insert into actions (action_url, action_user) values ('/jakis_url.html', 'admin');
INSERT 0 1
 
> INSERT INTO objects (o_type) VALUES ('A:bleble');
INSERT 0 1
 
> INSERT INTO objects (o_type) VALUES ('A:bleble2');
INSERT 0 1
 
> UPDATE objects SET o_type = 'A:xxx' WHERE o_type = 'A:bleble';
UPDATE 1
 
> DELETE FROM objects WHERE o_type = 'A:bleble2';
DELETE 1
 
> select * from objects;
id | o_type
----+--------
1 | xxx
3 | A:xxx
(2 rows)
 
> select * from history_objects;
id |        modified_on         | modification_type |           modification_url           |   modification_user   | modified_id | modified_o_type
----+----------------------------+-------------------+--------------------------------------+-----------------------+-------------+-----------------
1 | 2006-07-15 12:58:54.16915  | INSERT            | unknown url. direct database access? | database user: depesz |           1 | bleble
2 | 2006-07-15 12:58:58.46624  | INSERT            | unknown url. direct database access? | database user: depesz |           2 | bleble2
3 | 2006-07-15 12:59:04.454578 | UPDATE            | unknown url. direct database access? | database user: depesz |           1 | xxx
4 | 2006-07-15 12:59:09.519242 | DELETE            | unknown url. direct database access? | database user: depesz |           2 | bleble2
5 | 2006-07-15 13:00:15.098039 | INSERT            | /jakis_url.html                      | admin                 |           3 | A:bleble
6 | 2006-07-15 13:00:18.315004 | INSERT            | /jakis_url.html                      | admin                 |           4 | A:bleble2
7 | 2006-07-15 13:00:21.304025 | UPDATE            | /jakis_url.html                      | admin                 |           3 | A:xxx
8 | 2006-07-15 13:00:24.483061 | DELETE            | /jakis_url.html                      | admin                 |           4 | A:bleble2
(8 rows)

działa. i to całkiem ładnie. oczywiście mechanizm można dalej rozszerzać. dodawać nowe pola (ip, browser) czy z części rezygnować.

zmienna lista “cech” – ciąg dalszy

jakiś czas temu pisałem jak poradzić sobie z przechowywaniem zmiennej listy cech obiektów w bazach relacyjnych.

okazuje się, że nie tylko ja zetknąłem się z tym problemem (szokujące, czyż nie?).

dwóch, znanych w świecie postgresowym ludzi – teodor sigaev i oleg bartunov – zajęli się tym problemem.

jeśli ich nie kojarzycie – to są ludzie którzy stworzyli i rozwinęli tsearcha. zajmują się  optymalizacjami indeksów typu gist, wymyślili własny typ indeksów (gin) i ogólnie siedzą w źródłach postgresa chyba 24 godziny na dobę. ich prace są częściowo sponsorowane przez rosyjską (aha, obaj są z rosji) agencję do wspierania badań (taki ichniejszy komitet badań naukowych).

problemem cech się zajęli, pogłówkowali i zrobili. nowy typ danych do postgresa – hstore.

co potrafi?

w jednym polu może przechowywać dowolnie wiele par klucz/wartość. wyszukiwać rekordów mających określone pola w sposób który korzysta z indeksów.

operować na tych danych i ogólnie – dać programiście wreszcie dobre rozwiązanie tego problemu.

jak się tego używa? trywialnie. nie chcę tutaj kopiować dokumentacji, odsyłam was do przykładów. warto się zainteresować, bo soft działa ładnie, skutecznie i szybko.

nowa wersja slony’ego!

jak nie wiesz co to slony, to polecam zajrzenie na strone projektu i poczytanie. w skrócie – silnik replikacyjny do postgresa, działający jako replikacja asynchroniczna master-multi_slave.

silnik co trzeba dodać działający stabilnie, wygodnie i szybko.

ostatnio pojawiła się nowa wersja. a właściwie zapowiedź nowej wersji – prerelease wersji 1.2.

w niej – sporo ulepszeń. tym razem nie są to ulepszenia które pomagają zestawić replikację (jak większośc wersji 1.1.x), ale modyfikacje tego jak sam core slony'ego działa. efekt ma być murowany – szybciej, skuteczniej, mniej zasobożernie, bardziej monitorowalnie :):

  • najważniejsze – spore zmiany w zarządzaniu pamięcią. slony teraz wykrywa kiedy ma do czynienia z dużymi rekordami i replikuje je w inny sposób dzięki czemu nie zżera tyle pamięci co poprzednie wersje
  • szybko zapychana tabela sl_log_1 została wreszcie “dopalona". slony będzie co jakiś czas przełączał się między sl_log_1 i sl_log_2, i jak upewni się, że nic z tej nieaktualnej nie jest już używane, to będzie ją czyścił poprzez TRUNCATE – dzięki czemu rozmiar tych tabel zostanie mocno ograniczony
  • poprawki tyczące się wykonywania poleceń modyfikujących bazę danych poprzez mechanizmy slony'ego – teraz kolejne polecenia będą wykonywane oddzielnie dzięki czemu zniknie problem z odwoływaniem się do dopiero-co utworzonych tabel i/lub pól.
  • poprawki tyczące się uruchamiania slony'ego jako serwisu w windowsach (nie używam więc nie jestem w stanie zweryfikować)
  • sporo poprawek tyczących się stabilności działania slonyego. teraz ma już być zbędne uruchamianie slonego przez watchdoga.

pełna lista zmian dostępna jest w tej chwili via cvsweb.

własne operatory upraszczające życie

na jednej z list postgresowych jakiś koleś zamarudził, że o ile warunek:

where pole!=3

działa poprawnie, to już

where pole!=-3 nie działa poprawnie.

wiele osób mu wyjaśniło, że:

  • standard sql nie oferuje operatora != w ogóle. do tego jest <>
  • != w postgresie to dodatek. takie “nice to have"
  • gdy doda się spację między “!=" a “-3" to działa ok (where pole!= -3)
  • całość działa tak dlatego, że system nie ma jak rozróżnić czy chodzi o “pole" “!=" “-3" czy może “pole" “!=-" “3", a to drugie jest w pełni legalną nazwę operatora – którego domyślnie nie ma, ale może być – w końcy postgres ma architekturę mocno otwartą i można sobie dodefiniować własne operatory czy cokolwiek.

facet oczywiście marudził dalej, że powinno działa.

postanowiłem pomóc. jak? dopisać operator !=- który zadziała tak jak facet chce.

najpierw potrzebna była funkcja. 30 sekund później miałem:

CREATE FUNCTION not_equals_minus(int8, int8) RETURNS bool AS $BODY$
SELECT $1 <> -$2;
$BODY$ LANGUAGE SQL IMMUTABLE STRICT;

proste i szybkie. działa dla liczb typu int8 (bigint), ale przerobienie do innych typów to trywiał.

to teraz definiojemy operator. jak? nie pamiętam. ale szybki search w manualu dał mi odpowiednią stronę, z jej pomocą napisałem:

CREATE OPERATOR !=- (
leftarg = int8,
rightarg = int8,
procedure = not_equals_minus,
commutator = !=-
);

efekt? działa. można teraz zrobić WHERE pole <> -3, WHERE pole != -3, lub WHERE pole!=-3. kocham postgresa 🙂

postgresql summit – nowości

10 lat temu, na małym webserwerze w kanadzie został wystawiony w internecie – po raz pierwszy – projekt postgresql.

aby uczcić tę rocznicę, a także by dać developerom możliwość spotkania się osobiście (i pewnie uroślinnienia :), została przygotowana mała, 2 dniowa, konferencja – postgresql summit.

są na niej przedstawiciele z praktycznie wszystkich liczących się firm około postgresowych: afilias, enterprisedb, greenplum, command prompt, sra, sun, pervasive i inni.

pierwszego dnia (wczoraj) było kilka interesujących wystąpień.

jednym z najważniejszych technologicznie (w/g mnie) był pokaz przygotowany przez korry'ego douglasa (autora niezłej książki o postgresie, aktualnie pracującego w enterprise db). korry pokazywał przygotowany przez enterprisedb mechanizm pozwalający na np. debugowanie funkcji plpgsql'owych.

całość podobno ma około 10 linii kodu, dzięki czemu wpływ tego kodu w momencie gdy nie korzystamy z niego jest minimalny.

a dodatkowo całość nie została zrobiona jako debugger, tylko zestaw hook'ów w backendzie postgresql'a. dzięki czemu w oparciu o ten sam mechanizm będzie można budować profilery, optymalizatory czy tracery.

wstępne szacunki mówią, że kod zostanie włączony do źródeł postgresa przed wydaniem 8.2, więc jest spora szansa, że w 8.2 będzie już debugger z prawdziwego zdarzenia (z breakpointami, wykonywaniem krokowym, inspekcją zmiennych itd.).

interesujące jest to, że kod został przygotowany w taki sposób by dało się do niego (jako interfejs) dopiąć cokolwiek – w sensie, nie jest to monolityczne rozwiązanie, interfejsy będzie można mieć różne. np. phpmyadmin, czy dowolny inny program tego typu.

pozostaje czekać kiedy 8.2 się ukaże 🙂

zmienna lista “cech”

przy wielu projektach pojawia się potrzeba przechowywania zmiennej listy “cech" jakichś obiektów.

weźmy na przykład sklep internetowy: mamy jakieś tam kategorie (struktura drzewiasta, moja ulubiona 🙂 ), w nich produkty. każdy produkt ma pewne cechy stałe – cena, tytuł, opis. natomiast produkty w określonych kategoriach mają swoje własne cechy dodatkowe.

np. dla samochodów możemy chcieć przechowywać:

  • ilość drzwi
  • pojemność silnika
  • typ paliwa
  • rodzaj skrzyni biegów

z drugiej strony, ogłoszenia w kategorii komputery będą miały pola takie jak:

  • ilość pamięci
  • wielkość dysku
  • typ procesora

najprostszym rozwiązaniem jest trzymanie produktów w każdej kategorii w oddzielnej tabelce – gdzie każda z tych tabelek ma różną strukturę (inna lista pól).

jest to rozwiązanie nieakceptowalne – osobiście uważam, że jakiekolwiek rozwiązanie zakładające modyfikacje struktury bazy danych w trakcie normalnego użytkowania jest błędne.

inną metodą jest zrobienie sobie tabelki typu:

 CREATE TABLE zmienne_cechy (
id         SERIAL PRIMARY KEY,
produkt_id INT  NOT NULL references produkty(id),
cecha1     TEXT,
cecha2     TEXT,
cecha3     TEXT,
cecha4     TEXT,
...
);

i pilnowanie, że dla danego produktu kolumna cecha1 oznacza pojemność silnika, a dla innego jest to ilość pamięci.

takie rozwiązanie ma swoje zalety – najważniejszą jest to, że aby wyciągnąć informacje o wszystkich polach dla danego produktu wystarczy pobrać jeden rekord z bazy.

ale dopóki nie obsługujesz miliona page-views dziennie w swoim sklepie – ten problem jest mało istotny 🙂

zdecydowanie najskuteczniejszą metodą jest tabelka:

 CREATE TABLE zmienne_cechy (
id         SERIAL PRIMARY KEY,
produkt_id INT  NOT NULL references produkty(id),
cecha      TEXT,
wartosc    TEXT
);

taka tabelka w jednej prostej strukturze pozwala na zapisanie wszystkich mozliwych cech i łatwe wyszukiwanie. no właśnie. czy na pewno łatwe?

tabelka pokazana taka jak tu – jest może i fajna, ale brakuje jej jeszcze jednej rzeczy:

create unique index ui_zmienne_cechy_pic on zmienne_cechy (produkt_id, cecha);

jeśli nie czytacie sql'i ze 100% zrozumieniem, to powyższe powoduje, że dany produkt może mieć tylko jedną wartość danej cechy. może mieć dowolnie wiele cech, ale żadna z cech nie może mieć wielu wartości.

zazwyczaj takie ograniczenie w niczym nie przeszkadza. zdarzają się czasem (ale bardzo rzadko) sytuacje, że istnieje potrzeba wielu wartości jednej cechy – sugeruję by wtedy nie kasować tego indeksu/klucza unikalnego tylko po prostu użyć ciut innych cech.

czemu?

otóż taka tabelka z pokazanym kluczem unikalnym pozwala nam w trywialny sposób zrobienie tego co bez klucza jest dużo trudniejsze (zasobochłonne): znalezienia produktów w/g kilku cech jednocześnie.

załóżmy, że chcemy znaleźć samochody o pojemności silnika 2000 z automatyczną skrzynią biegów.

bez klucza unikalnego jesteśmy skazani na coś takiego:

 select zc1.produkt_id
from zmienne_cechy zc1 join zmienne_cechy zc2 on zc1.produkt_id = zc2.produkt_id
where zc1.cecha='pojemnosc silnika' and zc1.wartosc = '2000' and zc2.cecha = 'skrzynia biegow' and zc2.wartosc = 'automat';

nie jest to oczywiście takie złe. ale przy dużej ilości produktów stanie się problematyczne. nie mówiąc o tym jak będziemy chcieli sprawdzić produkty w/g np. 5 cech na raz. 5 joinów? jeśli w bazie jest np. 1000 produktów i każdy ma średnio 10 cech, to łączymy 5 razy ze sobą tabelę o 50000 rekordów. i szukamy na nich wszystkich. mało przyjemne.

dodanie wspomnianego wyżej klucza unikalnego pozwala na użycie w naszym select'cie rzadko używanej (i słabo znanej) klauzuli HAVING:

 select produkt_id
from zmienne_cechy
where
(cecha='pojemnosc silnika' and wartosc='2000')
or
(cecha='skrzynia biegow' and wartosc = 'automat')
group by produkt_id
having count(*) = 2;

powstałe zapytanie ma kilka zalet:

  • jest trywialnie rozbudowywalne o kolejne cechy – bez konieczności dodatkowych joinów = wystarczy dodać dodatkowe warunki i podbić wartość w klauzuli having
  • jeśli używamy postgresql'a 8.1 i mamy dodatkowo indeks dwupoowy na (cecha, wartosc), to postgresql uzyje bardzo szybkich bitmap-or'ów

moje testy wykazały, że na postgresie 8.1 obie metody dają bardzo podobne wyniki (chodzi o czas zapytania) jeśli szukamy dwóch cech, natomiast już od 3 przewaga rozwiązania z having jest olbrzymia.

postgresql 8.1.4 (i inne)

wyszedł nowy postgresql.

co nowego:

  • poprawka  związana z możliwością robienia jakichś sql-injection. szczegółów nie czytałem, ale coś mocno skomplikowanego, bo zrobili specjalną stronę tylko o tym.
  • istotne jest to, że nowy postgres będzie odrzucał escape'owanie apostrofów (‘) przez backslash (\). tzn. nie będzie można zrobić:
    select ‘costam \'bleble';
    tylko trzeba będzie napisać:
    select ‘costam "bleble';
    ta druga metoda jest zgodna ze standardami i ogólnie lepsza. jeśli używać \' – nie desperuj. zakaz używania tego tyczy tylko niektórych kodowań (zasadniczo azjatyckich).
  • w pgcrypto poprawiono funkcję generującą słabe klucze – tyczy się to tylko sytuacji gdy buduje się pgcrypto bez używania openssl'a – czyli dosyć niszowa sprawa
  • poprawiono funkcje konwertujące między różnymi charsetami. nic związanego z polską (na oko), ale jeśli pracujecie z cyrlicą to może was zainteresować (o ile używacie konwersji win1251_to_iso)
  • procesy autovacuuma są teraz widoczne w pg_stat_activity (wreszcie!)
  • wyłączono funkcję full_page_writes. funkcja ta w pewnych sytuacjach może uszkodzić dane. sensowny fix będzie w 8.2, na razie zostało to po prostu wyłączone.
  • trochę poprawek do planera i optymalizatora (związanych z lepszą obsługą join'ów, bitmap scanów, oraz specyficznych optymalizacji dla agregatorów min() i max())
  • poprawka błędu który mógl doprowadzić do padu backendu jeśli w jednej transakcji jakaś funkcja była modyfikowana i później (w tej samej transakcji) używana.
  • poprawka odtwarzania danych z rekordów WAL w sytuacji gdy indekxy btree były obcinane
  • usunięcie błędu związanego z nieprawidłową obsługą operatora | w wyrażeniach SIMILAR TO (takie jakby LIKE skrzyżowane z regexpami)
  • 2 zapytania tworzące tabele (select into i create table as) tworzą tabele w domyślnym tablespace, a nie w głównym.
  • poprawka do serwera tycząca negocjacji ssl'i (a dokładniej nieprawidłowego obsługiwania parametrów wymiany diffie-helmana)
  • poprawiona wydajność sortowania algorytmem qsort (ta poprawka tyczy się kodu który jest używany jedynie na solarisie)
  • korekty działania bonjour w architekturze x86 (bonjour to taki system wykrywania usług, oryginalnie stworzony na OS/X)
  • kilka poprawek drobnych memleaków
  • poprawka tycząca wprowadzania hasła na systemach win32
  • pg_dump lepiej zapisuje wartości domyślne dla domen
  • pg_dumpall lepiej obsługuje sytuacje gdy dumpuje z systemu mającego i usera i grupę userów o tej samej nazwie (tyczy się tylko dumpowania z postgresql'a < 8.1
  • pg_restore zostało poprawione tak, aby switch -n działał zgodnie z dokumentacją 🙂 (-n XXX odtworzy tylko obiekty z tej schemy)

poprawki związane z bezpieczeństwem zostały też sportowane do wcześniejszych wersji postgresa (8.0 (8.0.8), 7.4 (7.4.13) i 7.3 (7.3.15))

łączenie pól tekstowych z wielu rekordów

czasem jest taka potrzeba by zwrócić kilka połączonych stringów z kolejnych rekordów.

np.:

mając tabelkę z takimi danymi:

> select pracownik from pracownicy where firma_id = 1;

  • Jan Kowalski
  • Hubert Lubaczewski
  • Piotr Nowak
  • Adam Słodowy

chcielibyśmy dostać jedno pole z zawartością:

Jan Kowalski, Hubert Lubaczewski, Piotr Nowak, Adam Słodowy

lub wręcz posortowane:

Adam Słodowy, Hubert Lubaczewski, Jan Kowalski, Piotr Nowak

aby to zrobić najwygodniej jest zdefiniować własny agregat (czyli funkcję taką jak min(), max(), sum(), avg()).

najpierw zdefiniujemy funkcję łączącą:

CREATE OR replace function agg_text_sum(txt_before TEXT, txt_new TEXT) RETURNS TEXT AS
$BODY$
DECLARE
BEGIN
IF (txt_before IS NULL) THEN
RETURN txt_new;
END IF;
RETURN txt_before || ‘, ‘ || txt_new;
END;
$BODY$
LANGUAGE ‘plpgsql';

następnie tworzymy agregat:

CREATE aggregate text_sum (
basetype = TEXT,
sfunc = agg_text_sum,
stype = TEXT
);

i już możemy:

> select text_sum(pracownik) from pracownicy where firma_id = 1;

  • Jan Kowalski, Hubert Lubaczewski, Piotr Nowak, Adam Słodowy

aby to posortować trzeba uciec się do podzapytań:

> select text_sum(pracownik) from (select pracownik from pracownicy where firma_id = 1 order by pracownik) x;

  • Adam Słodowy, Hubert Lubaczewski, Jan Kowalski, Piotr Nowak

wstawianie rekordu, gdy jeszcze go nie ma

kolega z pracy zapytał:

jak wstawić rekord do tabelki, ale tylko jeśli go tam jeszcze nie ma?

teoretycznie sprawa jest prosta: select, jak nic nie zwróci – insert. no ale jak będzie to działało równolegle to jest spora szansa na wysypanie się takiego kodu.

potrzebna jest więc inna metoda.

może insert i przechwycenie błędu.

w sumie to nie takie głupie:

  • BEGIN;
  • INSERT;
  • COMMIT;

jeśli insert się nie udał, no to commit zrobi to samo co rollback.

fajnie. zadziała. ale jest jakoś mało eleganckie.

a może inaczej … chwila zastanowienia i … jest. proste rozwiązanie:

INSERT INTO tabelka (pole_1, pole_2, …, pole_n)
SELECT wartosc_1, wartosc_2, …, wartosc_n
WHERE NOT EXISTS ( SELECT * FROM tabelka WHERE pole_1 = wartosc_1 AND pole_2 = wartosc_2 AND … AND pole_n = wartosc_n);

nie jest to może najśliczniejsze, ale działa na pewno. nie wywala się niezależnie od tego czy wstawiło czy nie (więc można bezpiecznie używać w transakcjach). same plusy 🙂