Jak pracować z bazami danych?

Zdecydowana większość funkcji w programie R wymaga by dane na których pracujemy były w pamięci RAM. A jeżeli chcemy pracować z dużymi zbiorami danych, które zajmują dużo miejsca, to mamy dwie możliwości:

  • pracować na ,,tłustych’’ komputerach z dużą ilością RAM (jednak na dzień dzisiejszy dzisiaj trudno wyjść poza 1 TB RAM, chyba że ma się baaardzo duży budżet),
  • trzymać dane poza R, możliwie dużo przetwarzania wykonywać poza R, a do R wczytywać tylko takie dane, które są naprawdę niezbędne. Poza R, oznacza tutaj w bazie danych.

Nawet jeżeli danych nie jest bardzo dużo, to bazy dane mogą być używane by zapewnić jednolity sposób dostępu do danych z różnych narzędzi, by efektywnie zarządzać danymi, by zapewnić skalowalność operacji na danych.

Poniżej przedstawiamy dwa przykłady komunikacji R z bazami danych. Pierwszy przykład będzie dotyczył prostej bazy danych SQLite bazującej na jednym pliku. Są to najczęściej zabawkowe przykłady pozwalające na przećwiczenie podstawowych operacji. Drugi przykład będzie dotyczył pracy z popularną bazą PostgreSQL. Sposób pracy z nią jest bardzo podobny do większości popularnie stosowanych baz relacyjnych.

Jak pracować z bazą danych SQLite?

SQLite to lekka baza danych oparta o jeden plik. Ma ona dosyć ograniczone funkcjonalności, ale tak prosto ją zainstalować, że wręcz trudno powiedzieć w którym momencie się to robi. Łatwo też tę bazę skopiować czy komuś wysłać. Dlatego pomimo ograniczonych możliwości ma ona sporo zastosowań w których liczby się prostota nad skalowalnością.

Aby korzystać z tej bazy danych potrzebny jest pakiet RSQLite. Korzystanie z tej bazy składa się z następujących kroków

  • Należy wczytać sterownik do łączenia się z bazą danych, najczęściej funkcją dbDriver().
  • Należy nawiązać połączenie z bazą danych, najczęściej funkcją dbConnect(). W przypadku bazy SQLite wystarczy wskazać ścieżkę do pliku z bazą danych.
  • Operacje na bazie danych wykonuje się poprzez nawiązane połączenie.
  • Po zakończeniu pracy z bazą danych należy zwolnić połączenie (funkcja dbDisconnect()) i sterownik.

Przykładowa sesja z bazą danych jest następująca.

Ładujemy sterownik do bazy danych i inicjujemy połączenie z serwerem bazodanowym. Jeżeli wskazany plik nie istnieje, to zostanie stworzony z pustą bazą danych.

library("RSQLite")
sterownik <- dbDriver("SQLite")
polaczenie <- dbConnect(sterownik, "zabawka.db")

Wyświetlamy tabele widoczne w bazie danych pod wskazanym połączeniem a następnie wyświetlamy kolumny w określonej tabeli.

dbListTables(polaczenie)
## [1] "auta2012"     "sqlite_stat1" "wynik"
dbListFields(polaczenie, "auta2012")
##  [1] "Cena"                         "Waluta"                      
##  [3] "Cena.w.PLN"                   "Brutto.netto"                
##  [5] "KM"                           "kW"                          
##  [7] "Marka"                        "Model"                       
##  [9] "Wersja"                       "Liczba.drzwi"                
## [11] "Pojemnosc.skokowa"            "Przebieg.w.km"               
## [13] "Rodzaj.paliwa"                "Rok.produkcji"               
## [15] "Kolor"                        "Kraj.aktualnej.rejestracji"  
## [17] "Kraj.pochodzenia"             "Pojazd.uszkodzony"           
## [19] "Skrzynia.biegow"              "Status.pojazdu.sprowadzonego"
## [21] "Wyposazenie.dodatkowe"

Używając funkcji dbGetQuery() możemy wykonać na bazie zapytanie SQL.

pierwsze5 <- dbGetQuery(polaczenie, 
           "select Cena, Waluta, Marka, Model from auta2012 limit 5")
pierwsze5
##    Cena Waluta         Marka     Model
## 1 49900    PLN           Kia    Carens
## 2 88000    PLN    Mitsubishi Outlander
## 3 86000    PLN     Chevrolet   Captiva
## 4 25900    PLN         Volvo       S80
## 5 55900    PLN Mercedes-Benz  Sprinter
agregat <- dbGetQuery(polaczenie, 
           "select count(*) as liczba, avg(`Cena.w.PLN`) as cena, Marka from auta2012 group by Marka limit 10")
agregat
##    liczba      cena       Marka
## 1      24  32735.33            
## 2      59  68140.39       Acura
## 3      50  27037.04       Aixam
## 4    2142  21403.58   AlfaRomeo
## 5       4  15424.42         Aro
## 6      35 505359.61 AstonMartin
## 7   12851  64608.61        Audi
## 8      17  46396.60      Austin
## 9   10126  72385.68         BMW
## 10     39 478483.82     Bentley

Używając funkcji dbDisconnect() możemy się z bazą danych rozłączyć. Ważne jest by po sobie sprzątać na wypadek gdyby dane z pamięci nie zostały zapisane do pliku.

dbDisconnect(polaczenie)
## [1] TRUE

Funkcja dbGetQuery() tworzy zapytanie, wykonuje je i pobiera jego wyniki. Tę operację można rozbić na części. Funkcja dbSendQuery() jedynie tworzy i wysyła zapytanie SQL do bazy, a funkcja fetch() pobiera kolejne porcje danych.

Funkcja dbWriteTable() zapisuje wskazany obiekt data.table jako tabelę w bazie danych.

Jak pracować z relacyjnymi bazami danych?

SQLite to baza zabawka. Do przechowywania większych danych w produkcyjnych rozwiązaniach wykorzystywać można otwarte rozwiązania typu PostgreSQL czy MySQL lub komercyjnie rozwijane silniki bazodanowe takie jak Oracle, RedShift, Teradata, Netezza i inne.

O ile te bazy różnią się funkcjonalnością, skalowalnością i prędkością, to z perspektywy użytkownika R korzystanie z nich jest dosyć podobne. Poniżej pokażemy jak korzystać z PostgreSQL.

Jako przykład, wykorzystamy bazę PostgreSQL dostępną na serwerze services.mini.pw.edu.pl. PostgreSQL pozwala na zarządzanie wieloma użytkownikami i wieloma bazami danych, tutaj wykorzystamy bazę sejmrp przechowującą dane z Sejmu RP 7 i 8 kadencji (głosowania i stenogramy). Dane te są uzupełniane za pomocą pakietu sejmRP.

Aby korzystać z bazy danych potrzebny jest użytkownik i hasło. Poniżej przedstawimy przykład dla użytkownika reader i hasła qux94874. Ten użytkownik ma wyłącznie uprawnienia do czytania.

Aby połączyć się z bazą PostgreSQL potrzebujemy sterownika, który jest dostępny w pakiecie RPostgreSQL. Wczytajmy ten pakiet i nawiążmy połączenie z bazą danych.

library(RPostgreSQL)
dbname = "sejmrp"
user = "reader"
password = "qux94874"
host = "services.mini.pw.edu.pl"

sterownik <- dbDriver("PostgreSQL")
polaczenie <- dbConnect(sterownik, dbname = dbname, user = user, password = password, host = host)

Możemy teraz zadawać dowolne zapytania SQL, pobierać i wysyłać całe tabele z danymi.

gadki <- dbGetQuery(polaczenie, "SELECT * FROM statements ORDER BY nr_term_of_office, id_statement limit 1")
gadki
##   id_statement nr_term_of_office                    surname_name
## 1    100.1.001                 7 Sekretarz Poseł Marek Poznański
##   date_statement titles_order_points
## 1     2015-09-16                    
##                                                                                          statement
## 1 Informuję, że w dniu dzisiejszym o godz. 16 odbędzie się posiedzenie Komisji  Zdrowia. Dziękuję.
glosy <- dbGetQuery(polaczenie, "SELECT club, vote, count(*) FROM votes GROUP BY club, vote limit 12")
glosy
##       club          vote  count
## 1       ZP       Przeciw   8448
## 2       PO       Przeciw 827472
## 3     KPSP       Przeciw   5659
## 4    niez.     Nieobecny  12418
## 5       PO Wstrzymał się   3291
## 6       ZP Wstrzymał się   3472
## 7  Kukiz15            Za   4495
## 8    niez.            Za  27274
## 9     KPSP Wstrzymał się   2699
## 10 Kukiz15     Nieobecny    893
## 11     PSL       Przeciw 120436
## 12     PiS Wstrzymał się 111698

Na koniec pracy należy rozłączyć się z bazą danych i zwolnić połączenie.

dbDisconnect(polaczenie)
## [1] TRUE

Jak używać pakietu dplyr w pracy z bazami danych?

Standardów i implementacji SQLa jest tak wiele, że zastanawiające jest dlaczego nazywane są standardami. Praktycznie każda baza danych różni się listą zaimplementowanych funkcjonalności czy agregatów. Jeżeli pracujemy z jedną bazą danych to ta różnorodność nie będzie nam doskwierać. Ale jeżeli przyjdzie nam jednocześnie korzystać z baz MSSQL, MySQL, RedShift i Postgres? Lub gdy okaże się, że dane zostały zmigrowane na nową bazę danych?

Wielu problemów można sobie oszczędzić używając pośrednika do komunikacji z bazą danych. Takim pośrednikiem może być pakiet dplyr omówiony w poprzednim rozdziale. Pozwala on do pewnego stopnia na pracę z danymi bez zastanawiania się gdzie te dane aktualnie są i jak nazywa się w tym systemie bazodanowym potrzebna nam funkcja.

W pakiecie dplyr połączenia ze źródłem danych (tabelą z liczbami lub bazą danych) tworzy się funkcjami src_*. Poniżej skorzystamy z funkcji src_sqlite() i src_postgres().

Zainicjujmy połączenie z bazą SQLite i pobierzmy kilka pierwszych wierszy z tabeli auta2012.

 library(dplyr)
polaczenie <- src_sqlite(path = 'zabawka.db')
auta1 <- tbl(polaczenie, "auta2012")

Mając taki obiekt, reszta operacji wygląda tak jak w zwykłym dplyr.

auta1 %>% 
  head(2)
##    Cena Waluta Cena.w.PLN Brutto.netto  KM  kW      Marka     Model Wersja
## 1 49900    PLN      49900       brutto 140 103        Kia    Carens       
## 2 88000    PLN      88000       brutto 156 115 Mitsubishi Outlander       
##   Liczba.drzwi Pojemnosc.skokowa Przebieg.w.km          Rodzaj.paliwa
## 1          4/5              1991         41000 olej napedowy (diesel)
## 2          4/5              2179         46500 olej napedowy (diesel)
##   Rok.produkcji Kolor Kraj.aktualnej.rejestracji Kraj.pochodzenia
## 1          2008                           Polska                 
## 2          2008                           Polska                 
##   Pojazd.uszkodzony Skrzynia.biegow Status.pojazdu.sprowadzonego
## 1                          manualna                             
## 2                          manualna                             
##                                                                                                                                                                                Wyposazenie.dodatkowe
## 1                         ABS, el. lusterka, klimatyzacja, alufelgi, centralny zamek, autoalarm, poduszka powietrzna, radio / CD, wspomaganie kierownicy, immobiliser, komputer, przyciemniane szyby
## 2 ABS, 4x4, el. lusterka, klimatyzacja, skorzana tapicerka, alufelgi, centralny zamek, poduszka powietrzna, radio / CD, wspomaganie kierownicy, immobiliser, komputer, tempomat, przyciemniane szyby

A teraz pokażmy przykład dla bazy PostgreSQL. Zainicjujmy połączenie do tabeli votes.

polaczenie <- src_postgres(dbname = dbname, 
                        host = host, user = user, password = password)
src_tbls(polaczenie)
## [1] "votes"            "counter"          "statements"      
## [4] "deputies"         "votings"          "votes_copy_27_08"
## [7] "db"               "test_statements"
glosy <- tbl(polaczenie, "votes")

Zdefiniowawszy połączenie możemy na tej tabeli robić dosyć zaawansowane rzeczy używając już znanych funkcji z pakietu dplyr.

glosy %>% 
  group_by(club, vote) %>% 
  summarise(liczba = n()) ->
  liczba_glosow

class(liczba_glosow)
## [1] "tbl_postgres" "tbl_sql"      "tbl"

Wynikiem tych operacji jest obiekt klasy tbl_sql. Nie przechowuje on jednak danych, ale instrukcje pozwalające na dostęp do danych (zapytanie SQL). Można to zapytanie i plan zapytania wyłuskać

liczba_glosow$query
## <Query> SELECT "club", "vote", "liczba"
## FROM (SELECT "club", "vote", count(*) AS "liczba"
## FROM "votes"
## GROUP BY "club", "vote") AS "zzz3"
## <PostgreSQLConnection:(42999,5)>
explain(liczba_glosow)
## <SQL>
## SELECT "club", "vote", "liczba"
## FROM (SELECT "club", "vote", count(*) AS "liczba"
## FROM "votes"
## GROUP BY "club", "vote") AS "zzz3"
##
## <PLAN>
## HashAggregate  (cost=74577.20..74577.72 rows=52 width=9)
##   ->  Seq Scan on votes  (cost=0.00..51780.54 rows=3039554 width=9)

Leniwość ale nie lenistwo. Operacje na tych obiektach nie są materializowane o ile nie muszą być materializowane (użytkownik wprost tego zażąda). Gdy już jest jasne co użytkownik chce zrobić i gdy jawnie zażąda wyniku, wszystkie operacje są wykonywane w możliwie małej liczbie (=jeden) kroków.

Materializować wyniki można na dwa sposoby

  • collect() - wyznacza wynik oraz pobiera do R,
  • compute() - wyznacza wynik i zapisuje w tymczasowej tabeli w bazie danych.

Poniższa instrukcja pobierze wyliczone agregaty ze zbioru danych.

collect(liczba_glosow)
## Source: local data frame [60 x 3]
## Groups: club [15]
## 
##       club          vote liczba
##      (chr)         (chr)  (dbl)
## 1       ZP       Przeciw   8448
## 2       PO       Przeciw 827472
## 3     KPSP       Przeciw   5659
## 4    niez.     Nieobecny  12418
## 5       PO Wstrzymał się   3291
## 6       ZP Wstrzymał się   3472
## 7  Kukiz15            Za   4495
## 8    niez.            Za  27274
## 9     KPSP Wstrzymał się   2699
## 10 Kukiz15     Nieobecny    893
## ..     ...           ...    ...

Więcej informacji o funkcjach z pakietu dplyr, które można stosowac do baz danych znaleźć można na stronie http://cran.rstudio.com/web/packages/dplyr/vignettes/databases.html.

Do ćwiczeń

Czego potrzebuje użytkownik mi2user?

digest::digest("All you need is R")
## [1] "bdb54d9c58b91c382f58423bec2bf5f0"

I funkcji copy_to, która zapisuje lokalną tabelę do bazy danych.