Замечание:

Выполните код ниже до лекции


In [44]:
%load_ext sql
%sql sqlite://


The sql extension is already loaded. To reload it, use:
  %reload_ext sql
Out[44]:
'Connected: None@None'

In [45]:
%%sql
pragma foreign_keys = ON; -- WARNING: by default off in sqlite
drop table if exists product; -- This needs to be dropped if exists, see why further down!
drop table if exists company;
create table company (
    cname varchar primary key, -- company name uniquely identifies the company.
    stockprice money, -- stock price is in money 
    country varchar); -- country is just a string
insert into company values ('GizmoWorks', 25.0, 'USA');
insert into company values ('Canon', 65.0, 'Japan');
insert into company values ('Hitachi', 15.0, 'Japan');
create table product(
       pname varchar primary key, -- name of the product
       price money, -- price of the product
       category varchar, -- category
       manufacturer varchar, -- manufacturer
       foreign key (manufacturer) references company(cname));
insert into product values('Gizmo', 19.99, 'Gadgets', 'GizmoWorks');
insert into product values('SingleTouch', 149.99, 'Photography', 'Canon');
insert into product values('PowerGizmo', 29.99, 'Gadgets', 'GizmoWorks');
insert into product values('MultiTouch', 203.99, 'Household', 'Hitachi');


Done.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
Out[45]:
[]

In [46]:
%%sql
DROP TABLE IF EXISTS franchise;
CREATE TABLE franchise (name TEXT, db_type TEXT);
INSERT INTO franchise VALUES ('Bobs Bagels', 'NoSQL');
INSERT INTO franchise VALUES ('eBagel', 'NoSQL');
INSERT INTO franchise VALUES ('BAGEL CORP', 'MySQL');

DROP TABLE IF EXISTS store;
CREATE TABLE store (franchise TEXT, location TEXT);
INSERT INTO store VALUES ('Bobs Bagels', 'NYC');
INSERT INTO store VALUES ('eBagel', 'PA');
INSERT INTO store VALUES ('BAGEL CORP', 'Chicago');
INSERT INTO store VALUES ('BAGEL CORP', 'NYC');
INSERT INTO store VALUES ('BAGEL CORP', 'PA');

DROP TABLE IF EXISTS bagel;
CREATE TABLE bagel (name TEXT, price MONEY, made_by TEXT);
INSERT INTO bagel VALUES ('Plain with shmear', 1.99, 'Bobs Bagels');
INSERT INTO bagel VALUES ('Egg with shmear', 2.39, 'Bobs Bagels');
INSERT INTO bagel VALUES ('eBagel Drinkable Bagel', 27.99, 'eBagel');
INSERT INTO bagel VALUES ('eBagel Expansion Pack', 1.99, 'eBagel');
INSERT INTO bagel VALUES ('Plain with shmear', 0.99, 'BAGEL CORP');
INSERT INTO bagel VALUES ('Organic Flax-seed bagel chips', 0.99, 'BAGEL CORP');

DROP TABLE IF EXISTS purchase;
-- Note that date is an int here just to simplify things
CREATE TABLE purchase (bagel_name TEXT, franchise TEXT, date INT, quantity INT, purchaser_age INT);
INSERT INTO purchase VALUES ('Plain with shmear', 'Bobs Bagels', 1, 12, 28);
INSERT INTO purchase VALUES ('Egg with shmear', 'Bobs Bagels', 2, 6, 47);
INSERT INTO purchase VALUES ('Plain with shmear', 'BAGEL CORP', 2, 12, 24);
INSERT INTO purchase VALUES ('Plain with shmear', 'BAGEL CORP', 3, 1, 17);
INSERT INTO purchase VALUES ('eBagel Expansion Pack', 'eBagel', 1, 137, 5);
INSERT INTO purchase VALUES ('Plain with shmear', 'Bobs Bagels', 4, 24, NULL);


1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
Out[46]:
[]

Сортировка по значению не в выводе

  • SQL-89 запрещает следующее, но современные СУБД позволяют:

    SELECT pname FROM Product ORDER BY Price


In [47]:
%sql SELECT * FROM Product;


Done.
Out[47]:
pname price category manufacturer
Gizmo 19.99 Gadgets GizmoWorks
SingleTouch 149.99 Photography Canon
PowerGizmo 29.99 Gadgets GizmoWorks
MultiTouch 203.99 Household Hitachi

In [48]:
%%sql SELECT pname,price FROM Product
ORDER BY pname


Done.
Out[48]:
pname price
Gizmo 19.99
MultiTouch 203.99
PowerGizmo 29.99
SingleTouch 149.99

In [49]:
%%sql SELECT pname FROM Product
ORDER BY Price


Done.
Out[49]:
pname
Gizmo
PowerGizmo
SingleTouch
MultiTouch

In [50]:
%%sql SELECT distinct pname FROM Product
ORDER BY Price


Done.
Out[50]:
pname
Gizmo
PowerGizmo
SingleTouch
MultiTouch
  • Часть СУБД выполнит запрос выше, а часть - нет

Операции с множествами

Сгенерим 3 таблицы:

  • R is {1,2,3,4,5}
  • S is {}
  • T is {1,4,7,10}

In [51]:
# Create tables & insert some random numbers
# Note: in Postgresql, try the generate_series function...
%sql DROP TABLE IF EXISTS R; DROP TABLE IF EXISTS S; DROP TABLE IF EXISTS T;
%sql CREATE TABLE R (A int); CREATE TABLE S (A int); CREATE TABLE T (A int);
for i in range(1,6):
    %sql INSERT INTO R VALUES (:i)
for i in range(1,11,3):
    %sql INSERT INTO T VALUES (:i)


1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.

Попробуем получить $R \cap (S \cup T) = \{1,4\}$


In [60]:
%%sql SELECT DISTINCT R.A FROM R, S, T
WHERE R.A=S.A OR R.A=T.A


Done.
Out[60]:
A

Почему возвращаемое множество пусто?

Посмотрим на порядок операций для данного запроса:

  1. Выполняем декартово произведение R,S,T
  2. Фильтруем таблицу из (1) по условию в WHERE.

Выполним (1):


In [61]:
%sql SELECT DISTINCT R.A FROM R, S, T;


Done.
Out[61]:
A

Декартово произведение пусто, потому что S - пусто!

Union

Воспользуемся UNION:


In [62]:
%%sql
SELECT R.A FROM R, S WHERE R.A=S.A
UNION ALL

SELECT R.A FROM R, T WHERE R.A=T.A


Done.
Out[62]:
A
1
4
  • Нет дублей (union возвращает множество)
  • Если нам нужны дубли, используем UNION ALL
    • R = {1,2,3,4,5}
    • S = {1,2,3,4,5}
    • T = {1,4,7,10}

In [63]:
%sql DROP TABLE IF EXISTS S; CREATE TABLE S (A int);
for i in range(1,6):
    %sql INSERT INTO S VALUES (:i)


1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.
1 rows affected.

In [64]:
%%sql

SELECT R.A FROM R, S WHERE R.A=S.A
UNION ALL
SELECT R.A FROM R, T WHERE R.A=T.A


Done.
Out[64]:
A
1
2
3
4
5
1
4

Другие операции со множествами: INTERSECT, EXCEPT


In [14]:
%%sql
SELECT R.A FROM R, S, T WHERE R.A = S.A
INTERSECT
SELECT R.A FROM R, S, T WHERE R.A = T.A


Done.
Out[14]:
A
1
4

In [15]:
%%sql
SELECT R.A FROM R, S, T WHERE R.A = S.A
EXCEPT
SELECT R.A FROM R, S, T WHERE R.A = T.A


Done.
Out[15]:
A
2
3
5

Пример: BAGELS

История:

  • eBagel - это новый стартап для создания новой NoSQL-системы
  • eBagel только что получил $100M инвестиций. Однако, их продажи уменьшаются, и вас попросили проанализировать данные и понять, что не так

Заполним таблицы

Franchise(name TEXT, db_type TEXT)

Store(franchise TEXT, location TEXT)

Bagel(name TEXT, price MONEY, made_by TEXT)

Purchase(bagel_name TEXT, franchise TEXT, date INT, quantity INT, purchaser_age INT)

Union

Найдем франшизы, которые расположены в PA или NYC, чтобы определить конкурентов.


In [65]:
%%sql 
SELECT franchise FROM store WHERE location = 'NYC'
UNION
SELECT franchise FROM store WHERE location = 'PA';


Done.
Out[65]:
franchise
BAGEL CORP
Bobs Bagels
eBagel

Intersect: небольшая проблема...

eBagel's CEO заинтересован узнать о back-end технологиях компании Bagel, который находятся в нескольких местах. Попробуем использовать оператор INTERSECT, чтобы найти типы баз данных франшиз, который имеют магазины в PA и NYC:


In [17]:
%%sql
SELECT f.db_type
FROM franchise f, store s 
WHERE f.name = s.franchise AND s.location = 'NYC'
INTERSECT
SELECT f.db_type
FROM franchise f, store s 
WHERE f.name = s.franchise AND s.location = 'PA'


Done.
Out[17]:
db_type
MySQL
NoSQL

Что не так

Если посмотреть на данные, то должно вернуться только "MySQL" в качестве результата:


In [18]:
%%sql 
SELECT f.name, s.location, f.db_type
FROM franchise f, store s 
WHERE f.name = s.franchise;


Done.
Out[18]:
name location db_type
Bobs Bagels NYC NoSQL
eBagel PA NoSQL
BAGEL CORP Chicago MySQL
BAGEL CORP NYC MySQL
BAGEL CORP PA MySQL

Посмотрим на значения до пересечения


In [19]:
%%sql SELECT f.db_type 
FROM franchise f, store s 
WHERE f.name = s.franchise AND s.location = 'NYC'


Done.
Out[19]:
db_type
NoSQL
MySQL

In [20]:
%%sql SELECT f.db_type
FROM franchise f, store s
WHERE f.name = s.franchise AND s.location = 'PA'


Done.
Out[20]:
db_type
NoSQL
MySQL

Проблема в том, что мы сделали операцию INTERSECTпосле возращение атрибутов запросов, а не до

Вложенные запросы

Данную задачу можно решить через вложенные запрос


In [71]:
%%sql
select distinct f.dbtype
from 
(
    select franchise
    from store where location = 'NYC'
    
    INTERSECT 
    
    SELECT franchise
    from store where location = 'PA'
) A, franchise
where A.franchise = f.name


(sqlite3.OperationalError) no such column: f.dbtype [SQL: "select distinct f.dbtype\nfrom \n(\n    select franchise\n    from store where location = 'NYC'\n    \n    INTERSECT \n    \n    SELECT franchise\n    from store where location = 'PA'\n) A, franchise\nwhere A.franchise = f.name"]

In [72]:
%%sql
SELECT f.db_type
FROM franchise f
WHERE f.name IN (
    SELECT s.franchise FROM store s WHERE s.location = 'NYC')
  AND f.name IN (
    SELECT s.franchise FROM store s WHERE s.location = 'PA');


Done.
Out[72]:
db_type
MySQL

Другой пример: eBagel's CEO хочет знать, какие бд используют компании с возрастом покупателей от 20 до 30:


In [73]:
%%sql
SELECT f.db_type
FROM franchise f
WHERE f.name IN (
    SELECT b.made_by
    FROM bagel b, purchase p
    WHERE b.name = p.bagel_name 
      AND p.purchaser_age >= 20 AND p.purchaser_age < 30);


Done.
Out[73]:
db_type
NoSQL
MySQL

Можно ли обойтись без вложенного запроса?


In [76]:
%%sql
SELECT f.db_type
FROM franchise f, bagel b, purchase p
WHERE f.name = b.made_by 
  AND b.name = p.bagel_name 
  AND p.purchaser_age >= 20 AND p.purchaser_age < 30;


Done.
Out[76]:
db_type
MySQL
NoSQL
MySQL
NoSQL

Обращайте внимание на дубли! используйте DISTINCT.

Также можно использовать:

  • ALL
  • ANY
  • EXISTS

К сожалению, ALL и ANY не поддерживаются SQLite Покажем пример с EXISTS. Предположим, что мы хотим ответить на вопрос: у eBagel есть какие-нибудь products, которые дешевле всех продуктов конкурентов?


In [77]:
%%sql
SELECT b.name, b.price
FROM bagel b
WHERE b.made_by = 'eBagel'
  AND EXISTS (SELECT name FROM bagel WHERE made_by <> 'eBagel' AND price > b.price);


Done.
Out[77]:
name price
eBagel Expansion Pack 1.99

Выводы:

SQL:

  • Предоставляет высоко уровневый декларативный язык программирования для манипулирования данными(DML)
  • SFW блок - основа
  • Есть поддержка работы с множествами и вложенными запросами

Агрегация

SQL поддерживает следующие агрегатные операции:

  • SUM
  • COUNT
  • AVG
  • MIN
  • MAX

Кроме COUNT, все операторы агрегации применяются только к единственному атрибуту

Примеры


In [25]:
%sql SELECT AVG(price) FROM bagel WHERE made_by = 'eBagel';


Done.
Out[25]:
AVG(price)
14.989999999999998

In [26]:
%sql SELECT COUNT(*) AS "Number of Stores in PA" FROM store WHERE location = 'PA';


Done.
Out[26]:
Number of Stores in PA
2

In [27]:
%sql SELECT COUNT(location) FROM store;


Done.
Out[27]:
COUNT(location)
5

In [28]:
%sql SELECT COUNT(DISTINCT location) FROM store;


Done.
Out[28]:
COUNT(DISTINCT location)
3

Можно ли получить общую сумму, заработанную компаниями?


In [29]:
%%sql
SELECT SUM(b.price * p.quantity) AS net_sales
FROM bagel b, purchase p
WHERE b.name = p.bagel_name;


Done.
Out[29]:
net_sales
432.99

Информацию можно детализировать!


In [80]:
%%sql
SELECT b.made_by, SUM(b.price * p.quantity) AS revenue
FROM bagel b, purchase p
WHERE b.made_by = p.franchise AND b.name = p.bagel_name
GROUP BY b.made_by;


Done.
Out[80]:
made_by revenue
BAGEL CORP 12.87
Bobs Bagels 85.98
eBagel 272.63

Найдем только те компании, количество продаж которых больше 12.


In [79]:
%%sql
SELECT b.name, SUM(b.price * p.quantity) AS sales
FROM bagel b, purchase p
WHERE b.name = p.bagel_name AND b.made_by = p.franchise
GROUP BY b.name
HAVING SUM(p.quantity) > 12;


Done.
Out[79]:
name sales
Plain with shmear 84.50999999999999
eBagel Expansion Pack 272.63

Посмотрим на предыдущий запрос детально Сформируем SFW


In [32]:
%%sql
SELECT *
FROM bagel b, purchase p
WHERE b.name = p.bagel_name AND b.made_by = p.franchise;


Done.
Out[32]:
name price made_by bagel_name franchise date quantity purchaser_age
Plain with shmear 1.99 Bobs Bagels Plain with shmear Bobs Bagels 1 12 28
Plain with shmear 1.99 Bobs Bagels Plain with shmear Bobs Bagels 4 24 None
Egg with shmear 2.39 Bobs Bagels Egg with shmear Bobs Bagels 2 6 47
eBagel Expansion Pack 1.99 eBagel eBagel Expansion Pack eBagel 1 137 5
Plain with shmear 0.99 BAGEL CORP Plain with shmear BAGEL CORP 2 12 24
Plain with shmear 0.99 BAGEL CORP Plain with shmear BAGEL CORP 3 1 17

Применим GROUP_BY


In [33]:
%%sql
SELECT 
    b.name,
    GROUP_CONCAT(b.price, ',') AS prices,
    GROUP_CONCAT(b.made_by, ',') AS made_bys,
    bagel_name, 
    GROUP_CONCAT(p.franchise, ',') AS franchises,
    GROUP_CONCAT(p.date, ',') AS dates,
    GROUP_CONCAT(p.quantity, ',') AS quantities,
    GROUP_CONCAT(p.purchaser_age, ',') AS purchaser_ages
FROM bagel b, purchase p
WHERE b.name = p.bagel_name AND b.made_by = p.franchise
GROUP BY b.name;


Done.
Out[33]:
name prices made_bys bagel_name franchises dates quantities purchaser_ages
Egg with shmear 2.39 Bobs Bagels Egg with shmear Bobs Bagels 2 6 47
Plain with shmear 1.99,1.99,0.99,0.99 Bobs Bagels,Bobs Bagels,BAGEL CORP,BAGEL CORP Plain with shmear Bobs Bagels,Bobs Bagels,BAGEL CORP,BAGEL CORP 1,4,2,3 12,24,12,1 28,24,17
eBagel Expansion Pack 1.99 eBagel eBagel Expansion Pack eBagel 1 137 5

Group_concat показывает сгенерированную строку

Применим Having;


In [34]:
%%sql
SELECT 
    b.name,
    GROUP_CONCAT(b.price, ',') AS prices,
    GROUP_CONCAT(b.made_by, ',') AS made_bys,
    bagel_name, 
    GROUP_CONCAT(p.franchise, ',') AS franchises,
    GROUP_CONCAT(p.date, ',') AS dates,
    SUM(p.quantity) AS total_quantity,
    GROUP_CONCAT(p.purchaser_age, ',') AS purchaser_ages
FROM bagel b, purchase p
WHERE b.name = p.bagel_name AND b.made_by = p.franchise
GROUP BY b.name
HAVING SUM(p.quantity) > 12;


Done.
Out[34]:
name prices made_bys bagel_name franchises dates total_quantity purchaser_ages
Plain with shmear 1.99,1.99,0.99,0.99 Bobs Bagels,Bobs Bagels,BAGEL CORP,BAGEL CORP Plain with shmear Bobs Bagels,Bobs Bagels,BAGEL CORP,BAGEL CORP 1,4,2,3 49 28,24,17
eBagel Expansion Pack 1.99 eBagel eBagel Expansion Pack eBagel 1 137 5

Выполним проекцию.


In [35]:
%%sql
SELECT b.name, SUM(b.price * p.quantity) AS sales
FROM bagel b, purchase p
WHERE b.name = p.bagel_name AND b.made_by = p.franchise
GROUP BY b.name
HAVING SUM(p.quantity) > 12;


Done.
Out[35]:
name sales
Plain with shmear 84.50999999999999
eBagel Expansion Pack 272.63

Вхождение во множества:

Найдем компании, у которых есть shmear в ассортименте:


In [36]:
%sql SELECT DISTINCT made_by FROM bagel WHERE name LIKE '%shmear%';


Done.
Out[36]:
made_by
Bobs Bagels
BAGEL CORP

Найдем компании, у которых все продукты имеют shmear в названии:


In [37]:
%%sql
SELECT DISTINCT made_by
FROM bagel
WHERE made_by NOT IN (
    SELECT made_by
    FROM bagel
    WHERE name NOT LIKE '%shmear%');


Done.
Out[37]:
made_by
Bobs Bagels

NULL в SQL


In [38]:
%sql SELECT * FROM purchase WHERE bagel_name LIKE '%shmear%';


Done.
Out[38]:
bagel_name franchise date quantity purchaser_age
Plain with shmear Bobs Bagels 1 12 28
Egg with shmear Bobs Bagels 2 6 47
Plain with shmear BAGEL CORP 2 12 24
Plain with shmear BAGEL CORP 3 1 17
Plain with shmear Bobs Bagels 4 24 None

In [39]:
%%sql SELECT * FROM purchase 
WHERE bagel_name LIKE '%shmear%' 
  AND (purchaser_age >= 5 OR purchaser_age < 5);


Done.
Out[39]:
bagel_name franchise date quantity purchaser_age
Plain with shmear Bobs Bagels 1 12 28
Egg with shmear Bobs Bagels 2 6 47
Plain with shmear BAGEL CORP 2 12 24
Plain with shmear BAGEL CORP 3 1 17

Применяя условия, необходимо корректно обрабатывать условия на NULL:


In [40]:
%%sql SELECT * FROM purchase
WHERE bagel_name LIKE '%shmear%'
  AND (purchaser_age >= 5 OR purchaser_age < 5 
       OR purchaser_age IS NULL);


Done.
Out[40]:
bagel_name franchise date quantity purchaser_age
Plain with shmear Bobs Bagels 1 12 28
Egg with shmear Bobs Bagels 2 6 47
Plain with shmear BAGEL CORP 2 12 24
Plain with shmear BAGEL CORP 3 1 17
Plain with shmear Bobs Bagels 4 24 None

Что произойдет если есть null в join'е?


In [41]:
%%sql 
SELECT DISTINCT b.name 
FROM bagel b, purchase p 
WHERE b.name = p.bagel_name AND b.made_by = p.franchise;


Done.
Out[41]:
name
Plain with shmear
Egg with shmear
eBagel Expansion Pack

Пропускаем пончики, которые никто не покупал

Inner/Outer Joins


In [42]:
%%sql 
SELECT DISTINCT b.name 
FROM bagel b
    INNER JOIN purchase p ON b.name = p.bagel_name AND b.made_by = p.franchise;


Done.
Out[42]:
name
Plain with shmear
Egg with shmear
eBagel Expansion Pack

INNER JOIN на таблицыA и B с условием на соединение C(A,B) возвращает только такие отношения (a,b), для которых C(a,b) = TRUE. LEFT OUTER JOIN. возвращает также (a, NULL) для тех кортежей a, для которых нет b таких, что C(a,b) = TRUE:


In [43]:
%%sql 
SELECT DISTINCT b.name 
FROM bagel b
    LEFT OUTER JOIN purchase p ON b.name = p.bagel_name AND b.made_by = p.franchise;


Done.
Out[43]:
name
Plain with shmear
Egg with shmear
eBagel Drinkable Bagel
eBagel Expansion Pack
Organic Flax-seed bagel chips

In [ ]: