dbi-link

ukazała się nowa wersja – 2.0, pakietu dbi-link. jest to pakiet umożliwiający podłączanie postgresa do innych baz danych (innych instancji postgresa, albo po prostu zupełnie innych baz), w sposób możliwie przezroczysty dla użytkownika.

przy użyciu tego pakietu, dostajemy wirtualne tabelki na których możemy prawie normalnie pracować  (jeszcze nie można join'ować), nie przejmując się tym, że tak naprawdę są to tabelki z innych baz.

wersja 2.0 zawiera olbrzymie zmiany idące w kierunku poprawienia działania, stabilności oraz ogólnej odporności.

jak się sprawdzi – za wcześnie jeszcze na wyrokowanie, ale wygląda mocno interesująco.

jeśli chodzi o to do jakich baz można się podłączyć – praktycznie do każdej. jak nazwa wskazuje do immplementacji samego połączenia do zdalnej bazy jets używana biblioteka dbi z perla, a ona ma sterowniki praktycznie do wszystkiego.

losowy rekord z bazy danych

czy stanęliście kiedyś przed problemem wylosowania rekordu z tabeli? dowolnego rekordu?
oczywistym pomysłem jest:

# SELECT * FROM tabelka ORDER BY random() LIMIT 1;

no ale to jest wolne. wymaga posortowania całej tabeli. co w najlepszym układzie ma złożoność "n log n".
przykładowo u mnie na testowej tabelce trwało to 90 sekund! (1.7 miliona rekordów).
no nie za dobrze.
niektórzy mogą sugerować takie rozwiązanie:

  1. znajdź maksymalne
  2. SELECT * FROM tabelka WHERE id <= random() * maksymalne_id limit 1;

na oko jest ok. tzn. akurat nie jest, bo random jest funkcją volatile, i trzeba by raczej … WHERE id <= (select random() * maksymalne_id) LIMIT 1, ale to już szczegół.
czemu to jest złe?
bo wprowadza pewien istotny problem. jeśli numeracja pola id w naszej tabelce zawiera dziury (czyli jest takie id, które jest większe od minimalnego i mniejsze od maksymalnego, dla którego nie ma rekordu) – to te losowane rekordy wcale nie będą dobrze losowane.
jako ekstremalny przykład (ale dobrze pokazujący rzeczywistość) podajmy tabelkę z dwoma rekordami, o id odpowiednio: 1 i 100. rekord z id = 1 będzie wypadał 99 razy częściej niż rekord z id = 100!.
cóż więc pozostaje? siąść i płakać?
nie.
można użyć inteligencji. czyli funkcji/procedury.
przykładowo taka funkcja:

CREATE OR REPLACE FUNCTION random_record() RETURNS tabelka AS $BODY$
DECLARE
    id_min INT8;
    id_max INT8;
    range INT8;
    temp_id INT8;
    temprec RECORD;
BEGIN
    SELECT min(id) INTO id_min FROM tabelka;
    SELECT max(id) INTO id_max FROM tabelka;
    range:= 1 + ( id_max - id_min );
    LOOP
        temp_id := id_min + (random() * range::float8)::INT8;
        SELECT * INTO temprec FROM tabelka WHERE id = temp_id;
        IF found THEN
            RETURN temprec;
        END IF;
    END LOOP;
END;
$BODY$ language 'plpgsql';

co ona robi?zwraca losowy rekord. całkowicie losowy – każdy rekord ma te same szanse bycia wylosowanym.
warunki brzegowe? pole id musi być unikatowe (szokujące, nie?). no i: im więcej dziur w numeracji tym wolniej działa. ale co znaczy wolniej?
ta moja testowa tabelka ma takie dane:

# select min(id), max(id), count(*) from tabelka;
 min |   max    |  count
-----+----------+---------
   3 | 36574227 | 1721217
(1 row)

czyli jak widać – dziur jest sporo. w szczególności – dziur jest 21 razy więcej niż istniejących rekordów!
przypomnę, że

select * from tabelka order by random() limit 1;

działało na tej tabelce w około 90 sekund.
ile czasu zajmuje to funkcji?
6 kolejnych wywołań. czasy odpowiednio: 124.700, 141.442, 201.708, 94.413, 145.128, 110.076. milisekund!
jak widać – jest szybko.
problemem tej funkcji jest to, że teoretycznie może się zdarzyć, że nigdy się nie skończy. ale w/g mnie jest to gdybanie. zresztą – zawsze można dorobić warunek, że jeśli np. wykonano już 1000 strzałów niecelnych, to zwróćmy pierwszy rekord z brzegu.
i już.
czy można to jakoś dopalić?
tak.
jeśli wiecie, że tabelka w której szukacie ma dużo dziur, to dodajcie do niej jedno pole:

create sequence random_thing_seq;
alter table tabelka add column random_thing int8;
alter table tabelka alter column random_thing set default nextval('random_thing_seq');
update tabelka set random_thing = nextval('random_thing_seq') where random_thing is null;
alter table tabelka alter column random_thing set not null;
create unique index ui_random_thing on tabelka (random_thing);

i potem używajcie w funkcji random_thing a nie id.
cel ćwiczenia?
jak sie pojawi za dużo dziur w numeracji (random_thing też będzie miał dziury) to zawsze możecie:

update tabelka set random_thing = nextval('random_thing_seq');

i już dziur nie ma,
a ponieważ random_thing nie jest do niczego innego używane – jest to w pełni bezpieczne.
oczywiście po takim update'cie dobrze jest zrobić vacuum'a. a najlepiej vacuum full'a.

czytanie manuali popłaca

eh. od dawna mi tego brakowało w postgresie. i okazało się, że niepotrzebnie, bo już od 8.1 jest! wyłączanie triggerów. pojedynczo. każdy można tymczasowo wyłączyć.
na wypadek gdybyście nie wiedzieli:

# ALTER TABLE test DISABLE TRIGGER xxx;

gdzie xxx to nazwa triggera, lub słowo "ALL" jeśli chcemy wyłączyć wszystkie triggery, lub słowo "USER" jeśli chcemy wyłączyć wszystkie triggery, ale pozostawić działające klucze obce (które są realizowane triggerami).
włączenie z powrotem to:

# ALTER TABLE test ENABLE TRIGGER xxx;

jedynym problemem z tym polega na tym, że w psql'u, przy robieniu \d tabeli nie widać które triggery są wyłączone:

# create table x (id serial primary key, wstawiana text);
# CREATE TRIGGER test AFTER INSERT OR UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test();
# CREATE TRIGGER test_u AFTER UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test_u();
# alter table x disable trigger test;
# \d x
                           Table "public.x"
  Column   |  Type   |                   Modifiers
-----------+---------+------------------------------------------------
 id        | integer | not null default nextval('x_id_seq'::regclass)
 wstawiana | text    |
Indexes:
    "x_pkey" PRIMARY KEY, btree (id)
Triggers:
    test AFTER INSERT OR UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test()
    test_u AFTER UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test_u()

można sobie ręcznie sprawdzić które triggery są wyłączone:

# select t.tgname, t.tgenabled from pg_class c join pg_trigger t on c.oid = tgrelid where c.relname = 'x';
 tgname | tgenabled
--------+-----------
 test_u | t
 test   | f
(2 rows)

w 8.3 na szczęście jest to już poprawione:

# \d x
                           Table "public.x"
  Column   |  Type   |                   Modifiers
-----------+---------+------------------------------------------------
 id        | integer | not null default nextval('x_id_seq'::regclass)
 wstawiana | text    |
Indexes:
    "x_pkey" PRIMARY KEY, btree (id)
Triggers:
    test_u AFTER UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test_u()
Disabled triggers:
    test AFTER INSERT OR UPDATE ON x FOR EACH ROW EXECUTE PROCEDURE test()

słodko 🙂

zaśmiecony template1 i kopiowanie baz

standardowa instalacja postgresa zawiera 2 (lub 3) bazy:

  • template0
  • template1
  • postgres (tylko w najnowszych wersjach)

template0 jest bezpieczna, o tyle, że nie da się do niej prosto podłączyć, więc nikt w niej nie namiesza.
baza template1 jest używana jako "podstawa" do tworzenia nowych baz. tzn. za każdym razem jak robisz: "create database xxx;" to tak naprawdę jest wykonywana kopia bazy template1.
daje to parę fajnych możliwości – np. zrobienie czegokolwiek w bazie template1 oznacza, że każda nowa baza będzie to miała automatycznie.
np. załadowane rozszerzenie, języki itd.
co jednak jeśli do template1 przez pomyłkę wrzuciliście jakieś zbędne dane? np. odtworzyliście do template1 zamiast do xxx jakąś bazę z dumpa?
ręczne kasowanie jest skomplikowane.
ideałem byłoby przywrócenie template1 do stanu początkowego.
da się to prosto zrobić.
w pierwszym kroku łączymy się psql'em do postgresa, na konto admina (zazwyczaj postgres, lub pgdba). ważne jest by połączyć się do bazy innej niż template1.
będąc tak połączonym wykonujemy 4 proste kroki:

  1. update pg_database set datistemplate = false where datname = ‘template1';
  2. drop DATABASE template1;
  3. CREATE DATABASE template1 with template template0;
  4. update pg_database set datistemplate = true where datname = ‘template1';

krok 1 jest niezbędny gdyż baza z wartością "datistemplate = true" nie może być skasowana. więc zaznaczamy, że template1 nie jest szablonem 🙂
krok 2 – kasujemy bazę template1. tu dwie ważne uwagi:

  1. w czasie wykonywania drop database do dropowaniej bazy nie może być żadnych połączeń. właśnie dlatego musieliśmy się psql'em podpiąć do innej bazy.
  2. między drop database template1, a create database template1 nie da sie stworzyć innych baz (a dokłaniej, nie jest to tak proste jak zazwyczaj)

krok 3 – odtwarzamy bazę template1, korzystając z szablonu template0
krok 4 – zaznaczamy template1 jako szablon.
i już to wszystko.

tu informacja dodatkowo. może to wykryliście z powyższego przykładu, ale jak nie, to piszę:
w podobny sposób można zrobić *szybką* kopię całej bazy.
np. jeśli potrzebujecie backup przed odpaleniem na bazie jakichś skomplikowanych rzeczy, można:

create database xxx_backup with template xxx;

oczywiście do tego przydałoby się też ‘encoding qqq owner yyy', ale to już szczegół.
olbrzymim plusem tej metody jest szybkość. przy czym nie szybkość tworzenia kopii. to czasem jest dłuższe niż pg_dump. to co jest istotne, to fakt iż "przywrócenie" bazy danych z takiej kopii, to proste:

drop database xxx;
alter database xxx_backup rename to xxx;

największym minusem jest fakt iż w czasie kopiowania do bazy źródłowej nie może być żadnych połączeń. czyli nie można tak skopiować bazy używanej produkcyjnie.
to spora wada. tym niemniej w środowiskach testowych/developerskich stosuję ją często z wyśmienitym skutkiem.

drzewa w sql’u – metoda “zagnieżdżonych zbiorów”

metodę zagnieżdżonych zbiorów poznałem po raz pierwszy po przeczytaniu którejś z książek joe celko. chyba tej: Advanced SQL Programming, ale na 100% nie jestem pewien.
zagnieżdżone zbiory (nested sets) polegają w duzym skrócie na tym, że każdy element drzewa jest opisany nie jednym id, ale parą liczb. są to w miarę dowolne liczby, z założeniem jedynie takim, że "lewa" jest mniejsza od "prawej", oraz, że obie liczby (lewa i prawa) wszystkich elementów drzewa poniżej danego muszą się mieścić w zakresie (lewa, prawa) swoich rodziców.
skomplikowane? też nie zrozumiałem.
przypomnijmy sobie nasze oryginalne, testowe drzewo:

teraz.
tworzymy sobie taką tabelkę:

# create table nested_sets ( id_left int4 primary key, id_right int4 not null check (id_left < id_right), name text);

jako primary key wybrałem sobie id_left, ale mogłem wybrać też right. w szczególności – wartości w polach id_left i id_right muszą być unikatowe. czyli jeśli w którymś elemencie w polu id_left jest wartość 5, to nie może się ona powtórzyć ani w id_left ani w id_right.
ok. jak nadać numerki?
proste. zaczynamy od elementu głównego, i przyznajemy mu id_left = 1. potem idziemy do jego pierwszego dziecka. jego id_left dajemy kolejny numer. jeśli ten element ma podelementy, to powtarzamy zejście w dół z przyznawaniem kolejnych id_left. jeśli dany element nie ma "dzieci", to nadajemy mu id_right równy kolejnej liczbie. i wracamy piętro wyżej.
mało jasne? pewnie nie umiem za dobrze opisać. więc lecimy w krokach:

  1. elementowi sql, przypisujemy id_left = 1, i schodzimy w dół do "postgresql".
  2. elementowi postgresql, przypisujemy id_left = 2, i ponieważ ma jakieś dzieci, schodzimy w dół – do "linux"
  3. elementowi linux przypisujemy id_left = 3
  4. ponieważ linux nie ma "dzieci", przypisujemy mu id_right = 4 i wracamy wyżej
  5. ponieważ postgresql nie ma "dzieci", przypisujemy mu kolejne id_right. czyli id_right = 5. i wracamy wyżej.
  6. jesteśmy z powrotem w sql. wchodzimy w kolejne dziecko – oracle
  7. elementowi oracle przypisujemy id_left = 6.
  8. itd. aż dojdziemy do ustawienia dla elementu sql, id_right = 18

zwracam uwagę na to, że w tej numeracji widać od razu, że ilość elementów jest równo połową największego id_right. co jest poniekąd logiczne.
cała tabelka wygląda tak:

id_left id_right name
1 18 sql
2 5 postgresql
3 4 linux
6 17 oracle
7 8 solaris
9 14 linux
10 11 glibc1
12 13 glibc2
15 16 windows

ok. jak się pyta taką bazę?
tu uwaga – ten model znam najsłabiej. głównie dlatego, że go osobiście nie lubię. jeśli znajdziecie błąd w tym co poniżej napiszę – proszę o informację. nie jestem nieomylny, a jak już mówiłem – tego modelu drzew nie lubię i nie bawiłem się nim w ogóle.
1. pobranie listy elementów głównych (top-levelowych)

SELECT
    ns1.*
FROM
    nested_sets ns1
    LEFT OUTER JOIN nested_sets ns2 ON (ns1.id_left > ns2.id_left AND ns1.id_right < ns2.id_right)
WHERE
    ns2.id_left IS NULL
;

zwrócić należy uwagę na fakt iż jeśli nasze drzewo ma tylko i wyłącznie 1 element top-levelowy to zapytanie można uprościć do:

# SELECT * FROM nested_sets WHERE id_left = 1;

jeśli stosujemy numerację od 1, lub

# SELECT * FROM nested_sets ORDER BY id_left ASC LIMIT 1;

jeśli numeracja startuje od nieznanej liczby.

2. pobranie elementu bezpośrednio “nad” podanym elementem:

dane wejściowe:

  • ID : id_left elementu
SELECT
ns.*
FROM
nested_sets ns
WHERE
[ID] BETWEEN ns.id_left + 1 AND ns.id_right
ORDER BY ns.id_left DESC LIMIT 1

jeśli zapytanie nic nie zwróci – znaczy to, że dany element był “top-levelowy”.

3. pobranie listy elementów bezpośrednio “pod” podanym elementem

dane wejściowe:

  • ID : id elementu
SELECT
nsc.*
FROM
nested_sets nsp
JOIN nested_sets nsc ON (nsc.id_left BETWEEN nsp.id_left + 1 AND nsp.id_right)
WHERE
nsp.id_left = [ID]
AND NOT EXISTS (
SELECT *
FROM nested_sets ns
WHERE
( ns.id_left BETWEEN nsp.id_left + 1 AND nsp.id_right )
AND
( nsc.id_left BETWEEN ns.id_left + 1 AND ns.id_right )
)

4. pobranie listy wszystkich elementów “nad” danym elementem (wylosowanym)

dane wejściowe:

  • ID : id elementu
SELECT
    ns.*
FROM
    nested_sets ns
WHERE
    [ID] BETWEEN ns.id_left + 1 AND ns.id_right

5. pobranie listy wszystkich elementów “pod” danym elementem (wylosowanym)

dane wejściowe:

  • ID : id elementu
SELECT
    nsc.*
FROM
    nested_sets nsp
    JOIN nested_sets nsc ON nsc.id_left BETWEEN nsp.id_left + 1 AND nsp.id_right
WHERE
    nsp.id_left = 6

6. sprawdzenie czy dany element jest “liściem” (czy ma pod-elementy)

dane wejściowe:

  • ID : id elementu
>SELECT true from nested_sets WHERE id_left = [ID] AND id_right = [ID] + 1;

jeśli zwróci true – to jest liść. jeśli nic nie zwróci – to nie jest liściem.

7. pobranie głównego elementu w tej gałęzi drzewa w której znajduje się dany (wylosowany) element

  • ID : id elementu
SELECT
    ns.*
FROM
    nested_sets ns
WHERE
    [ID] BETWEEN ns.id_left + 1 AND ns.id_right
ORDER BY
    ns.id_left ASC LIMIT 1

podstawową zaletą tego rozwiązania jest to, że bardzo szybko zwraca listę wszystkiego "nad", "pod" czy listę liści.

wadami jest mała intuicyjność, skomplikowane przenoszenie elementów (gdybym miał to zaimplementować, to najprawdopodobniej po prostu po każdym przeniesieniu od nowa bym numerował id_left/id_right.

dodatkowo – część zapytań wymaga albo "order by xxx limit 1", albo subselectów co raczej nie wróży dobrze wydajności. a order by limit 1 nie zawsze można użyć (np. użycie tego w joinach jest już mocno problematyczne).

wybory w nowej zelandii – sukcesem postgresa

w nowej zelandii od pewnego czasu istniał centralny system zarządzania wyborami. pracował w oparciu o oracle'a i jakoś nie dawał sobie rady.
w 2003 roku rozpoczęto prace nad nowym systemem. i właśnie ostatnio został pomyślnie wdrożony. aktualny system pracuje w oparciu o postgresa 7.4, z replikacją (system jest rozproszony) opartą o rserva. firma która się tym zajmuje przymierza się do migracji na 8.1 i slony'ego, plus kilka zmian wewnątrz bazodanowych (jak np. wprowadzenie partycjonowania).
baza zajmuje teraz około 50giga, i trzyma informacje o około 2.8 miliona osób i ich głosów.

zmaterializowane widoki

heh. tytuł pewnie nic nie mówi ludziom nie siedzącym w bazach danych, ale to mało istotne – post jest dla bazodanowców.
trafiłem na bardzo fajny tekst mówiący o tym jakie są typy "materialized views", oraz jak je zrobić w postgresie. tekst nie jest nowy, ale jest to przyjemny zbiór informacji który nie zestarzał się za bardzo. polecam jeśli chcecie w prosty sposób szybko wyciągać skomplikowane dane 🙂

kilka nowych postgresów

3 dni temu (zajęty byłem, nie mogłem od razu zblogować) core-team wypuścił kilka nowych wersji postgresql'a: 8.2.1, 8.1.6, 8.0.10, 7.4.15 i 7.3.17.
wersje te to typowe bugfixy – bez nowych funkcjonalności. tym niemniej – jeśli używasz postgresa – warto się zastanowić nad upgrade'em przynajnmniej w ramach swojej wersji głównej (7.3, 7.4, 8.0, 8.1 i 8.2) – poprawiono np. błędy związane z tym, że w pewnych określonych sytuacjach postgres mógł po prostu paść (backend, nie cała baza).

nieznany bohater: generate_series

kto z was wie o istnieniu funkcji generate_series() w postgresie? a kto z niej kiedykolwiek (nie testowo!) skorzystał?
wydaje mi się, że jest to najbardziej niedoceniania funkcja istniejąca w tym systemie bazodanowym.
co daje? zobacz:

select * from generate_series(0, 100);

na pierwszy rzut oka nic rewolucyjnego? taki tam sobie liczniczek?
zobaczmy więc kilka przykładowych zastosowań.
pierwsze:
mamy tabelkę, z polem id (z seriala), i chcemy sprawdzić czy są dziury w numeracji. tradycyjna metoda to jakieś joiny czy skanowanie sekwencyjne ze zliczaniem dziur w aplikacji klienckiej.
a zobaczmy coś takiego:

create table x as select * from generate_series(1, 10000) as x (i);
delete from x where random() < 0.0010;

w ten sposób uzyskaliśmy tabelę x, z numerycznym polem i. a w numeracji tego pola są dziury.
jak je wylistować? tak:

select * from generate_series(1, 10000) except select i from x;

voila. działa. ślicznie. i szybko 🙂

to było proste i oczywiste.
teraz przykład trochę mniej oczywisty – przykład jest z mojej pracy, ale użyję trochę uproszczonej wersji struktur danych 🙂
stwórzmy sobie tabelkę z użytkownikami:

CREATE TABLE users (id serial PRIMARY KEY, username TEXT, registered date);

ok. teraz – wstawmy do niej 10 użytkowników, z losowymi unikalnymi username'ami, i różnymi, losowymi datami rejestracji:

insert into users (username, registered) select 'user: ' || x.i, '2000-01-01'::date + cast(random()*1000 as int4) from generate_series(1, 10) as x(i);

mamy teraz w users 10 użytkowników. daty rejestracji są losowane z zakresu ‘2000-01-01' do ‘2002-09-27';
teraz. to co potrzebowałem to musiałem wygenerować "faktury" dla użytkowników. w tym zaległe.
idea była taka, że w tabelce z fakturami:

CREATE TABLE invoices (id serial PRIMARY KEY, user_id INT4, paid_from date, paid_to date, status INT4);

są trzymane faktury użytkowników. każda faktura jest na 30 dni. musimy wstawić każdemu użytkownikowi tyle faktur by obejmowały cały okres od daty rejestracji użytkownika do chwili obecnej.
przykładowo:
jeśli mielibyśmy użytkownika który zarejestrował się 2006-12-01 (a dziś jest 2007-01-11) to musielibyśmy wygenerować dla niego 2 faktury:

  1. od 2006-12-01 do 2006-12-30
  2. od 2007-12-31 do 2007-01-29

dalszych nie, bo musimy zakończyć faktury na fakturze "aktualnej".
pierwsze co potrzebujemy to wiedzieć ile maksymalnie nam się może faktur wygenerować dla pojedynczego użytkownika. w naszym przypadku sprawdzamy to poprzez:

select ceil((now()::date - min(registered))/30.0) from users;

u mnie pokazało się 83.
ponieważ lubię proste liczby, zaokrąglę sobie to do 100. nie ma żadnego matematycznego uzasadnienia – po prostu tak wolę.
tak więc. to co nam się przyda to generate_series(1,100), a dokładniej generate_series(0,100).
pierwsze zapytanie które napiszemy zwróci nam dla danego użytkownika (na razie tylko jednego) listę 101 faktur (od 0 do 100):

SELECT u.registered + (x.i * 30), u.registered + (x.i*30) + 29 FROM users u, generate_series(0, 100) as x(i) WHERE u.id = 1;

zwracam uwagę na to, że nie mamy żadnego warunku na połączenie users i generate_series – nie jest to join, ale tzw. kartezjan.
jak widać – zapytanie zadziałało.
teraz musimy je ograniczyć, by zwróciło dane tylko do aktualnych:

SELECT
    u.registered + (x.i * 30), u.registered + (x.i*30) + 29
FROM
    users u, generate_series(0, 100) as x(i)
WHERE
    u.id = 1
    AND now()::date >= u.registered + (x.i * 30)
;

wow. działa.
to teraz. ponieważ mamy do czynienia z kartezjanem a nie joinem, to usunięcie "where u.id = 1" nie spowoduje uszkodzenia danych, tylko wypełnienie ich dla wszystkich userów.
dzięki czemu możemy zapisać finalne zapytanie w postaci:

INSERT INTO invoices (user_id, paid_from, paid_to, status)
SELECT
    u.id,
    u.registered + (x.i * 30),
    u.registered + (x.i*30) + 29,
    0
FROM
    users u, generate_series(0, 100) as x(i)
WHERE
    now()::date >= u.registered + (x.i * 30)
;

czy na pewno zadziałało dobrze?
zobaczmy:

SELECT
    u.id,
    u.registered,
    ( SELECT count(*) FROM invoices i WHERE i.user_id = u.id ) as invoice_count,
    i1.paid_from as invoice1_from,
    i1.paid_to as invoice1_to,
    i2.paid_from as invoice2_from,
    i2.paid_to as invoice2_to,
    now()::date as "now"
FROM
    users u,
    invoices i1,
    invoices i2
WHERE
    i1.id = (SELECT min(t.id) FROM invoices t WHERE t.user_id = u.id)
    AND
    i2.id = (SELECT max(t.id) FROM invoices t WHERE t.user_id = u.id)
;

u mnie wszystko zadziałało ok.
jak widać (mam nadzieję) – generate_series potrafi sporo. generowanie list liczb pozwala na robienie np. robienie zestawień, włącznie z wyświetlaniem sum danych nieistniejących, sprawdzanie braków, "zgadywanie" pewnych wartości.
pozostaje zachęcić was do testowania i bawienia się – to czym i jak się bawicie jest niejednokrotnie ważniejsze od finalnego efektu – efekt jest jednorazowy. zabawa owocuje wiedzą która zostaje i kiedyś sie przyda.
aha. ważna notatka – generate_series zostało dodane w 8.0. no ale przecież chyba nikt już nie używa baz starszych 🙂 (żartuję oczywiście)

mysql – skalowalność. kolejna odsłona

napisałem ostatnio o kolejnych testach jakie robili kolesie z tweakers.net.
bluszcz wtedy skomentował, że chciałby zobaczyć testy postgresa z soliddb lub choćby falconem, a nie z kiepskawym innodb.
prosił i ma 🙂 blog mysql performance przeprowadził testy innodb, myisam i falcona. właśnie pod kątem skalowalności. efekt – ogólna, praktycznie całkowita porażka falcona.
przykłady wykres wyników jednego z testów wygląda tak:

przy czym uwaga: nie wybrałem takiego na którym falcon wyszedł najgorzej. są takie wykresy gdzie linia od wydajności falcona "leży" na osi.
do tego dochodzi jeszcze takie jedno zdanie:
"… Falcon cannot handle LIMIT properly …" (wyjęte z kontekstu, ale sens niezmieniony).
czekam teraz aż ktoś pokaże testy z soliddb. wtedy też na pewno ja zaprezentuję.