Generowanie danych losowych dla PostgreSQL

Generowanie danych losowych dla PostgreSQL z wykorzystaniem EuroDB

Generowanie danych losowych dla PostgreSQL to temat niezwykle interesujący dla wielu osób związanych ze światem IT. Prawdopodobnie wiąże się to ściśle ze wzrostem znaczenia metodyki DevOps oraz związanego z nim rygoru testowania. W poprzedniej części cyklu omawialiśmy, jak można podejść do tego problemu od strony wykorzystania samego PostgreSQL. Zgodnie z zapowiedziami, w tym artykule zaprezentujemy […]

Generowanie danych losowych dla PostgreSQL to temat niezwykle interesujący dla wielu osób związanych ze światem IT. Prawdopodobnie wiąże się to ściśle ze wzrostem znaczenia metodyki DevOps oraz związanego z nim rygoru testowania. W poprzedniej części cyklu omawialiśmy, jak można podejść do tego problemu od strony wykorzystania samego PostgreSQL. Zgodnie z zapowiedziami, w tym artykule zaprezentujemy rozwinięcie tego tematu w oparciu o jeden z ciekawszych komponentów EuroDB – moduł do generowania danych.

EuroDB Data Generator to stosunkowo proste, lecz bardzo potężne narzędzie umożliwiające łatwe generowanie ogromnych ilości danych losowych dla PostgreSQL. Można powiedzieć, że w pod tym względem doskonale wpisuje się w jedno z założeń – „do one thing and do it well” – przyświecającego twórcom Unixa.

Data Generator działa w oparciu o pojęcie generatorów dla poszczególnych typów danych. Narzędzie rozpoznaje bogatą listę zdefiniowanych typów znanych z silnika PostgreSQL, które pozwalają na generowanie danych bez potrzeby pisania ani jednej linijki kodu. Jedynym potrzebnym argumentem będzie plik z definicjami tabel, dla których mają zostać stworzone dane. Wykorzystując standardowe narzędzie pg_dump można w prosty sposób stworzyć taki plik.

Podstawowe generowanie danych losowych dla PostgreSQL

By zademonstrować działanie Data Generatora, posłużę się prostym schematem, dla którego wygeneruję dane testowe:

CREATE TABLE app_user(
    id serial PRIMARY KEY,
    username text NOT NULL,
    firstname text,
    lastname text,
    email text NOT NULL,
    birthday date,
    is_active boolean NOT NULL,
    created timestamp NOT NULL,
    last_login timestamp NOT NULL,
    last_ip inet NOT NULL
);

Dla pełności, poniżej zaprezentuję jeszcze, w jaki sposób można wyciągnąć te informacje z działającego serwera korzystającego z silnika PostgreSQL:

$ pg_dump -s -O -x -d NAZWA_BAZY

Opcje kolejno odpowiadają za wypisywanie definicji poszczególnych obiektów (-s) oraz ograniczają dane przez zablokowanie dodawania informacji o właścicielach (-O), a także o rolach i uprawnieniach (-x).

Mając plik wejściowy z definicją tabeli jak wyżej o nazwie app_user.sql, możemy po prostu wywołać:

eurodb-datagenerator app_user.sql

Doprowadzi to do wygenerowania danych zgodnie z domyślnymi ustawieniami programu. Na tym etapie istotne jest jeszcze sterowanie ilością wygenerowanych rekordów. Domyślna wartość odpowiadającego za to parametru --size to 100. Przyjrzyjmy się jednak przykładowym wynikom dla analogicznego polecenia z ograniczeniem ilości generowanych danych:

-- eurodb-datagenerator --size 5 app_user.sql

-- data generated by /usr/bin/eurodb-datagenerator version 2.0.0 (r792 on 2014-03-23) for postgresql

-- fill table app_user (5)
\echo # filling table app_user (5)
COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1	username	firstname_1_	lastname_1	email_1_1_1_1_1_	2019-05-18	FALSE	2019-05-20 13:59:45	2019-05-20 14:03:45	0.0.0.4
2	username_2_2_2_	firstname_2_2_2	lastname_5_	email_2_2_2	2019-05-18	TRUE	2019-05-20 13:59:45	2019-05-20 13:59:45	0.0.0.3
3	username_3_3_	firstname_1_	lastname_3_3_3_	email_3_3_3_3_3	2019-05-17	TRUE	2019-05-20 14:00:45	2019-05-20 13:59:45	0.0.0.2
4	username_4_	firstname_5_5_5	lastname_4_4_	email_4_4_	2019-05-20	FALSE	2019-05-20 14:01:45	2019-05-20 14:00:45	0.0.0.0
5	username_	firstname_5_5_5	lastname_3_3_3_	email_5_5_5_5_	2019-05-19	FALSE	2019-05-20 14:01:45	2019-05-20 14:02:45	0.0.0.3
\.

