Nauč se Python > Kurzy > Datový kurz PyLadies > Databáze > Databáze a SQL

Databáze

Obsah:

  • Úvod do databází
  • Úvod do relačních databází
  • Úvod do SQL + Python a Pandas

Úvod do databází

Doteď jste na kurzu zřejmě 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 s těmito daty potřebuje pracovat více lidí (nebo strojů) najednou a zároveň je i aktualizovat. To se řeší tak, že spustíte databázový systém (DBMS, SŘBD), ten ta data u sebe spravuje (soubory na disku obsahující specializované datové struktury) a vy se na tento systém připojujete přes počítačovou síť. Nebo třeba používáte webovou aplikaci a ta je na tento databázový systém (databázi) připojena.

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.

V těchto materiálech budeme 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, jak je tomu u většiny databází. O to snadněji se ale s SQLite pracuje. I SQLite umožňuje pracovat s jednou databází (jedním souborem) z několika programů zároveň, narozdíl od např. CSV nebo XLS sobourů.

Vlastnosti databází

Od databáze (resp. databázového systému) očekáváme několik vlastností:

  • Že bude uchovávat data :) Ne, vážně - data musí zůstat beze ztráty uložena i v případě výpadku napájení, když dojde místo na disku... Že existují nástroje pro zálohování a obnovu databáze.
  • Práce s daty "z různých úhlů". Např. v e-shopu chceme jednou vypsat zboží podle kategorie, jindy podle stavu skladových zásob, jindy na základě full-textového vyhledávání..
  • Podpora uživatelů, rolí, oprávnění a kontroly přístupu k datům.
  • Rychlost, výkon, propustnost, nízká latence
  • Škálovatelnost - jak do velikosti (gigabajty, terabajty, petabajty), tak do počtu prováděných operací za sekundu
  • ACID
    • Atomicity: celá transakce (série několika databázových operací) se provede celá, nebo vůbec
    • Consistency: nesmí být narušena integrita dat, tj. podmínek definových v databázovém schématu (např. unikátní hodnoty, reference řádků mezi tabulkami...)
    • Isolation: vícero současně probíhajících transakcí se nesmí navzájem ovlivnit
    • Durability: jakmile je transakce dokončena, je zaznamenána trvalým způsobem, nesmaže ji např. výpadek napájení

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. Někdy lze databáze nakonfigurovat, zda má např. preferovat izolaci, nebo propustnost (viz PostgreSQL Transaction Isolation). Žá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.

Relační databáze

Jde o typickou, klasickou, nejčastěji používanou kategorii databázových systémů. Používají se od 70. let, existuje okolo nich široký ekosystém se spoustou nástrojů a také lidí, kteří s nimi umí pracovat.

Co relační databáze obsahuje:

  • "databáze" (jeden databázový systém může spravovat několik databází)
    • uživatelé (users)
    • tabulky
      • sloupce
      • indexy
      • constraints
      • cizí klíče (foreign keys)
      • triggery
    • pohledy (views)
    • procedury
    • sekvence

Data se ukládají v podobě řádek a sloupců do jednotlivých tabulek:

title year director_name director_birthdate
Pelíšky 1999 Jan Hřebejk 1967-06-27
Forrest Gump 1994 Robert Zemeckis 1951-05-14
Návrat do budoucnosti 1985 Robert Zemeckis 1951-05-14

Každý řádek je označen svým unikátním identifikátorem, a duplicitní data se normalizují:

movie_id title year director_id
1 Pelíšky 1999 1
2 Forrest Gump 1994 2
3 Návrat do budoucnosti 1985 2
director_id name birthdate
1 Jan Hřebejk 1967-06-27
2 Robert Zemeckis 1951-05-14

Vztah mezi režiséry a filmy je zde 1:N. Každý jeden režisér je provázán s několika filmy. Každý jeden film je provázán s jedním režisérem (a nebo žádným, pokud jeho director_id není vyplněno).

Co když chceme vyjádřit možnost, že jeden film může mít více než jednoho režiséra?

movie_id title year
1 Pelíšky 1999
2 Forrest Gump 1994
3 Návrat do budoucnosti 1985
director_id name birthdate
1 Jan Hřebejk 1967-06-27
2 Robert Zemeckis 1951-05-14
3 Joe Smith 1950-01-01
movie_id director_id
1 1
2 2
3 2
3 3

SQL

SQL je jazyk, kterým komunikujeme s relační 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ů... (Nepřípomíná vám to něco? Pandas DataFrame? :) )

Příklad:

SELECT title, year FROM movies;

UPDATE directors SET birthdate = "1950-01-02" WHERE director_id = 3;

Dokonce i jiný software, než jsou relační databáze, rozumí SQL. Často právě analytické nástroje, data lakes, Amazon S3... Pak existuje spousta databází a jazyků, které se jazykem SQL inspirovaly - např. databáze Cassandra a její jazyk CQL.

Instalace a import SQLAlchemy

Každá databáze se z Pythonu 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, liší se forma dat vrácených ve výsledku... Použijeme proto knihovnu SQLAlchemy, která tyto detaily abstrahuje a poskytuje rozhraní stejné pro všechny databáze (které SQLAlchemy podporuje).

SQLAlchemy se skládá ze dvou částí:

  • Core - to je právě ta zmíněná abstrakce základních operací s databází, tuto část budeme používat
    • engine, connections, transactions
    • table metadata
    • SQL expression language
  • ORM - umožňuje pracovat s daty v databázi prostřednistvím mapování na objekty
    • zajímavé hlavně při psaní větších aplikací (webových aplikací apod.)

Instalaci knihovny SQLAlchemy provedeme přes pip (nezapomeňte mít aktivovaný venv, do kterého to chcete instalovat):

pip install sqlalchemy

Případně lze instalaci spustit přímo z Jupyteru:

In [1]:
%pip install sqlalchemy
Requirement already satisfied: sqlalchemy in /opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages (1.3.13)
Note: you may need to restart the kernel to use updated packages.

Knihovnu SQLAlchemy naimportujeme:

In [2]:
import sqlalchemy

Jako vždy, naimportujeme i Pandas:

In [3]:
import pandas as pd

Stažení filmové databáze

Pro 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 není žádný velký server, ke kterému byste se připojovaly jménem a heslem, jako u "klasických" databází (PostgreSQL, Oracle, MS SQL...). 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ý :)

Soubor s databází stáhneme knihovnou Requests, kterou už máte nainstalovanou (instrukce byly v první hodině) a uložíme ho do souboru v pracovním adresáři, tj. tam, odkud jste spustili Jupyter Notebook.

In [4]:
from pathlib import Path
import requests
In [5]:
db_data = requests.get('https://github.com/messa/movie-db-2020/raw/master/movies.sqlite').content
Path('movies.sqlite').write_bytes(db_data)
Out[5]:
1421312

Otevření databáze, průzkum struktury

In [6]:
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.

In [7]:
sqlalchemy.inspect(engine).get_table_names()
Out[7]:
['actors', 'movie_to_actor', 'movies']
In [8]:
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'])
In [9]:
prozkoumat(engine)
Schema: main

  Table: actors

      Column: id           INTEGER
      Column: csfd_url     VARCHAR
      Column: name         VARCHAR
      Column: birth_date   DATE

  Table: movie_to_actor

      Column: id           INTEGER
      Column: movie_id     INTEGER
      Column: actor_id     INTEGER

  Table: movies

      Column: id           INTEGER
      Column: title        VARCHAR
      Column: csfd_url     VARCHAR
      Column: year         INTEGER
      Column: rating       NUMERIC

Na prozkoumání struktury a obsahu databáze můžete použít nějaký specializovaný program - DB Browser, DBeaver, MySQL Workbench, ... je jich hodně, typicky se specializují na konkrétní druh databáze, některé programy jsou zdarma a některé placené. Existují i webové aplikace, nejznámější asi český Adminer. Zde jsem vám chtěl ukázat, že toho samého se dá dosáhnout i takto z Pythonu.

Poznámka: struktura této databáze je trochu jiná, než byl motivační příklad na začátku. Nemáme tu directors, máme tu actors.

SELECT

Ukážeme si několik způsobů, jak data z databáze "vytáhnout" do Pythonu.

In [10]:
engine.execute('SELECT id, name, birth_date FROM actors')
Out[10]:
<sqlalchemy.engine.result.ResultProxy at 0x11522c5d0>

Výsledkem volání engine.execute(...) je objekt, který umí reprezentuje výsledek SQL dotazu. V případě SELECT se skrz tento objekt dají procházet získané řádky, v případě operací INSERT, UPDATE, DELETE tento objekt obsahuje např. počet změněných řádek.

Zkusíme si zobrazit jednotlivé řádky. LIMIT slouží k omezení počtu vracených záznamů.

In [11]:
result = engine.execute('SELECT id, name, birth_date FROM actors LIMIT 5')
for row in result:
    print(row)
(1, 'Tim Robbins', '1958-10-16')
(2, 'Morgan Freeman', '1937-06-01')
(3, 'Bob Gunton', '1945-11-15')
(4, 'William Sadler', '1950-04-13')
(5, 'Clancy Brown', '1959-01-05')

Zdálo by se, že procházení result vrací n-tice (tuple), ale není tomu tak.

In [12]:
result = engine.execute('SELECT id, name, birth_date FROM actors LIMIT 5')
for row in result:
    print(type(row))
<class 'sqlalchemy.engine.result.RowProxy'>
<class 'sqlalchemy.engine.result.RowProxy'>
<class 'sqlalchemy.engine.result.RowProxy'>
<class 'sqlalchemy.engine.result.RowProxy'>
<class 'sqlalchemy.engine.result.RowProxy'>

Jde o SQLAlchemy objekt RowProxy, který umí pár zajímavých věcí:

  • vrátit hodnotu daného sloupce podle pořadí – např. row[2]
  • vrátit hodnotu daného sloupce podle názvu – např. row["name"]
  • vrátit dict s hodnotami – např. dict(row)
In [13]:
result = engine.execute('SELECT id, name, csfd_url FROM actors LIMIT 5')
for row in result:
    print(row[0], row[1])
1 Tim Robbins
2 Morgan Freeman
3 Bob Gunton
4 William Sadler
5 Clancy Brown

Poznámka: sloupce vyjmenované za SELECT mohou být v jakémkoli pořadí. Stejně tak vytahávat si je z toho RowProxy objektu také můžeme libovolně v jakémkoli pořadí.

In [14]:
result = engine.execute('SELECT name, id, csfd_url FROM actors LIMIT 5')
for row in result:
    print(row[2], row[0])
https://www.csfd.cz/tvurce/103-tim-robbins/ Tim Robbins
https://www.csfd.cz/tvurce/92-morgan-freeman/ Morgan Freeman
https://www.csfd.cz/tvurce/202-bob-gunton/ Bob Gunton
https://www.csfd.cz/tvurce/203-william-sadler/ William Sadler
https://www.csfd.cz/tvurce/204-clancy-brown/ Clancy Brown

Pomocí SELECT * si vytáhneme všechny dostupné sloupce:

In [15]:
result = engine.execute('SELECT * FROM actors LIMIT 5')
for row in result:
    print(row)
(1, 'https://www.csfd.cz/tvurce/103-tim-robbins/', 'Tim Robbins', '1958-10-16')
(2, 'https://www.csfd.cz/tvurce/92-morgan-freeman/', 'Morgan Freeman', '1937-06-01')
(3, 'https://www.csfd.cz/tvurce/202-bob-gunton/', 'Bob Gunton', '1945-11-15')
(4, 'https://www.csfd.cz/tvurce/203-william-sadler/', 'William Sadler', '1950-04-13')
(5, 'https://www.csfd.cz/tvurce/204-clancy-brown/', 'Clancy Brown', '1959-01-05')

Asi bude lepší místo row[2] používat např. row["name"], nebo si to převést celé do slovníku přes dict(row):

In [16]:
result = engine.execute('SELECT * FROM actors LIMIT 5')
for row in result:
    print(row['id'], row['name'], row['birth_date'])
1 Tim Robbins 1958-10-16
2 Morgan Freeman 1937-06-01
3 Bob Gunton 1945-11-15
4 William Sadler 1950-04-13
5 Clancy Brown 1959-01-05
In [17]:
result = engine.execute('SELECT * FROM actors LIMIT 5')
for row in result:
    print(dict(row))
{'id': 1, 'csfd_url': 'https://www.csfd.cz/tvurce/103-tim-robbins/', 'name': 'Tim Robbins', 'birth_date': '1958-10-16'}
{'id': 2, 'csfd_url': 'https://www.csfd.cz/tvurce/92-morgan-freeman/', 'name': 'Morgan Freeman', 'birth_date': '1937-06-01'}
{'id': 3, 'csfd_url': 'https://www.csfd.cz/tvurce/202-bob-gunton/', 'name': 'Bob Gunton', 'birth_date': '1945-11-15'}
{'id': 4, 'csfd_url': 'https://www.csfd.cz/tvurce/203-william-sadler/', 'name': 'William Sadler', 'birth_date': '1950-04-13'}
{'id': 5, 'csfd_url': 'https://www.csfd.cz/tvurce/204-clancy-brown/', 'name': 'Clancy Brown', 'birth_date': '1959-01-05'}

Z dat lze vytvořit i Pandas DataFrame:

In [18]:
pd.read_sql_query('SELECT * FROM actors', engine)
Out[18]:
id csfd_url name birth_date
0 1 https://www.csfd.cz/tvurce/103-tim-robbins/ Tim Robbins 1958-10-16
1 2 https://www.csfd.cz/tvurce/92-morgan-freeman/ Morgan Freeman 1937-06-01
2 3 https://www.csfd.cz/tvurce/202-bob-gunton/ Bob Gunton 1945-11-15
3 4 https://www.csfd.cz/tvurce/203-william-sadler/ William Sadler 1950-04-13
4 5 https://www.csfd.cz/tvurce/204-clancy-brown/ Clancy Brown 1959-01-05
... ... ... ... ...
7522 7523 https://www.csfd.cz/tvurce/147104-jon-kenny/ Jon Kenny None
7523 7524 https://www.csfd.cz/tvurce/147105-lisa-hannigan/ Lisa Hannigan 1981-02-12
7524 7525 https://www.csfd.cz/tvurce/162204-lucy-o-connell/ Lucy O'Connell None
7525 7526 https://www.csfd.cz/tvurce/309228-paul-young/ Paul Young None
7526 7527 https://www.csfd.cz/tvurce/458624-grethe-mogen... Grethe Mogensen 1937-05-25

7527 rows × 4 columns

Toto je zkratka pro načtení celé tabulky:

In [19]:
pd.read_sql_table('actors', engine)
Out[19]:
id csfd_url name birth_date
0 1 https://www.csfd.cz/tvurce/103-tim-robbins/ Tim Robbins 1958-10-16
1 2 https://www.csfd.cz/tvurce/92-morgan-freeman/ Morgan Freeman 1937-06-01
2 3 https://www.csfd.cz/tvurce/202-bob-gunton/ Bob Gunton 1945-11-15
3 4 https://www.csfd.cz/tvurce/203-william-sadler/ William Sadler 1950-04-13
4 5 https://www.csfd.cz/tvurce/204-clancy-brown/ Clancy Brown 1959-01-05
... ... ... ... ...
7522 7523 https://www.csfd.cz/tvurce/147104-jon-kenny/ Jon Kenny NaT
7523 7524 https://www.csfd.cz/tvurce/147105-lisa-hannigan/ Lisa Hannigan 1981-02-12
7524 7525 https://www.csfd.cz/tvurce/162204-lucy-o-connell/ Lucy O'Connell NaT
7525 7526 https://www.csfd.cz/tvurce/309228-paul-young/ Paul Young NaT
7526 7527 https://www.csfd.cz/tvurce/458624-grethe-mogen... Grethe Mogensen 1937-05-25

7527 rows × 4 columns

Průzkum dat ve filmové databázi

Data z jednotlivých tabulek si nahrajeme do Pandas DataFramů, se kterými budeme dále pracovat.

Pro přehlednost si zde vypíšeme ukázku obsahu těch DataFramů, abychom věděli, s čím pracujeme.

In [20]:
sqlalchemy.inspect(engine).get_table_names()
Out[20]:
['actors', 'movie_to_actor', 'movies']
In [21]:
actors = pd.read_sql_table('actors', engine)
movies = pd.read_sql_table('movies', engine)
movie_to_actor = pd.read_sql_table('movie_to_actor', engine)
In [22]:
actors.head()
Out[22]:
id csfd_url name birth_date
0 1 https://www.csfd.cz/tvurce/103-tim-robbins/ Tim Robbins 1958-10-16
1 2 https://www.csfd.cz/tvurce/92-morgan-freeman/ Morgan Freeman 1937-06-01
2 3 https://www.csfd.cz/tvurce/202-bob-gunton/ Bob Gunton 1945-11-15
3 4 https://www.csfd.cz/tvurce/203-william-sadler/ William Sadler 1950-04-13
4 5 https://www.csfd.cz/tvurce/204-clancy-brown/ Clancy Brown 1959-01-05
In [23]:
movies.head()
Out[23]:
id title csfd_url year rating
0 1 Vykoupení z věznice Shawshank https://www.csfd.cz/film/2294-vykoupeni-z-vezn... 1994 95.3
1 2 Forrest Gump https://www.csfd.cz/film/10135-forrest-gump/ 1994 94.5
2 3 Zelená míle https://www.csfd.cz/film/2292-zelena-mile/ 1999 92.8
3 4 Přelet nad kukaččím hnízdem https://www.csfd.cz/film/2982-prelet-nad-kukac... 1975 92.5
4 5 Sedm https://www.csfd.cz/film/2671-sedm/ 1995 92.4
In [24]:
movie_to_actor.head()
Out[24]:
id movie_id actor_id
0 1 1 1
1 2 1 2
2 3 1 3
3 4 1 4
4 5 1 5

SELECT ... WHERE ... AND/OR

In [25]:
list(engine.execute('''
    SELECT * FROM actors 
    WHERE birth_date > "2000-01-01" AND birth_date < "2001-01-01"
'''))
Out[25]:
[(4849, 'https://www.csfd.cz/tvurce/39309-medved-bart-2/', 'medvěd Bart 2', '2000-01-20'),
 (5703, 'https://www.csfd.cz/tvurce/283519-zaira-wasim/', 'Zaira Wasim', '2000-10-23'),
 (6290, 'https://www.csfd.cz/tvurce/35296-morgan-lily/', 'Morgan Lily', '2000-04-11')]
In [26]:
actors[(actors['birth_date'] > '2000-01-01') & (actors['birth_date'] < '2001-01-01')]
Out[26]:
id csfd_url name birth_date
4848 4849 https://www.csfd.cz/tvurce/39309-medved-bart-2/ medvěd Bart 2 2000-01-20
5702 5703 https://www.csfd.cz/tvurce/283519-zaira-wasim/ Zaira Wasim 2000-10-23
6289 6290 https://www.csfd.cz/tvurce/35296-morgan-lily/ Morgan Lily 2000-04-11

SELECT ... ORDER BY

In [27]:
list(engine.execute('''
   SELECT * FROM actors 
   WHERE birth_date > "2005-01-01" 
   ORDER BY birth_date ASC'''))
Out[27]:
[(526, 'https://www.csfd.cz/tvurce/239127-noah-jupe/', 'Noah Jupe', '2005-02-25'),
 (7051, 'https://www.csfd.cz/tvurce/321955-rocco-lerro/', 'Rocco Lerro', '2005-08-21'),
 (4979, 'https://www.csfd.cz/tvurce/357987-ariana-greenblatt/', 'Ariana Greenblatt', '2007-08-27'),
 (1876, 'https://www.csfd.cz/tvurce/540779-hailey-hermida/', 'Hailey Hermida', '2007-09-13'),
 (1865, 'https://www.csfd.cz/tvurce/354371-pierce-pope/', 'Pierce Pope', '2008-06-02')]
In [28]:
actors[(actors['birth_date'] > '2005-01-01')].sort_values('birth_date')
Out[28]:
id csfd_url name birth_date
525 526 https://www.csfd.cz/tvurce/239127-noah-jupe/ Noah Jupe 2005-02-25
7050 7051 https://www.csfd.cz/tvurce/321955-rocco-lerro/ Rocco Lerro 2005-08-21
4978 4979 https://www.csfd.cz/tvurce/357987-ariana-green... Ariana Greenblatt 2007-08-27
1875 1876 https://www.csfd.cz/tvurce/540779-hailey-hermida/ Hailey Hermida 2007-09-13
1864 1865 https://www.csfd.cz/tvurce/354371-pierce-pope/ Pierce Pope 2008-06-02

Při řazení podle více sloupců pozor na pořadí :)

In [29]:
pd.read_sql_query('''
   SELECT title, year, rating FROM movies 
   WHERE rating > 90 AND year >= 2000
   ORDER BY year ASC, title DESC
''', engine)
Out[29]:
title year rating
0 Pán prstenů: Společenstvo Prstenu 2001 90.4
1 Pán prstenů: Návrat krále 2003 90.3
2 Temný rytíř 2008 90.2
3 Gran Torino 2008 90.3
4 Nedotknutelní 2011 91.3
5 Le Mans '66 2019 90.5
6 Gentlemani 2019 90.4
In [30]:
movies[(movies['rating'] > 90) & (movies['year'] >= 2000)] \
  .sort_values('rating').sort_values('year')
Out[30]:
id title csfd_url year rating
16 17 Pán prstenů: Společenstvo Prstenu https://www.csfd.cz/film/4711-pan-prstenu-spol... 2001 90.4
17 18 Pán prstenů: Návrat krále https://www.csfd.cz/film/4712-pan-prstenu-navr... 2003 90.3
19 20 Temný rytíř https://www.csfd.cz/film/223734-temny-rytir/ 2008 90.2
18 19 Gran Torino https://www.csfd.cz/film/240479-gran-torino/ 2008 90.3
7 8 Nedotknutelní https://www.csfd.cz/film/306731-nedotknutelni/ 2011 91.3
21 22 Gentlemani https://www.csfd.cz/film/675173-gentlemani/ 2019 90.4
15 16 Le Mans '66 https://www.csfd.cz/film/332773-le-mans-66/ 2019 90.5

SELECT ... GROUP BY

GROUP BY umožňuje "shluknout" řádky do skupin, a nad těmito skupinami pak provádět agregační funkce (MIN, MAX, AVG, COUNT apod.).

Tyto skupiny pak lze dále filtrovat pomocí HAVING (podobně jako WHERE filtruje řádky, a to předtím, než se z nich vytvářejí ty skupiny).

In [31]:
list(engine.execute('''
    SELECT year, COUNT(*) FROM movies GROUP BY year 
    HAVING COUNT(*) > 5 ORDER BY COUNT(*) DESC '''))
Out[31]:
[(1993, 11),
 (2004, 9),
 (1999, 9),
 (2001, 8),
 (1997, 8),
 (1995, 7),
 (1990, 7),
 (1986, 7),
 (2008, 6),
 (2003, 6),
 (2002, 6),
 (2000, 6),
 (1998, 6),
 (1991, 6),
 (1984, 6),
 (1980, 6),
 (1973, 6),
 (1968, 6),
 (1957, 6)]
In [32]:
pd.read_sql_query('''
    SELECT year, COUNT(id), MIN(rating), AVG(rating), MAX(rating) 
    FROM movies 
    WHERE rating > 10
    GROUP BY year
    HAVING COUNT(id) > 7
    ORDER BY year DESC
    ''', engine)
Out[32]:
year COUNT(id) MIN(rating) AVG(rating) MAX(rating)
0 2004 9 84.5 85.777778 87.9
1 2001 8 84.7 86.850000 90.4
2 1999 9 84.6 87.922222 92.8
3 1997 8 84.6 86.587500 89.1
4 1993 11 84.6 86.027273 92.3

To GROUP BY je potřeba, protože bez toho se bude celá tabulka považovat za jedinou skupinu a použití agregačních funkcí způsobí to, že se vám vrátí jen jeden řádek výsledků:

In [33]:
pd.read_sql_query('''
    SELECT COUNT(*), MIN(birth_date), MAX(birth_date)
    FROM actors 
    ''', engine)
Out[33]:
COUNT(*) MIN(birth_date) MAX(birth_date)
0 7527 1857-11-11 2008-06-02

Jaký je rozdíl mezi COUNT(*), COUNT(id) a COUNT(něco)?

COUNT(*) vrací počet řádků. COUNT(sloupec) vrací počet řádků, ve kterých je v daném sloupci nějaká hodnota (tj. není tam NULL). Pokud tím sloupcem je id, který je téměř vždy NON-NULL, tak je COUNT(id) a COUNT(*) vlastně to samé :)

In [34]:
pd.read_sql_query('''
    SELECT COUNT(*), COUNT(id), COUNT(birth_date) 
    FROM actors 
    ''', engine)
Out[34]:
COUNT(*) COUNT(id) COUNT(birth_date)
0 7527 7527 5753

Jak je vidět, u některých herců není vyplněno birth_date.

Struktura SQL dotazu

Ještě rekapitulace, jak se probíhá provádění SQL dotazu :)

  1. databáze vezme tabulky vyjmenované ve FROM
  2. databáze čte řádky (jeden po druhém)
  3. ty řádky se filtrují podle uvedených WHERE výrazů
  4. přefiltrované řádky se seskupí podle GROUP BY výrazů do skupin
  5. ty skupiny se filtrují pode uvedených HAVING výrazů
  6. výsledek se seřadí podle ORDER BY
  7. výsledek se ořízne podle LIMIT

Probrali jsme jen to nejdůležitější. Pro představu, jaké jsou možnosti zápisu SELECT dotazu (převzato z dokumentace SQLite):



Úkol: který herec účinkoval v největším počtu filmů

Najděte v tabulce movie_to_actor id herce, u kterého je v té tabulce nejvíce id filmů.

Postup:

  1. Pomocí GROUP BY si vypište vždy actor_id a k tomu počet řádků s tímto actor_id
  2. Pomocí ORDER BY a LIMIT zjistěte actor_id, u kterého je tento počet nejvyšší
  3. Bonus: zjistěte jméno tohoto herce

JOIN

JOIN je v SQL to samé, co je merge v DataFrame (viz některá předchozí hodina).

In [35]:
pd.read_sql_query('''
    SELECT movie_id, title, actor_id, name, a.id, m.id AS idecko_filmu
    FROM actors              AS a
    LEFT JOIN movie_to_actor AS ma ON a.id = ma.actor_id
    LEFT JOIN movies         AS m  ON m.id = ma.movie_id
''', engine)
Out[35]:
movie_id title actor_id name id idecko_filmu
0 1 Vykoupení z věznice Shawshank 1 Tim Robbins 1 1
1 1 Vykoupení z věznice Shawshank 2 Morgan Freeman 2 1
2 5 Sedm 2 Morgan Freeman 2 5
3 20 Temný rytíř 2 Morgan Freeman 2 20
4 64 Million Dollar Baby 2 Morgan Freeman 2 64
... ... ... ... ... ... ...
9806 300 Píseň moře 7523 Jon Kenny 7523 300
9807 300 Píseň moře 7524 Lisa Hannigan 7524 300
9808 300 Píseň moře 7525 Lucy O'Connell 7525 300
9809 300 Píseň moře 7526 Paul Young 7526 300
9810 300 Píseň moře 7527 Grethe Mogensen 7527 300

9811 rows × 6 columns

CREATE TABLE

Pokud budeme chtít vytvořit novou tabulku, používá se k tomu příkaz CREATE TABLE. Musíme vyjmenovat názvy a typy sloupců, které v tabulce chceme mít.

In [39]:
try:
    engine.execute('''
        CREATE TABLE phonebook (
          id INT PRIMARY KEY,
          name VARCHAR,
          phone_number VARCHAR
        ) 
    ''')
except OperationalError as e:
    if str(e) == 'database is locked':
        print('Zkus zabit kernel a znovu spustit primo tento prikaz :/ Me')
---------------------------------------------------------------------------
OperationalError                          Traceback (most recent call last)
/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1245                     self.dialect.do_execute(
-> 1246                         cursor, statement, parameters, context
   1247                     )

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/default.py in do_execute(self, cursor, statement, parameters, context)
    587     def do_execute(self, cursor, statement, parameters, context=None):
--> 588         cursor.execute(statement, parameters)
    589 

OperationalError: database is locked

The above exception was the direct cause of the following exception:

OperationalError                          Traceback (most recent call last)
<ipython-input-39-32fb8e6f855b> in <module>
      5       phone_number VARCHAR
      6     ) 
----> 7 ''')

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in execute(self, statement, *multiparams, **params)
   2180 
   2181         connection = self._contextual_connect(close_with_result=True)
-> 2182         return connection.execute(statement, *multiparams, **params)
   2183 
   2184     def scalar(self, statement, *multiparams, **params):

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in execute(self, object_, *multiparams, **params)
    974         """
    975         if isinstance(object_, util.string_types[0]):
--> 976             return self._execute_text(object_, multiparams, params)
    977         try:
    978             meth = object_._execute_on_connection

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in _execute_text(self, statement, multiparams, params)
   1147             parameters,
   1148             statement,
-> 1149             parameters,
   1150         )
   1151         if self._has_events or self.engine._has_events:

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1248         except BaseException as e:
   1249             self._handle_dbapi_exception(
-> 1250                 e, statement, parameters, cursor, context
   1251             )
   1252 

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in _handle_dbapi_exception(self, e, statement, parameters, cursor, context)
   1474                 util.raise_from_cause(newraise, exc_info)
   1475             elif should_wrap:
-> 1476                 util.raise_from_cause(sqlalchemy_exception, exc_info)
   1477             else:
   1478                 util.reraise(*exc_info)

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/util/compat.py in raise_from_cause(exception, exc_info)
    396     exc_type, exc_value, exc_tb = exc_info
    397     cause = exc_value if exc_value is not exception else None
--> 398     reraise(type(exception), exception, tb=exc_tb, cause=cause)
    399 
    400 

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/util/compat.py in reraise(tp, value, tb, cause)
    150             value.__cause__ = cause
    151         if value.__traceback__ is not tb:
--> 152             raise value.with_traceback(tb)
    153         raise value
    154 

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, *args)
   1244                 if not evt_handled:
   1245                     self.dialect.do_execute(
-> 1246                         cursor, statement, parameters, context
   1247                     )
   1248         except BaseException as e:

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/sqlalchemy/engine/default.py in do_execute(self, cursor, statement, parameters, context)
    586 
    587     def do_execute(self, cursor, statement, parameters, context=None):
--> 588         cursor.execute(statement, parameters)
    589 
    590     def do_execute_no_params(self, cursor, statement, context=None):

OperationalError: (sqlite3.OperationalError) database is locked
[SQL: 
    CREATE TABLE phonebook (
      id INT PRIMARY KEY,
      name VARCHAR,
      phone_number VARCHAR
    ) 
]
(Background on this error at: http://sqlalche.me/e/e3q8)

INSERT

UPDATE

DELETE

Závěr


Toto je stránka lekce z kurzu, který probíhá nebo proběhl naživo s instruktorem.