Obsah:
Doteď jsme na kurzu pracovali se soubory - odněkud soubor stáhli na svůj počítač, a tam ho zpracovali, a pak třeba zase výsledek někam nahráli. Tento postup ale nestačí, pokud jsou data příliš veliká, s těmito daty pracuje naráz více lidí najednou a zároveň je i aktualizuje. To se řeší tak, že spustíte databázový systém (DBMS), ten data u sebe spravuje a vy se na tento systém připojujete přes klienta. DBMS může běžet na stejném počítači jako váš Python - nebo také úplně jinde.
Např. e-shop bude pracovat s databází, ve které jsou uloženy informace o prodávaném zboží, rozdělení zboží do kategorií, objednávky, reklamace, registrovaní uživatelé... Takovou databázi můžeme označit za "transakční". Vy jako datoví analytici, inženýři apod. se pravděpodobně budete chtít do takovéto databáze připojit a stáhnout si z ní část dat pro svou další práci. Nebo dokonce může být vaším úkolem vytvořit novou "analytickou" databázi, která obsahuje data v jiné podobě, např. prodávané zboží bez popisků, bez fotografií, ale zato se statistikami návštěvnosti a prodejnosti.
Od databáze (resp. databázového systému) očekáváme několik vlastností:
Když se nad tím zamyslíte, jedná se o protichůdné požadavky. Tvůrci databázových systémů tak musí zvolit nějaký kompomis. Žádná databáze není "nejlepší" a vhodná na všechno. Zrovna co se týče rozdílu mezi "transakční" a "analytickou" databází, tak často pro tyto oblasti jsou vhodné odlišné systémy a přístupy.
Na začátku budeme pro jednoduchost pracovat s databází SQLite. Ta je poněkud specifická tím, že je uložena v jednom souboru (s příponou .sqlite
nebo .db
), není to tedy samostatně běžící server, ke kterému byste se připojovali. O to snadněji se ale s SQLite pracuje. A SQL jazyk, který si zde osvojíte je stejný (nebo minimálně velmi podobný) jako u složitějších databázových systémů jako MySQL nebo PostgreSQL.
SQL je jazyk, kterým komunikujeme s databází. Umí vyjádřit různé operace nad tabulkami - výběr sloupců, řádků, vztahy mezi více tabulkami, agregace hodnot z více řádků... Tedy něco, co jsem v Pythonu
Příklad:
SELECT title, year FROM movies;
UPDATE directors SET birthdate = "1950-01-02" WHERE director_id = 3;
Každá databáze se používá trochu jinak - liší se název modulu, který je potřeba importovat, mírně se liší způsob předávání dat použitých v SQL dotazech, může se lišit forma dat vrácených ve výsledku...
Tento problém řeší knihovna SQLAlchemy, která poskytuje stejné rozhraní pro všechny širokou třídu databází. Nejprve si ji tedy nainstalujte.
# smažte # na následujícím řádku pro instalaci balíčku sqlalchemy
# %pip install sqlalchemy
Knihovnu SQLAlchemy naimportujeme následovně
import sqlalchemy
a jako vždy, importujeme i Pandas:
import pandas as pd
Pro první ukázku práce s databází použijeme data od Míry Brabence. Jde o databázi typu SQLite - skládá se z jediného souboru, ve kterém jsou všecha data. To znamená, že se nepřipojujeme k žádnému serveru pomocí jména a hesla jako u "klasických" databází. Databáze v podobě SQLite souboru má výhodu v tom, že když něco rozbijete, tak ten soubor prostě smažete a uděláte si nový :)
# smažte # na následujících řádcích pro stažení dat movies.sqlite
#from pathlib import Path
#import requests
#db_data = requests.get('https://github.com/PyDataCZ/pyladies-kurz/raw/main/lessons/pydata/databases/movies.sqlite').content
#Path('movies.sqlite').write_bytes(db_data)
engine = sqlalchemy.create_engine('sqlite:///movies.sqlite')
Objekt engine
obsahuje připojení do databáze a skrz něj budeme provádět operace s danou databází.
Začneme tím, že se podíváme, jaká je struktura té databáze - jaké tabulky obsahuje a co obsahují ty tabulky. Každá databáze na to má jiné finty, jak toto zjistit (někdy funguje SQL příkaz SHOW TABLES
, jindy je zase potřeba SELECT
z metadat), naštěstí SQLAlchemy nám s tím pomůže.
sqlalchemy.inspect(engine).get_table_names()
Pro průzkum databáze jsem připravil funkci prozkoumat
.
def prozkoumat(engine):
inspector = sqlalchemy.inspect(engine)
for schema in inspector.get_schema_names():
print('Schema:', schema)
for table_name in inspector.get_table_names(schema=schema):
print()
print(' Table:', table_name)
print()
for column in inspector.get_columns(table_name, schema=schema):
print(' Column:', column['name'].ljust(12), column['type'])
prozkoumat(engine)
Celou tabulku stáhneme pomocí funkce pd.read_sql_table
.
pd.read_sql_table('actor', engine)
Reálná databáze ale může obsahovat tak velké, že je celé stáhnout nemůžete nebo nechcete. Stáhnete tedy pouze jejich část určenou pomocí SQL příkazu SELECT (funkce pd.read_sql_query
). Takže pokud např. chcete zjistit údaje k filmu "Chasing Amy".
pd.read_sql_query('SELECT title, yr, budget, gross FROM movie WHERE title = "Chasing Amy"', engine)
Jedna hodina je příliš málo na to, abychom probrali SQL a databáze do hloubky. Soustředíme se tedy na nejčastější případ použití: databázi nespravujete vy, ale nějaký datový inženýr z firmy a vy jste dostali práva pro čtení (nikoli pro zápis). Z databáze potřebujete získat data, abyste s nimi dále pracovali.
Učite se budeme na příkladech. Budeme však potřeboval ještě několik datových sad.
# ostraňte # na začátku následujících řádků pro stažení souboru world.sqlite
#db_data = requests.get('https://github.com/PyDataCZ/pyladies-kurz/raw/main/lessons/pydata/databases/world.sqlite').content
#Path('world.sqlite').write_bytes(db_data)
engine_world = sqlalchemy.create_engine('sqlite:///world.sqlite')
prozkoumat(engine_world)
pd.read_sql_query('SELECT * FROM world LIMIT 3', engine_world)
Upravte následující příklady a zkontrolujte, že jste dostali očekávaný výsledek. Nulté cvičení projdeme společně.
Cvičení 0: Kód níže získá z databáze počet obyvatel Francie. Upravte jej a zjistěte počet obyvatel Německa (řešení 80716000).
sql_statement = """
SELECT population FROM world
WHERE name = 'France'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 1: Pomocí operátoru IN můžete testovat, jestli sloupec nabývá jedné z několika hodnot. Kód níže získá z databáze tabulku se jménem státu a počtem obyvatel pro Brazílii, Rusko, Indii a Čínu.
Upravte jej a získejte počet obyvatel pro skandinávské země ('Sweden', 'Norway', 'Denmark'). Která z těchto tří zemí má nejméně obyvatel? (řešení: Norsko - 5124383)
sql_statement = """
SELECT name, population FROM world
WHERE name IN ('Brazil', 'Russia', 'India', 'China');
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 2: Které státy nejsou příliš malé, ani příliš velké? Funkce BETWEEN umožňuje zadat rozsah (hraniční hodnoty). Níže uvedený příklad zobrazuje státy o rozloze 200 000-250 000 km². Upravte jej tak, aby zobrazoval jméno státu a jeho plochu pro státy s rozlohou mezi 250 000 a 300 000 km². (řešení: Burkina Faso, Ecuador, Gabon, New Zealand)
sql_statement = """
SELECT name, area FROM world
WHERE area BETWEEN 200000 AND 250000
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 3: K nalezení států začínajících na písmeno B můžete použít WHERE name LIKE 'B%'
(%
nahradí cokoli).
Kolik je v tabulce států začínajících na Y? (řešení: je právě jeden)
sql_statement = """
SELECT name FROM world
WHERE name LIKE 'F%'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 4: Kolik je států, jejichž jméno končí písmenem y? (řešení: osm)
sql_statement = """
SELECT name FROM world
WHERE name LIKE 'F%'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 5: Kolik států má ve jméně písmeno x? Kolik jich končí -land? A kolik jich začíná písmenem C a končí -ia? (řešení: 2, 8, 3)
sql_statement = """
SELECT name FROM world
WHERE name LIKE 'F%'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 6: Indie a Angola mají jako druhý znak písmeno n. Podtržítko můžete použít jako zástupný znak pro právě jeden znak.
Kolik států má jako druhý znak písmeno t? (řešení: dva)
sql_statement = """
SELECT name FROM world
WHERE name LIKE '_n%'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 7: Kolik názvů států má právě 4 písmena? (řešení: 10)
sql_statement = """
SELECT name FROM world
WHERE name LIKE '___'
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 8: Hlavní město Luxembourg je Luxembourg. Kolik je států, kde se hlavní město jmenuje stejně jako stát sám? (řešení: čtyři)
sql_statement = """
SELECT name, capital, continent FROM world
"""
pd.read_sql_query(sql_statement, engine_world)
Pro komplexní dotazy potřebujeme často porovnat hodnotu s hodnotou, kterou samotnou musíme z databáze získat. K tomu slouží možnost vnořit jeden SELECT do druhého.
Cvičení 0: Které státy mají více obyvatel než Rusko? To už umíme - na dva dotazy... (povšimněte si použití ORDER - seřadí výsledek podle jednoho či více sloupců)
sql_statement = """
SELECT name, population FROM world
WHERE name='Russia'
"""
pd.read_sql_query(sql_statement, engine_world)
sql_statement = """
SELECT name FROM world
WHERE population > 146000000
ORDER by name
"""
pd.read_sql_query(sql_statement, engine_world)
Ale s vnořeným SELECT nám stačí dotaz jeden:
sql_statement = """
SELECT name FROM world
WHERE population >
(SELECT population FROM world
WHERE name='Russia')
ORDER BY name
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 1: Vypište evropské země, které mají HDP na osobu vyšší než Dánsko. (řešení: je jich šest)
sql_statement = """
SELECT name, gdp/population FROM world
WHERE continent = 'Asia' AND population >
(SELECT population FROM world
WHERE name='Russia')
"""
pd.read_sql_query(sql_statement, engine_world)
Můžeme použít MAX, MIN, SUM, AVG, COUNT pro maximum, minimum, součet, průměr a počet hodnot. Zemi s nejvyšším počtem obyvatel tedy nalezneme následovně.
sql_statement = """
SELECT name, population
FROM world
WHERE population = (SELECT MAX(population) FROM world)
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 2: Nalezněte státy, které mají HDP vyšší než libovolný evropský stát. (řešení: jsou tři)
sql_statement = """
SELECT name, gdp FROM world
WHERE gdp >
(SELECT gdp FROM world
WHERE name='Japan')
"""
pd.read_sql_query(sql_statement, engine_world)
Pokud chceme spočítat počet obyvatel Asie, můžeme použít SUM
a WHERE
z minulého oddílu.
sql_statement = """
SELECT continent, SUM(population)
FROM world
WHERE continent = "Asia"
"""
pd.read_sql_query(sql_statement, engine_world)
Přehled všech kontinentů můžeme dostat pomocí funkce DISTINCT
(v Pythonu existuje podobné unique
).
sql_statement = """
SELECT DISTINCT(continent)
FROM world
"""
pd.read_sql_query(sql_statement, engine_world)
Jak to ale udělat, abychom dostali počet obyvatel každého kontinentu a nemuseli to dělat naosmkrát? Řešení je stejně jako v Pandas použití GROUP BY
i s podobnout syntaxí.
sql_statement = """
SELECT continent, SUM(population)
FROM world
GROUP BY continent
"""
pd.read_sql_query(sql_statement, engine_world)
Pokud bychom chtěli vyloučit malé kontinenty jako Oceania a Caribbean, filtrování výsledků po GROUP BY
se provádí pomocí HAVING
, tedy například...
sql_statement = """
SELECT continent, SUM(population)
FROM world
GROUP BY continent
HAVING SUM(population) > 100000000
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 1: Kolik je pro každý kontinent států v databázi? Použijte funkci COUNT
(řešení: např. pro Evropu 44)
sql_statement = """
SELECT continent, SUM(population)
FROM world
GROUP BY continent
"""
pd.read_sql_query(sql_statement, engine_world)
Cvičení 2: Některé státy jsou velmi malé. Zkuste modifikovat kód na Cvičení 1 tak, aby počítal jen státy, které mají alespoň 10 milionů obyvatel. A kdybychom filtrovali nikoli malé státy, ale malé kontinenty? Kdy se použije WHERE, kdy HAVING, a proč? (řešení: např. pro Evropu 14)
sql_statement = """
SELECT continent, SUM(population)
FROM world
GROUP BY continent
"""
pd.read_sql_query(sql_statement, engine_world)
Spojování tabulek je velmi podobné jako v Pandas. Vraťme se nyní k databázi movie
. Obsahuje tři tabulky, které jsou propojené následovně...
Cvičení 0: Vypište všechny herce, kteří hráli ve filmu Vetřelec (Alien).
sql_statement = """
SELECT actor.name
FROM movie
JOIN casting ON movie.id = casting.movieid
JOIN actor ON actor.id = casting.actorid
WHERE title = 'Alien'
"""
pd.read_sql_query(sql_statement, engine)
Cvičení 1: Vypište všechny filmy, v kterých hrála 'Hilary Swank'. (řešení: je jich 16)
sql_statement = """
SELECT actor.name
FROM movie
JOIN casting ON movie.id = casting.movieid
JOIN actor ON actor.id = casting.actorid
WHERE title = 'Alien'
"""
pd.read_sql_query(sql_statement, engine)
Cvičení 2: Vypište všechny filmy, v kterých hrál 'Brad Pitt', ale ne v hlavních dvou rolích (ord
v casting listu je větší než 2). (řešení: je jich 12)
sql_statement = """
SELECT actor.name
FROM movie
JOIN casting ON movie.id = casting.movieid
JOIN actor ON actor.id = casting.actorid
WHERE title = 'Alien'
"""
pd.read_sql_query(sql_statement, engine)
Cvičení 3: Vypište název filmů, které byly uvedeny do kin v roce 1903, společně s hlavní hvězdou (ord
v castingu je rovno jedné). (řešení: je jich pět, hlavní roli v Alice in Wonderland hrála May Clark)
sql_statement = """
SELECT actor.name
FROM movie
JOIN casting ON movie.id = casting.movieid
JOIN actor ON actor.id = casting.actorid
WHERE title = 'Alien'
"""
pd.read_sql_query(sql_statement, engine)
Cvičení 4: Vypište všechny filmy, v kterých hrála Julie Andrews, a ke každému z nich uveďte i herce v hlavní roli.
Cviření 5: Najděte všechny herce, kteří hráli alespoň patnáctkrát v hlavní roli.
Cvičení 6: Vypište všechny filmy z roku 1978 seřazené podle počtu herců, kteří v nich hráli. V případě rovnosti řaďte podle názvu filmu.
Cvičení 7: Nalezněte všechny herce, kteří hráli ve filmu, v kterém hrál též Art Garfunkel.
Pokud se chcete dozvědět o SQL více, velmi doporučuji stránku https://sqlzoo.net/, z které jsem převzal databáze a většinu cvičení.
Většina databázových serverů není z bezpečnostních důvodů veřejně dostupných a přistupujete k nim skrz nějakou API službu (o těch si budeme vykládat příště). Pokud byste si ale chtěli vyzkoušet, jaké to je připojit se ke vzdálenému serveru, jeden z mála dostupných je Ensembl poskytující genomická data.
Možná vám přišlo, že dnešní SQL lekce byla velmi podobná prvním lekcím na Pandách. Jeden z významných rozdílů SQL tabulky je, že může obsahovat více indexů, zadímco pandas DataFrame má jen jeden. Dalším rozdílem je, že NA
se v jazyce SQL nazývá NULL
- testuje se na něj IS NULL, podobně jako v Pythonu.
Pokud vám SQL přijde intuitivní a chtěli byste takto pracovat i s Pandas tabulkami, je to možné pomocí knihovny pandasql.
A naopak, ve všech příkladech zde jsme používali SQLAlchemy v kombinaci s pandas. To ale vůbec není nutné, můžeme použít engine.execute(...)
a dostaneme iterátor reprezentující výsledek SQL dotazu. Například...
result = engine.execute('SELECT id, title, yr FROM movie LIMIT 5')
for row in result:
print(row['id'], row['title'], row['yr'])