-- restart sequences
ALTER SEQUENCE app_user_id_seq RESTART WITH 6;

-- analyze modified tables
ANALYZE app_user;

Jak widać, Data Generator korzysta z postgresowego polecenia COPY, które jest zdecydowanie szybsze od standardowego INSERT. Jest to szczególnie istotne, gdy generujemy dużo większe ilości danych niż w prezentowanych tu przykładach.

Kolejną rzeczą, którą widać, jest format danych. Dla kolumn tekstowych domyślnym zachowaniem jest po prostu wykorzystanie nazwy kolumny oraz dodatkowych losowych znaków. Daty urodzenia również rzucają się w oczy – zakres, z którego pochodzą, jest zdecydowanie zbyt bliski, aby można go było uznać za potencjalne urodziny prawdziwej osoby korzystającej z naszej aplikacji.

Pomimo pewnych uwag wygenerowaliśmy jednak dane na podstawie samej definicji tabeli w SQL, bez potrzeby pisania własnego skryptu. Moglibyśmy w tym momencie skończyć – bez żadnego wysiłku udało nam się odtworzyć funkcjonalnie równoważne dane do tych z poprzedniego postu, a nawet zrobić nieco więcej. Przyjrzyjmy się jednak jeszcze dokładniej innym możliwościom EuroDB Data Generatora.

Poprawianie wygenerowanych danych przy pomocy dyrektyw

Jak widać, włączenie programu do procesu testowania w jego najprostszej formie jest bardzo łatwe. Wiadomo jednak, że prawdziwe dane produkcyjne nie wyglądają w ten sposób i byłoby dobrze, gdyby dane testowe lepiej je odzwierciedlały w środowisku testowym. Szczególnie jeśli aplikacja, z której korzystamy, ma być testowana w najdokładniejszy sposób.

Z pomocą przychodzą nam dyrektywy Data Generatora, które służą do sterowania generowaniem. W ten sposób możemy na przykład określić rozkład, który mają przyjąć dane, stworzyć w deklaracyjny sposób własny generator czy też odpowiednio zmodyfikować argumenty wbudowanych generatorów.

Wykorzystanie wyrażeń regularnych

Korzystając z naszego przykładu, spróbujmy zmienić zachowanie Data Generatora podczas tworzenia adresu e-mail. By to zrobić, wystarczy umieścić komentarz SQL przy definicji konkretnej kolumny:

email text NOT NULL, --df: pattern='[a-z]{3,8}\.[a-z]{3,8}@(qmail|inbox)\.com'

Na początku komentarza znajduje się magiczny ciąg znaków, który pozwala na sterowanie generowaniem. Następnie deklarujemy, który z wbudowanych generatorów ma zostać wykorzystany oraz przekazujemy do niego odpowiedni argument.
Kolejne wywołanie daje bardziej satysfakcjonujący wynik, który wygląda jak adres e-mail:

-- eurodb-datagenerator --size 5 app_user.SQL

COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1	username	firstname_1_	lastname_5_	[email protected]	2019-05-17	FALSE	2019-05-20 16:29:43	2019-05-20 16:31:43	0.0.0.3
2	username_2_2_2_	firstname	lastname_3_3_3_	[email protected]	2019-05-17	FALSE	2019-05-20 16:29:43	2019-05-20 16:30:43	0.0.0.1
3	username_3_3_	firstname	lastname_3_3_3_	[email protected]	2019-05-16	TRUE	2019-05-20 16:29:43	2019-05-20 16:29:43	0.0.0.4
4	username_4_	firstname_5_5_5	lastname_3_3_3_	[email protected]	2019-05-16	FALSE	2019-05-20 16:30:43	2019-05-20 16:30:43	0.0.0.2
5	username_	firstname_5_5_5	lastname_5_	[email protected]	2019-05-17	TRUE	2019-05-20 16:31:43	2019-05-20 16:29:43	0.0.0.0
\.

Jak można się domyślić, wbudowany generator pattern działa w oparciu o wzorzec regularny, do którego zostaną wygenerowane wartości. W tym wypadku wykorzystaliśmy bardzo prostego regexpa, który opisuje niewielki podzbiór ze wszystkich możliwych adresów e-mail. W zależności od potrzeb można zdefiniować bardziej rozbudowane wzorce.

Wykorzystanie list słów

Spróbujmy teraz stworzyć bardziej rozróżnialne wartości dla kolumny username. W tym celu możemy skorzystać na przykład z listy słów, które można znaleźć w pakiecie words. W EuroLinuxie oraz podobnych systemach po jego instalacji uzyskujemy dostęp do pokaźniej listy angielskich słów w katalogu /usr/share/dict/. By wykorzystać te dane podczas generowania, wystarczy dopisać:

--df English: word=/usr/share/dict/words sub=uniform
username text NOT NULL, --df: text=English length=1 lenvar=0

Aby uzyskać bardziej interesujące rezultaty:

COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1	smackee	firstname_1_	lastname_1	[email protected]	2019-05-20	TRUE	2019-05-20 17:03:57	2019-05-20 17:05:57	0.0.0.4
2	shivaite	firstname_4_	lastname	[email protected]	2019-05-20	FALSE	2019-05-20 17:06:57	2019-05-20 17:03:57	0.0.0.0
3	barish	firstname_2_2_2	lastname	[email protected]	2019-05-18	TRUE	2019-05-20 17:05:57	2019-05-20 17:06:57	0.0.0.2
4	syncategorematic	firstname_1_	lastname_3_3_3_	[email protected]	2019-05-20	FALSE	2019-05-20 17:06:57	2019-05-20 17:03:57	0.0.0.3
5	slugs	firstname_4_	lastname_1	[email protected]	2019-05-17	TRUE	2019-05-20 17:03:57	2019-05-20 17:03:57	0.0.0.4
\.

W tym momencie warto jeszcze wytłumaczyć poszczególne elementy składni. Otóż pierwsza deklaracja w komentarzu deklaruje makro English, które posłuży w następnej deklaracji jako źródło danych do generatora text. Przy pomocy argumentu sub definiujemy rozkład, z jakim mają się pojawiać poszczególne elementy pliku. W tym wypadku jest to rozkład jednostajny (uniform). Stąd mając do dyspozycji na przykład prawdziwe dane z jakiejś kolumny oraz rozkład jaki przyjmują w bazie, moglibyśmy wygenerować bardzo podobne wyniki. Jednym z przykładów może być rozkład wieku użytkowników, który można wymodelować przy pomocy dostępnych funkcji.

Określanie zakresu dat

Wróćmy więc do generowania dat z zadanego przedziału, tak aby uwiarygodnić to, co zostanie wygenerowane. Zakładając, że z aplikacji mogą korzystać tylko osoby pełnoletnie, należy podać argumenty w następujący sposób:

birthday date, -- df: start=1955-05-21 end=2001-05-21 sub=power alpha=0.5

Parametry start i end oczywiście definiują zakres. Przy pomocy sub=power określamy dystrybucję, której parametrem w tym wypadku będzie alpha. Dzięki temu wyniki będą bardziej interesujące niż standardowy rozkład jednostajny.

\echo # filling table app_user (5)
COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1	bivector	firstname_5_5_5	lastname_4_4_	[email protected]	1999-11-14	TRUE	2019-05-21 10:16:52	2019-05-21 10:17:52	0.0.0.4
2	shallows	firstname_1_	lastname_5_	[email protected]	2000-12-17	FALSE	2019-05-21 10:14:52	2019-05-21 10:16:52	0.0.0.2
3	Boule	firstname_5_5_5	lastname_3_3_3_	[email protected]	1999-11-21	FALSE	2019-05-21 10:13:52	2019-05-21 10:14:52	0.0.0.3
4	metasoma	firstname_1_	lastname_4_4_	[email protected]	1996-03-14	TRUE	2019-05-21 10:13:52	2019-05-21 10:15:52	0.0.0.2
5	entrochite	firstname	lastname_4_4_	[email protected]	1997-03-25	FALSE	2019-05-21 10:17:52	2019-05-21 10:14:52	0.0.0.3
\.

Na koniec można by jeszcze wygenerować imiona i nazwiska, które wyglądają bardziej naturalnie. Z pomocą przyjdą nam odpowiednie listy słów. Można oczywiście skorzystać z własnoręcznie skompletowanej listy popularnych imion czy nazwisk. Ja skorzystam z udostępnianej w domenie publicznej listy Moby Project zawierającej angielskie imiona. Dla nazwisk skorzystam z listy nazw miejscowości, co powinno dać całkiem wiarygodne rezultaty. Podsumowując, nasze reguły wyglądają więc następująco:

CREATE TABLE app_user(
    id serial PRIMARY KEY,
    --df English: word=/usr/share/dict/words sub=uniform
    username text NOT NULL, --df: text=English length=1 lenvar=0
    --df Names: word=/tmp/names.txt sub=uniform
    firstname text, --df: text=Names length=1 lenvar=0
    --df Places: word=/tmp/places.txt sub=uniform
    lastname text, --df: text=Places length=1 lenvar=0
    email text NOT NULL, --df: pattern='[a-z]{3,8}\.[a-z]{3,8}@(qmail|inbox)\.com'
    birthday date, -- df: start=1955-05-21 end=2001-05-21 sub=power alpha=0.5
    is_active boolean NOT NULL,
    created timestamp NOT NULL,
    last_login timestamp NOT NULL,
    last_ip inet NOT NULL
);

Natomiast po uruchomieniu generatora otrzymamy następujące wyniki:

\echo # filling table app_user (5)
COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1       metagalactic    John Bramwell    [email protected]    1988-12-21    FALSE    2019-06-04    10:08:48    2019-06-04    10:06:48    0.0.0.3
2       splinter-bar    Mary Lacoste     [email protected]   1988-01-11    TRUE     2019-06-04    10:07:48    2019-06-04    10:07:48    0.0.0.1
3       distinctions    Diana Farnam     [email protected]  1993-01-12    TRUE     2019-06-04    10:07:48    2019-06-04    10:07:48    0.0.0.4
4       sow             Wilson Orford    [email protected]    1967-02-23    TRUE     2019-06-04    10:06:48    2019-06-04    10:08:48    0.0.0.2
5       Magi            Bran Eustis      [email protected]      1985-07-12    FALSE    2019-06-04    10:09:48    2019-06-04    10:07:48    0.0.0.2
\.

Bonus – generator w generatorze

Ostatnią bardzo przydatną funkcją, którą chciałbym tutaj pokazać, jest możliwość osadzania generatora w generatorze. W przypadku generowania adresów e-mail znów można skorzystać z listy słów, aby adres e-mail lepiej zapadał w pamięć. Dodatkowo wykorzystajmy do tego celu prezentowany już generator text.

--df English: word=/usr/share/dict/words
email text NOT NULL, -- df: pattern='[:text=English length=1 lenvar=0:](\.\d{2,4})?@(qmail|inbox).com'

Jak widać, by zagnieździć inny generator, wystarczy umieścić go pomiędzy [: i :].

\echo # filling table app_user (5)
COPY app_user (id,username,firstname,lastname,email,birthday,is_active,created,last_login,last_ip) FROM STDIN (ENCODING 'UTF-8');
1	achymous	firstname_2_2_2	lastname_5_	[email protected]	1992-05-19	FALSE	2019-05-21 10:35:07	2019-05-21 10:38:07	0.0.0.1
2	alundum	firstname_1_	lastname_4_4_	[email protected]	1988-04-16	FALSE	2019-05-21 10:35:07	2019-05-21 10:39:07	0.0.0.3
3	coranoch	firstname_4_	lastname_3_3_3_	[email protected]	1999-07-02	FALSE	2019-05-21 10:39:07	2019-05-21 10:39:07	0.0.0.3
4	colored	firstname	lastname_3_3_3_	[email protected]	1993-10-10	FALSE	2019-05-21 10:36:07	2019-05-21 10:36:07	0.0.0.0
5	bestripe	firstname	lastname_4_4_	[email protected]	1987-03-31	TRUE	2019-05-21 10:37:07	2019-05-21 10:35:07	0.0.0.3
\.

Podsumowanie

Mam nadzieję, że udało mi się Was zaciekawić potężnymi możliwościami, jakie daje wykorzystanie EuroDB Data Generatora podczas generowania danych losowych. Warto wspomnieć, że ten artykuł opisuje tylko niektóre funkcje i parametry konfiguracyjne, które pozwalają na wygenerowanie dokładnie takich danych, jakich wymaga projekt. Dostępnych możliwości jest o wiele więcej, niż można zmieścić w krótkiej formie artykułu. Dlatego w tym miejscu chciałbym zachęcić jeszcze do zapoznania się z naszymi autoryzowanymi kursami administracyjnymi, podczas których można zgłębić tematykę generowania danych oraz nauczyć się w pełni wykorzystywać możliwości wszystkich modułów dodatkowych EuroDB.

Chcesz być na bieżąco? Obserwuj nasz profil w serwisie LinkedIn.

Autorzy

Artykuły na blogu są pisane przez osoby z zespołu EuroLinux. 80% treści zawdzięczamy naszym developerom, pozostałą część przygotowuje dział sprzedaży lub marketingu. Dokładamy starań, żeby treści były jak najlepsze merytorycznie i językowo, ale nie jesteśmy nieomylni. Jeśli zauważysz coś wartego poprawienia lub wyjaśnienia, będziemy wdzięczni za wiadomość.