SQLAlchemy Основные базовые операции: SELECT-запросы

Привет! В этом посте мы обсудим сложные SELECT-запросы, используя как SQLite, так и SQLAlchemy с Python. Наслаждайтесь!

https://t.me/sqlhub – телеграм канал обучающий работе с базами данных на практике.

Подготовка

Во-первых, давайте реализуем функции 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, condition).

Например, если вы выполняете INNER JOIN, это можно записать так: 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
)

Здесь мы извлекаем оператор join в переменную. Чтобы использовать его в операторе select с определенными столбцами, мы можем использовать синтаксис 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 | 

Where, 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. Кроме того, мы можем переименовать столбец в операторе 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 в запросе select. Кроме того, 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, эти запросы все равно легко читать и рефакторить.

+1
1
+1
3
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *