вступление
Привет! В предыдущем посте мы обсудили плюсы и минусы SQLAlchemy по сравнению с необработанным SQL, создали таблицу и вставили данные, используя как SQLite > и SQLAlchemy.
В этом посте мы переходим к сложным запросам SELECT с использованием как SQLite, так и SQLAlchemy. Наслаждаться!
Подготовка
Во-первых, давайте реализуем функции print_sqlite_cursor_as_table и print_sqlalchemy_cursor_as_table для вывода курсоров в виде красивых таблиц:
import sqlite3 import sqlalchemy def print_formatted_table(table: list[list]): column_lengths = [ max(len(table[i][column_num]) for i in range(len(table))) for column_num in range(len(table[0])) ] for row in table: print("|", end=" ") for item, column_length in zip(row, column_lengths): print(item.rjust(column_length), "|", end=" ") print() def print_sqlite_cursor_as_table(title: str, cursor: sqlite3.Cursor): headers = [d[0] for d in cursor.description] table: list[list[str]] = [headers] for row in cursor.fetchall(): table.append(list(map(str, row))) print(title) print_formatted_table(table) print() def print_sqlalchemy_cursor_as_table( connection: sqlalchemy.Connection, title: str, statement: sqlalchemy.Select, ): cursor = connection.execute(statement) headers = list(cursor.keys()) table: list[list[str]] = [headers] for row in cursor.fetchall(): table.append(list(map(str, row))) print(title) print_formatted_table(table) print()
Выбрать все записи из таблицы
В SQLite вы должны просто написать оператор SELECT FROM. Например, в случае таблицы items это можно записать так:
print_sqlite_cursor_as_table( "All Items:", sqlite_cursor.execute("SELECT * FROM items") ) >>> All Items: | item_id | item_name | item_price | | 1 | Cola | 2.5 | | 2 | Chips | 3.0 | | 3 | Water | 1.2 | | 4 | Apple | 0.3 | | 5 | Avocado | 1.75 |
В SQLAlchemy вместо этого следует использовать функцию select и передать таблицу в качестве параметра:
print_sqlalchemy_cursor_as_table( sqlalchemy_connection, "All items:", sqlalchemy.select(items_table) ) >>> All items: | item_id | item_name | item_price | | 1 | Cola | 2.5 | | 2 | Chips | 3.0 | | 3 | Water | 1.2 | | 4 | Apple | 0.3 | | 5 | Avocado | 1.75 |
Выберите определенные столбцы и упорядочите по
Представьте, теперь вам нужно выбирать только названия товаров и цены, упорядоченные по цене в порядке убывания.
В SQLite вы можете указать имена столбцов и использовать оператор ORDER BY:
print_sqlite_cursor_as_table( "Item names and prices:", sqlite_cursor.execute( """ SELECT item_name, item_price FROM items ORDER BY item_price DESC """ ) ) >>> Item names and prices: | item_name | item_price | | Chips | 3.0 | | Cola | 2.5 | | Avocado | 1.75 | | Water | 1.2 | | Apple | 0.3 |
Что вы делаете в SQLAlchemy, так это используете столбцы таблицы в качестве параметров функции select (вместо всей таблицы), а затем вызываете метод order_by для результата и передайте параметр table_column.desc():
print_sqlalchemy_cursor_as_table( sqlalchemy_connection, "Item names and prices:", sqlalchemy.select( items_table.c.item_name, items_table.c.item_price ).order_by(items_table.c.item_price.desc()) ) >>> Item names and prices: | item_name | item_price | | Chips | 3.0 | | Cola | 2.5 | | Avocado | 1.75 | | Water | 1.2 | | Apple | 0.3 |
Присоединяется
В SQLite обычно используется синтаксис FROM table_a JOIN table_b ON table_a.column_a = table_b.column_b:
print_sqlite_cursor_as_table( "Operations with details:", sqlite_cursor.execute( """ SELECT o.operation_id, o.operation_date, i.item_name, i.item_price, d.amount, t.operation_type_name FROM operations o LEFT JOIN operation_descriptions d ON o.operation_id = d.operation_id JOIN items i ON d.item_id = i.item_id JOIN operation_types t ON o.operation_type_id = t.operation_type_id """ ) ) >>> Operations with details: | operation_id | operation_date | item_name | item_price | amount | operation_type_name | | 1 | 2022-04-22 | Cola | 2.5 | 1 | sell | | 1 | 2022-04-22 | Chips | 3.0 | 2 | sell | | 2 | 2022-04-22 | Avocado | 1.75 | 1 | sell | | 3 | 2022-04-22 | Avocado | 1.75 | 1 | return | | 4 | 2022-04-23 | Water | 1.2 | 1 | sell | | 4 | 2022-04-23 | Apple | 0.3 | 2 | sell |
Как повторить ту же конструкцию в SQLAlchemy? Вы можете использовать синтаксис table_a.join(table_b, условие).
Например, если вы выполняете ВНУТРЕННЕЕ СОЕДИНЕНИЕ, это можно записать так: table_a.join(table_b, table_a.c.id = table_b.c.id).
Если вы используете LEFT JOIN, необходимо передать параметр isouter=True:
operations_details_statement = operations_table.join( operation_descriptions_table, operations_table.c.operation_id == \ operation_descriptions_table.c.operation_id, isouter=True ).join( items_table, operation_descriptions_table.c.item_id == \ items_table.c.item_id ).join( operation_types_table, operations_table.c.operation_type_id == \ operation_types_table.c.operation_type_id )
Здесь мы извлекаем оператор соединения в переменную. Чтобы использовать его в операторе выбора с определенными столбцами, мы можем использовать синтаксис select_from:
print_sqlalchemy_cursor_as_table( sqlalchemy_connection, "Operations with details:", sqlalchemy.select( operations_table.c.operation_id, operations_table.c.operation_date, items_table.c.item_name, items_table.c.item_price, operation_descriptions_table.c.amount, operation_types_table.c.operation_type_name ).select_from(operations_details_statement) ) >>> Operations with details: | operation_id | operation_date | item_name | item_price | amount | operation_type_name | | 1 | 2022-04-22 | Cola | 2.5 | 1 | sell | | 1 | 2022-04-22 | Chips | 3.0 | 2 | sell | | 2 | 2022-04-22 | Avocado | 1.75 | 1 | sell | | 3 | 2022-04-22 | Avocado | 1.75 | 1 | return | | 4 | 2022-04-23 | Water | 1.2 | 1 | sell | | 4 | 2022-04-23 | Apple | 0.3 | 2 | sell |
Где, Group By и функции SQL
В SQLite мы можем использовать функции SQL для столбцов, затем фильтровать записи с некоторым условием, а затем объединять их с помощью GROUP BY. Например, если мы решили вычислить сумму каждого чека, мы могли бы использовать этот запрос:
print_sqlite_cursor_as_table( "Check sums:", sqlite_cursor.execute( """ SELECT o.operation_date, -- multiply amount of items on their price to get per-item total ROUND(SUM(d.amount * i.item_price), 2) AS check_sum FROM operations o LEFT JOIN operation_descriptions d ON o.operation_id = d.operation_id JOIN items i ON d.item_id = i.item_id JOIN operation_types t ON o.operation_type_id = t.operation_type_id WHERE t.operation_type_name = 'sell' GROUP BY o.operation_id, o.operation_date """ ) ) >>> Check sums: | operation_date | check_sum | | 2022-04-22 | 8.5 | | 2022-04-22 | 1.75 | | 2022-04-23 | 1.8 |
В SQLAlchemy для вызова функций SQL мы будем использовать модуль sqlalchemy.sql.func. Для фильтрации и агрегирования есть функции where(condition) и group_by(column1, column2, …), которые применяются к запросу выбора. Кроме того, мы можем переименовать столбец в операторе select, используя функцию label(new_label) для этого столбца. В этом запросе повторно используем переменную operations_details_statement:
from sqlalchemy.sql import func print_sqlalchemy_cursor_as_table( sqlalchemy_connection, "Check sums:", sqlalchemy.select( operations_table.c.operation_date, func.round( func.sum( operation_descriptions_table.c.amount * \ items_table.c.item_price ), 2 ).label("check_sum") ).select_from( operations_details_statement ).where( operation_types_table.c.operation_type_name == "sell" ).group_by( operations_table.c.operation_id, operations_table.c.operation_date ) ) >>> Check sums: | operation_date | check_sum | | 2022-04-22 | 8.5 | | 2022-04-22 | 1.75 | | 2022-04-23 | 1.8 |
CTE и подзапросы
В SQL CTE очень похожи на именованные запросы, которые можно использовать в последующем операторе SELECT. Обычно они действуют как подзапросы, но вместо того, чтобы копировать тот же код в подзапрос, мы определяем его как своего рода переменную.
Например, давайте найдем все чеки, общая сумма которых больше средней, используя SQLite:
print_sqlite_cursor_as_table( "Check greater than average:", sqlite_cursor.execute( """ WITH full_checks AS ( SELECT o.operation_id, o.operation_date, i.item_name, d.amount, ROUND(d.amount * i.item_price, 2) AS item_sum FROM operations o LEFT JOIN operation_descriptions d ON o.operation_id = d.operation_id JOIN items i ON d.item_id = i.item_id JOIN operation_types t ON o.operation_type_id = t.operation_type_id WHERE t.operation_type_name = 'sell' ), sell_checks AS ( SELECT f.operation_id, f.operation_date, ROUND(SUM(f.item_sum), 2) AS check_sum FROM full_checks f GROUP BY f.operation_id, f.operation_date ) SELECT f.operation_id, f.operation_date, f.item_name, f.item_sum FROM sell_checks s LEFT JOIN full_checks f ON s.operation_id = f.operation_id WHERE s.check_sum > (SELECT AVG(check_sum) FROM sell_checks) """ ) ) >>> Check greater than average: | operation_id | operation_date | item_name | item_sum | | 1 | 2022-04-22 | Chips | 6.0 | | 1 | 2022-04-22 | Cola | 2.5 |
Кроме того, в SQLAlchemy мы можем вызвать метод cte(cte_name) для запроса, чтобы превратить его в CTE:
full_checks_cte = sqlalchemy.select( operations_table.c.operation_id, operations_table.c.operation_date, items_table.c.item_name, operation_descriptions_table.c.amount, func.round( operation_descriptions_table.c.amount * items_table.c.item_price, 2 ).label("item_sum") ).select_from( operations_details_statement ).where( operation_types_table.c.operation_type_name == "sell" ).cte("full_checks") sell_checks_cte = sqlalchemy.select( full_checks_cte.c.operation_id, full_checks_cte.c.operation_date, func.round( func.sum(full_checks_cte.c.item_sum), 2 ).label("check_sum") ).select_from( full_checks_cte ).group_by( full_checks_cte.c.operation_id, full_checks_cte.c.operation_date ).cte("sell_checks")
Позже мы можем использовать эти CTE во внешнем запросе выбора. Кроме того, SQLAlchemy предупреждает нас об использовании scalar_subquery() в подзапросах для преобразования их в скалярные значения:
print_sqlalchemy_cursor_as_table( sqlalchemy_connection, "Check greater than average:", sqlalchemy.select( full_checks_cte.c.operation_id, full_checks_cte.c.operation_date, full_checks_cte.c.item_name, full_checks_cte.c.item_sum ).select_from( sell_checks_cte.join( full_checks_cte, sell_checks_cte.c.operation_id == full_checks_cte.c.operation_id, isouter=True ) ).where( sell_checks_cte.c.check_sum > sqlalchemy.select( func.avg(sell_checks_cte.c.check_sum) ).scalar_subquery() ) ) >>> Check greater than average: | operation_id | operation_date | item_name | item_sum | | 1 | 2022-04-22 | Chips | 6.0 | | 1 | 2022-04-22 | Cola | 2.5 |
Заключение
В этой части мы обнаружили сложные запросы SELECT. SQLAlchemy часто предоставляет некоторый уровень абстракции, который позволяет нам повторно использовать существующий код. Хотя иногда SQLAlchemy довольно многословен по сравнению с чистым SQL, эти операторы все равно легко читать и рефакторить.
В дальнейшей части мы продолжим использовать SQLAlchemy с операциями DELETE и UPDATE. Следите за обновлениями!