Выбор строк с помощью ядра или ORM

Для Core и ORM функция select() генерирует конструкцию Select, которая используется для всех запросов SELECT. Переданный методам типа Connection.execute() в Core и Session.execute() в ORM, оператор SELECT выдается в текущей транзакции, а строки результата доступны через возвращаемый объект Result.

ORM Readers - содержание этой статьи одинаково хорошо применимо как к Core, так и к ORM, и здесь упоминаются основные варианты использования ORM. Однако существует также много других возможностей, специфичных для ORM; они документированы в Руководство по составлению запросов ORM.

Конструкция SQL-выражения select()

Конструкция select() строит высказывание так же, как и insert(), используя подход generative, при котором каждый метод наращивает состояние объекта. Как и другие конструкции SQL, она может быть построена на месте:

>>> from sqlalchemy import select
>>> stmt = select(user_table).where(user_table.c.name == "spongebob")
>>> print(stmt)
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = :name_1

Также, как и все остальные конструкции SQL на уровне оператора, для фактического выполнения оператора мы передаем его методу выполнения. Поскольку оператор SELECT возвращает строки, мы всегда можем итерировать объект результата, чтобы получить Row объектов обратно:

>>> with engine.connect() as conn:
...     for row in conn.execute(stmt):
...         print(row)
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('spongebob',)
(1, 'spongebob', 'Spongebob Squarepants')
ROLLBACK

При использовании ORM, особенно с конструкцией select(), составленной из сущностей ORM, мы захотим выполнить ее, используя метод Session.execute() на Session; используя этот подход, мы продолжаем получать объекты Row из результата, однако эти строки теперь способны включать полные сущности, такие как экземпляры класса User, как отдельные элементы в каждой строке:

>>> stmt = select(User).where(User.name == "spongebob")
>>> with Session(engine) as session:
...     for row in session.execute(stmt):
...         print(row)
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('spongebob',)
(User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)
ROLLBACK

В следующих разделах конструкция SELECT будет рассмотрена более подробно.

Настройка COLUMNS и предложения FROM

Функция select() принимает позиционные элементы, представляющие любое количество выражений Column и/или Table, а также широкий спектр совместимых объектов, которые преобразуются в список SQL-выражений, подлежащих ВЫБОРУ, которые будут возвращены в качестве столбцов в наборе результатов. Эти элементы также служат в более простых случаях для создания предложения FROM, которое выводится из переданных столбцов и табличных выражений:

>>> print(select(user_table))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account

Для SELECT из отдельных столбцов, используя подход Core, объекты Column доступны из аксессора Table.c и могут быть отправлены напрямую; предложение FROM будет выведено как набор всех Table и других FromClause объектов, которые представлены этими столбцами:

>>> print(select(user_table.c.name, user_table.c.fullname))
SELECT user_account.name, user_account.fullname FROM user_account

Выбор сущностей и столбцов ORM

ORM-сущности, такие как наш класс User, а также атрибуты, отображаемые на столбцы, такие как User.name, также участвуют в системе SQL Expression Language, представляющей таблицы и столбцы. Ниже показан пример выбора из сущности User, который в конечном итоге отображается так же, как если бы мы использовали непосредственно user_table:

>>> print(select(User))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account

При выполнении оператора, подобного приведенному выше, с использованием метода ORM Session.execute(), существует важное различие, когда мы выбираем из полной сущности, такой как User, в отличие от user_table, которое заключается в том, что сама сущность возвращается как один элемент в каждой строке. То есть, когда мы получаем строки из приведенного выше оператора, поскольку в списке объектов для выборки есть только сущность User, мы получаем обратно объекты Row, имеющие только один элемент, которые содержат экземпляры класса User:

>>> row = session.execute(select(User)).first()
BEGIN... SELECT user_account.id, user_account.name, user_account.fullname FROM user_account [...] ()
>>> row (User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)

Приведенный выше Row имеет только один элемент, представляющий сущность User:

>>> row[0]
User(id=1, name='spongebob', fullname='Spongebob Squarepants')

Настоятельно рекомендуемый удобный метод достижения того же результата, что и выше, заключается в использовании метода Session.scalars() для непосредственного выполнения оператора; этот метод вернет объект ScalarResult, который доставит первый «столбец» каждой строки сразу, в данном случае экземпляры класса User:

>>> user = session.scalars(select(User)).first()
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account [...] ()
>>> user User(id=1, name='spongebob', fullname='Spongebob Squarepants')

В качестве альтернативы, мы можем выбрать отдельные столбцы сущности ORM как отдельные элементы в строках результатов, используя атрибуты, связанные с классом; когда они передаются в такую конструкцию, как select(), они преобразуются в Column или другое выражение SQL, представленное каждым атрибутом:

>>> print(select(User.name, User.fullname))
SELECT user_account.name, user_account.fullname FROM user_account

Когда мы вызываем этот оператор с помощью Session.execute(), мы теперь получаем строки, которые имеют отдельные элементы на каждое значение, каждый из которых соответствует отдельному столбцу или другому выражению SQL:

>>> row = session.execute(select(User.name, User.fullname)).first()
SELECT user_account.name, user_account.fullname FROM user_account [...] ()
>>> row ('spongebob', 'Spongebob Squarepants')

Подходы также можно смешивать, как показано ниже, где мы ВЫБИРАЕМ атрибут name сущности User в качестве первого элемента строки, и объединяем его с полными сущностями Address во втором элементе:

>>> session.execute(
...     select(User.name, Address).where(User.id == Address.user_id).order_by(Address.id)
... ).all()
SELECT user_account.name, address.id, address.email_address, address.user_id FROM user_account, address WHERE user_account.id = address.user_id ORDER BY address.id [...] ()
[('spongebob', Address(id=1, email_address='spongebob@sqlalchemy.org')), ('sandy', Address(id=2, email_address='sandy@sqlalchemy.org')), ('sandy', Address(id=3, email_address='sandy@squirrelpower.org'))]

Подходы к выбору сущностей и столбцов ORM, а также общие методы преобразования строк обсуждаются далее в Выбор сущностей и атрибутов ORM.

Выборка из меченых выражений SQL

Метод ColumnElement.label(), а также одноименный метод, доступный в атрибутах ORM, предоставляет SQL-метку столбца или выражения, позволяя ему иметь определенное имя в наборе результатов. Это может быть полезно при обращении к произвольным SQL-выражениям в строке результата по имени:

>>> from sqlalchemy import func, cast
>>> stmt = select(
...     ("Username: " + user_table.c.name).label("username"),
... ).order_by(user_table.c.name)
>>> with engine.connect() as conn:
...     for row in conn.execute(stmt):
...         print(f"{row.username}")
BEGIN (implicit) SELECT ? || user_account.name AS username FROM user_account ORDER BY user_account.name [...] ('Username: ',)
Username: patrick Username: sandy Username: spongebob
ROLLBACK

См.также

Упорядочивание или группировка по метке - имена меток, которые мы создаем, могут также упоминаться в пункте ORDER BY или GROUP BY в Select.

Выбор с помощью текстовых выражений столбцов

Когда мы создаем объект Select с помощью функции select(), мы обычно передаем ему серию объектов Table и Column, которые были определены с помощью table metadata, или при использовании ORM мы можем передавать ORM-mapped атрибуты, представляющие столбцы таблицы. Однако иногда возникает необходимость в производстве произвольных блоков SQL внутри операторов, таких как константные строковые выражения или просто произвольный SQL, который быстрее написать буквально.

Конструкция text(), введенная в Работа с транзакциями и DBAPI, может быть встроена в конструкцию Select непосредственно, как показано ниже, где мы создаем жестко закодированный строковый литерал 'some label' и встраиваем его в оператор SELECT:

>>> from sqlalchemy import text
>>> stmt = select(text("'some phrase'"), user_table.c.name).order_by(user_table.c.name)
>>> with engine.connect() as conn:
...     print(conn.execute(stmt).all())
BEGIN (implicit) SELECT 'some phrase', user_account.name FROM user_account ORDER BY user_account.name [generated in ...] ()
[('some phrase', 'patrick'), ('some phrase', 'sandy'), ('some phrase', 'spongebob')]
ROLLBACK

Хотя конструкцию text() можно использовать в большинстве мест для введения буквальных фраз SQL, чаще всего мы имеем дело с текстовыми единицами, каждая из которых представляет собой отдельное выражение столбца. В этом случае мы можем получить больше функциональности от нашего текстового фрагмента, используя вместо него конструкцию literal_column(). Этот объект похож на text() за исключением того, что вместо того, чтобы представлять произвольный SQL в любой форме, он явно представляет один «столбец» и может быть затем помечен и упомянут в подзапросах и других выражениях:

>>> from sqlalchemy import literal_column
>>> stmt = select(literal_column("'some phrase'").label("p"), user_table.c.name).order_by(
...     user_table.c.name
... )
>>> with engine.connect() as conn:
...     for row in conn.execute(stmt):
...         print(f"{row.p}, {row.name}")
BEGIN (implicit) SELECT 'some phrase' AS p, user_account.name FROM user_account ORDER BY user_account.name [generated in ...] ()
some phrase, patrick some phrase, sandy some phrase, spongebob
ROLLBACK

Обратите внимание, что в обоих случаях при использовании text() или literal_column() мы пишем синтаксическое выражение SQL, а не буквальное значение. Поэтому мы должны включить любые кавычки или синтаксис, необходимые для того SQL, который мы хотим видеть в визуализации.

Предложение ГДЕ

SQLAlchemy позволяет нам составлять SQL-выражения, такие как name = 'squidward' или user_id > 10, используя стандартные операторы Python в сочетании с Column и подобными объектами. Для булевых выражений большинство операторов Python, таких как ==, !=, <, >= и т.д. генерируют новые объекты SQL Expression, а не простые булевые значения True/False:

>>> print(user_table.c.name == "squidward")
user_account.name = :name_1

>>> print(address_table.c.user_id > 10)
address.user_id > :user_id_1

Мы можем использовать выражения, подобные этим, для создания предложения WHERE, передавая полученные объекты в метод Select.where():

>>> print(select(user_table).where(user_table.c.name == "squidward"))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = :name_1

Для получения нескольких выражений, объединенных методом AND, метод Select.where() может быть вызван любое количество раз:

>>> print(
...     select(address_table.c.email_address)
...     .where(user_table.c.name == "squidward")
...     .where(address_table.c.user_id == user_table.c.id)
... )
SELECT address.email_address FROM address, user_account WHERE user_account.name = :name_1 AND address.user_id = user_account.id

Один вызов Select.where() также принимает несколько выражений с тем же эффектом:

>>> print(
...     select(address_table.c.email_address).where(
...         user_table.c.name == "squidward", address_table.c.user_id == user_table.c.id
...     )
... )
SELECT address.email_address FROM address, user_account WHERE user_account.name = :name_1 AND address.user_id = user_account.id

Конъюнкции «И» и «ИЛИ» доступны непосредственно с помощью функций and_() и or_(), проиллюстрированных ниже в терминах сущностей ORM:

>>> from sqlalchemy import and_, or_
>>> print(
...     select(Address.email_address).where(
...         and_(
...             or_(User.name == "squidward", User.name == "sandy"),
...             Address.user_id == User.id,
...         )
...     )
... )
SELECT address.email_address FROM address, user_account WHERE (user_account.name = :name_1 OR user_account.name = :name_2) AND address.user_id = user_account.id

Для простого сравнения «равенства» с одной сущностью существует также популярный метод Select.filter_by(), который принимает аргументы в виде ключевых слов, соответствующих ключам столбцов или именам атрибутов ORM. Он будет фильтровать по крайнему левому предложению FROM или последней сущности, к которой присоединяется:

>>> print(select(User).filter_by(name="spongebob", fullname="Spongebob Squarepants"))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = :name_1 AND user_account.fullname = :fullname_1

См.также

Справочник оператора - описание большинства функций операторов SQL в SQLAlchemy

Явные предложения FROM и JOIN

Как упоминалось ранее, предложение FROM обычно зависит от выражений, которые мы задаем в предложении columns, а также от других элементов Select.

Если мы задаем один столбец из определенного Table в предложении COLUMNS, он помещает этот Table также в предложении FROM:

>>> print(select(user_table.c.name))
SELECT user_account.name FROM user_account

Если мы поместим столбцы из двух таблиц, то получим предложение FROM, разделенное запятыми:

>>> print(select(user_table.c.name, address_table.c.email_address))
SELECT user_account.name, address.email_address FROM user_account, address

Для того чтобы соединить эти две таблицы вместе, мы обычно используем один из двух методов Select. Первый - это метод Select.join_from(), который позволяет нам явно указать левую и правую сторону JOIN:

>>> print(
...     select(user_table.c.name, address_table.c.email_address).join_from(
...         user_table, address_table
...     )
... )
SELECT user_account.name, address.email_address FROM user_account JOIN address ON user_account.id = address.user_id

Другим методом является метод Select.join(), который указывает только правую сторону JOIN, левая сторона выводится:

>>> print(select(user_table.c.name, address_table.c.email_address).join(address_table))
SELECT user_account.name, address.email_address FROM user_account JOIN address ON user_account.id = address.user_id

У нас также есть возможность добавлять элементы в предложение FROM явно, если они не выводятся нужным образом из предложения columns. Для этого мы используем метод Select.select_from(), как показано ниже, где мы устанавливаем user_table как первый элемент в предложении FROM, а Select.join() устанавливаем address_table как второй:

>>> print(select(address_table.c.email_address).select_from(user_table).join(address_table))
SELECT address.email_address FROM user_account JOIN address ON user_account.id = address.user_id

Другой пример, когда мы можем захотеть использовать Select.select_from(), это если в нашем предложении columns недостаточно информации для предложения FROM. Например, для SELECT из общего выражения SQL count(*) мы используем элемент SQLAlchemy, известный как sqlalchemy.sql.expression.func, чтобы создать функцию SQL count():

>>> from sqlalchemy import func
>>> print(select(func.count("*")).select_from(user_table))
SELECT count(:count_2) AS count_1 FROM user_account

См.также

Управление тем, к чему присоединяться - в Руководство по составлению запросов ORM - содержит дополнительные примеры и примечания относительно взаимодействия Select.select_from() и Select.join().

Установка положения о включении

Предыдущие примеры JOIN иллюстрировали, что конструкция Select может соединять две таблицы и автоматически создавать предложение ON. Это происходит в тех примерах потому, что объекты user_table и address_table Table включают одно определение ForeignKeyConstraint, которое используется для формирования предложения ON.

Если левая и правая цели объединения не имеют такого ограничения, или существует несколько ограничений, нам необходимо указать предложение ON напрямую. И Select.join(), и Select.join_from() принимают дополнительный аргумент для предложения ON, который указывается с помощью той же механики SQL Expression, о которой мы говорили в Предложение ГДЕ:

>>> print(
...     select(address_table.c.email_address)
...     .select_from(user_table)
...     .join(address_table, user_table.c.id == address_table.c.user_id)
... )
SELECT address.email_address FROM user_account JOIN address ON user_account.id = address.user_id

Совет ORM - существует еще один способ создания предложения ON при использовании сущностей ORM, использующих конструкцию relationship(), например, отображение, установленное в предыдущем разделе Объявление сопоставленных классов. Это целая тема, которая подробно рассматривается в Использование отношений для присоединения.

ВНЕШНЕЕ и ПОЛНОЕ соединение

Оба метода Select.join() и Select.join_from() принимают ключевые аргументы Select.join.isouter и Select.join.full, которые будут отображать LEFT OUTER JOIN и FULL OUTER JOIN, соответственно:

>>> print(select(user_table).join(address_table, isouter=True))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account LEFT OUTER JOIN address ON user_account.id = address.user_id
>>> print(select(user_table).join(address_table, full=True))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account FULL OUTER JOIN address ON user_account.id = address.user_id

Существует также метод Select.outerjoin(), который эквивалентен использованию .join(..., isouter=True).

Совет

В SQL также есть «RIGHT OUTER JOIN». SQLAlchemy не отображает это напрямую; вместо этого следует изменить порядок таблиц и использовать «LEFT OUTER JOIN».

ПО ПОРЯДКУ, ПО ГРУППАМ, ПО НАЛИЧИЮ

SQL-оператор SELECT включает предложение ORDER BY, которое используется для возврата выбранных строк в заданном порядке.

Предложение GROUP BY строится аналогично предложению ORDER BY и имеет целью разделить выбранные строки на определенные группы, для которых могут быть вызваны агрегатные функции. Предложение HAVING обычно используется вместе с GROUP BY и по форме аналогично предложению WHERE, за исключением того, что оно применяется к агрегированным функциям, используемым в группах.

ПОРЯДКОВЫЙ НОМЕР

Предложение ORDER BY строится в терминах конструкций SQL Expression, обычно основанных на Column или аналогичных объектах. Метод Select.order_by() принимает одно или несколько таких выражений позиционно:

>>> print(select(user_table).order_by(user_table.c.name))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account ORDER BY user_account.name

Возрастание / убывание доступно из модификаторов ColumnElement.asc() и ColumnElement.desc(), которые также присутствуют из ORM-связанных атрибутов:

>>> print(select(User).order_by(User.fullname.desc()))
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account ORDER BY user_account.fullname DESC

Приведенное выше утверждение даст строки, отсортированные по столбцу user_account.fullname в порядке убывания.

Агрегатные функции с GROUP BY / HAVING

В SQL агрегатные функции позволяют объединять выражения столбцов в нескольких строках для получения единого результата. Примеры включают подсчет, вычисление средних значений, а также нахождение максимального или минимального значения в наборе значений.

SQLAlchemy предоставляет функции SQL в открытом виде, используя пространство имен, известное как func. Это специальный объект-конструктор, который будет создавать новые экземпляры Function, когда ему дается имя конкретной SQL-функции, которая может иметь любое имя, а также ноль или более аргументов для передачи функции, которые, как и во всех других случаях, являются конструкциями SQL-выражений. Например, чтобы вывести функцию SQL COUNT() для столбца user_account.id, мы обращаемся к имени count():

>>> from sqlalchemy import func
>>> count_fn = func.count(user_table.c.id)
>>> print(count_fn)
count(user_account.id)

Функции SQL более подробно описаны далее в этом учебнике в Работа с функциями SQL.

При использовании агрегатных функций в SQL предложение GROUP BY является важным, поскольку оно позволяет разделить строки на группы, где агрегатные функции будут применяться к каждой группе отдельно. При запросе неагрегированных столбцов в предложении COLUMNS оператора SELECT, SQL требует, чтобы все эти столбцы были подчинены предложению GROUP BY, прямо или косвенно на основе ассоциации с первичным ключом. Предложение HAVING затем используется аналогично предложению WHERE, за исключением того, что оно отфильтровывает строки на основе агрегированных значений, а не прямого содержимого строки.

SQLAlchemy предусматривает эти два условия с помощью методов Select.group_by() и Select.having(). Ниже мы проиллюстрируем выбор полей имени пользователя, а также подсчет адресов для тех пользователей, которые имеют более одного адреса:

>>> with engine.connect() as conn:
...     result = conn.execute(
...         select(User.name, func.count(Address.id).label("count"))
...         .join(Address)
...         .group_by(User.name)
...         .having(func.count(Address.id) > 1)
...     )
...     print(result.all())
BEGIN (implicit) SELECT user_account.name, count(address.id) AS count FROM user_account JOIN address ON user_account.id = address.user_id GROUP BY user_account.name HAVING count(address.id) > ? [...] (1,)
[('sandy', 2)]
ROLLBACK

Упорядочивание или группировка по метке

Важным приемом, особенно в некоторых бэкендах баз данных, является возможность ORDER BY или GROUP BY выражения, которое уже указано в предложении columns, без повторного указания выражения в предложении ORDER BY или GROUP BY, а вместо этого используя имя столбца или имя метки из предложения COLUMNS. Эта форма доступна, если передать строковый текст имени в метод Select.order_by() или Select.group_by(). Переданный текст не отображается напрямую; вместо него отображается имя, заданное выражению в клаузе columns и отображаемое как имя этого выражения в контексте, выдавая ошибку, если совпадение не найдено. Унарные модификаторы asc() и desc() также могут быть использованы в этой форме:

>>> from sqlalchemy import func, desc
>>> stmt = (
...     select(Address.user_id, func.count(Address.id).label("num_addresses"))
...     .group_by("user_id")
...     .order_by("user_id", desc("num_addresses"))
... )
>>> print(stmt)
SELECT address.user_id, count(address.id) AS num_addresses FROM address GROUP BY address.user_id ORDER BY address.user_id, num_addresses DESC

Использование псевдонимов

Теперь, когда мы выбираем из нескольких таблиц и используем объединения, мы быстро сталкиваемся со случаем, когда нам нужно несколько раз сослаться на одну и ту же таблицу в предложении FROM оператора. Для этого мы используем SQL aliases, которые представляют собой синтаксис, дающий альтернативное имя таблице или подзапросу, с которого на нее можно ссылаться в операторе.

В языке выражений SQLAlchemy эти «имена» вместо этого представлены объектами FromClause, известными как конструкция Alias, которая создается в Core с помощью метода FromClause.alias(). Конструкция Alias подобна конструкции Table в том смысле, что она также имеет пространство имен объектов Column внутри коллекции Alias.c. Приведенный ниже оператор SELECT, например, возвращает все уникальные пары имен пользователей:

>>> user_alias_1 = user_table.alias()
>>> user_alias_2 = user_table.alias()
>>> print(
...     select(user_alias_1.c.name, user_alias_2.c.name).join_from(
...         user_alias_1, user_alias_2, user_alias_1.c.id > user_alias_2.c.id
...     )
... )
SELECT user_account_1.name, user_account_2.name AS name_1 FROM user_account AS user_account_1 JOIN user_account AS user_account_2 ON user_account_1.id > user_account_2.id

Псевдонимы сущностей ORM

ORM-эквивалентом метода FromClause.alias() является функция ORM aliased(), которая может быть применена к объекту, такому как User и Address. В результате получается внутренний объект Alias, который противопоставляется оригинальному сопоставленному объекту Table, сохраняя при этом функциональность ORM. Приведенный ниже SELECT выбирает из объекта User все объекты, включающие два определенных адреса электронной почты:

>>> from sqlalchemy.orm import aliased
>>> address_alias_1 = aliased(Address)
>>> address_alias_2 = aliased(Address)
>>> print(
...     select(User)
...     .join_from(User, address_alias_1)
...     .where(address_alias_1.email_address == "patrick@aol.com")
...     .join_from(User, address_alias_2)
...     .where(address_alias_2.email_address == "patrick@gmail.com")
... )
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account JOIN address AS address_1 ON user_account.id = address_1.user_id JOIN address AS address_2 ON user_account.id = address_2.user_id WHERE address_1.email_address = :email_address_1 AND address_2.email_address = :email_address_2

Совет

Как упоминалось в Установка положения о включении, ORM предусматривает другой способ объединения с помощью конструкции relationship(). Приведенный выше пример с использованием псевдонимов демонстрируется с помощью relationship() в Объединение между смежными целями.

Подзапросы и CTE

Подзапрос в SQL - это оператор SELECT, заключенный в круглые скобки и помещенный в контекст вложенного оператора, обычно оператора SELECT, но не обязательно.

В этом разделе мы рассмотрим так называемый «нескалярный» подзапрос, который обычно помещается в предложение FROM вложенного SELECT. Мы также рассмотрим общее табличное выражение или CTE, которое используется так же, как и подзапрос, но включает дополнительные возможности.

SQLAlchemy использует объект Subquery для представления подзапроса и CTE для представления CTE, обычно получаемого из методов Select.subquery() и Select.cte() соответственно. Любой из объектов может быть использован как элемент FROM внутри более крупной конструкции select().

Мы можем построить Subquery, который будет выбирать агрегированный подсчет строк из таблицы address (агрегированные функции и GROUP BY были представлены ранее в Агрегатные функции с GROUP BY / HAVING):

>>> subq = (
...     select(func.count(address_table.c.id).label("count"), address_table.c.user_id)
...     .group_by(address_table.c.user_id)
...     .subquery()
... )

Стрингизация подзапроса сама по себе, без встраивания его внутрь другого Select или другого оператора, дает обычный оператор SELECT без каких-либо заключающих скобок:

>>> print(subq)
SELECT count(address.id) AS count, address.user_id FROM address GROUP BY address.user_id

Объект Subquery ведет себя как любой другой объект FROM, такой как Table, в частности, он включает пространство имен Subquery.c столбцов, которые он выбирает. Мы можем использовать это пространство имен для ссылки как на столбец user_id, так и на наше пользовательское помеченное выражение count:

>>> print(select(subq.c.user_id, subq.c.count))
SELECT anon_1.user_id, anon_1.count FROM (SELECT count(address.id) AS count, address.user_id AS user_id FROM address GROUP BY address.user_id) AS anon_1

С выбором строк, содержащихся в объекте subq, мы можем применить объект к более крупному Select, который присоединит данные к таблице user_account:

>>> stmt = select(user_table.c.name, user_table.c.fullname, subq.c.count).join_from(
...     user_table, subq
... )

>>> print(stmt)
SELECT user_account.name, user_account.fullname, anon_1.count FROM user_account JOIN (SELECT count(address.id) AS count, address.user_id AS user_id FROM address GROUP BY address.user_id) AS anon_1 ON user_account.id = anon_1.user_id

Для того чтобы присоединиться от user_account к address, мы использовали метод Select.join_from(). Как было показано ранее, условие ON этого соединения было снова задано на основе ограничений внешнего ключа. Даже если подзапрос SQL сам по себе не имеет никаких ограничений, SQLAlchemy может действовать на ограничения, представленные в столбцах, определяя, что столбец subq.c.user_id является производным от столбца address_table.c.user_id, который выражает отношение внешнего ключа к столбцу user_table.c.id, который затем используется для создания предложения ON.

Общие табличные выражения (CTE)

Использование конструкции CTE в SQLAlchemy практически не отличается от использования конструкции Subquery. Изменив вызов метода Select.subquery() на использование Select.cte() вместо него, мы можем использовать полученный объект в качестве элемента FROM таким же образом, но в качестве SQL используется совсем другой синтаксис обычного табличного выражения:

>>> subq = (
...     select(func.count(address_table.c.id).label("count"), address_table.c.user_id)
...     .group_by(address_table.c.user_id)
...     .cte()
... )

>>> stmt = select(user_table.c.name, user_table.c.fullname, subq.c.count).join_from(
...     user_table, subq
... )

>>> print(stmt)
WITH anon_1 AS (SELECT count(address.id) AS count, address.user_id AS user_id FROM address GROUP BY address.user_id) SELECT user_account.name, user_account.fullname, anon_1.count FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id

Конструкция CTE также имеет возможность использования в «рекурсивном» стиле, и в более сложных случаях может быть составлена из пункта RETURNING оператора INSERT, UPDATE или DELETE. Подробные сведения об этих дополнительных шаблонах содержатся в документальной строке для CTE.

В обоих случаях подзапрос и CTE были названы на уровне SQL с помощью «анонимного» имени. В коде Python нам совсем не нужно предоставлять эти имена. Идентификатор объекта Subquery или CTE служит синтаксическим идентификатором объекта при отображении. Имя, которое будет отображаться в SQL, можно указать, передав его в качестве первого аргумента методов Select.subquery() или Select.cte().

См.также

Select.subquery() - более подробная информация о подзапросах

Select.cte() - примеры для CTE, включая использование RECURSIVE, а также DML-ориентированных CTE

ORM Entity Subqueries/CTEs

В ORM конструкция aliased() может быть использована для ассоциации сущности ORM, такой как наш класс User или Address, с любой концепцией FromClause, которая представляет собой источник строк. В предыдущем разделе Псевдонимы сущностей ORM показано использование aliased() для ассоциации сопоставленного класса с Alias его сопоставленного Table. Здесь мы иллюстрируем, как aliased() делает то же самое в отношении как Subquery, так и CTE, созданного в отношении конструкции Select, которая в конечном счете является производной от того же сопоставленного Table.

Ниже приведен пример применения aliased() к конструкции Subquery, чтобы из ее строк можно было извлекать сущности ORM. Результат показывает серию объектов User и Address, где данные для каждого объекта Address в конечном итоге получены из подзапроса к таблице address, а не непосредственно из этой таблицы:

>>> subq = select(Address).where(~Address.email_address.like("%@aol.com")).subquery()
>>> address_subq = aliased(Address, subq)
>>> stmt = (
...     select(User, address_subq)
...     .join_from(User, address_subq)
...     .order_by(User.id, address_subq.id)
... )
>>> with Session(engine) as session:
...     for user, address in session.execute(stmt):
...         print(f"{user} {address}")
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname, anon_1.id AS id_1, anon_1.email_address, anon_1.user_id FROM user_account JOIN (SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id FROM address WHERE address.email_address NOT LIKE ?) AS anon_1 ON user_account.id = anon_1.user_id ORDER BY user_account.id, anon_1.id [...] ('%@aol.com',)
User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org')
ROLLBACK

Далее следует другой пример, который точно такой же, только вместо него используется конструкция CTE:

>>> cte_obj = select(Address).where(~Address.email_address.like("%@aol.com")).cte()
>>> address_cte = aliased(Address, cte_obj)
>>> stmt = (
...     select(User, address_cte)
...     .join_from(User, address_cte)
...     .order_by(User.id, address_cte.id)
... )
>>> with Session(engine) as session:
...     for user, address in session.execute(stmt):
...         print(f"{user} {address}")
BEGIN (implicit) WITH anon_1 AS (SELECT address.id AS id, address.email_address AS email_address, address.user_id AS user_id FROM address WHERE address.email_address NOT LIKE ?) SELECT user_account.id, user_account.name, user_account.fullname, anon_1.id AS id_1, anon_1.email_address, anon_1.user_id FROM user_account JOIN anon_1 ON user_account.id = anon_1.user_id ORDER BY user_account.id, anon_1.id [...] ('%@aol.com',)
User(id=1, name='spongebob', fullname='Spongebob Squarepants') Address(id=1, email_address='spongebob@sqlalchemy.org') User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=2, email_address='sandy@sqlalchemy.org') User(id=2, name='sandy', fullname='Sandy Cheeks') Address(id=3, email_address='sandy@squirrelpower.org')
ROLLBACK

Скалярные и коррелированные подзапросы

Скалярный подзапрос - это подзапрос, который возвращает ровно ноль или одну строку и ровно один столбец. Этот подзапрос затем используется в пункте COLUMNS или WHERE заключающего оператора SELECT и отличается от обычного подзапроса тем, что он не используется в пункте FROM. correlated subquery - это скалярный подзапрос, который ссылается на таблицу во вложенном операторе SELECT.

SQLAlchemy представляет скалярный подзапрос с помощью конструкции ScalarSelect, которая является частью иерархии выражений ColumnElement, в отличие от регулярного подзапроса, который представлен конструкцией Subquery, находящейся в иерархии FromClause.

Скалярные подзапросы часто, но не обязательно, используются с агрегатными функциями, представленными ранее в Агрегатные функции с GROUP BY / HAVING. Скалярный подзапрос указывается в явном виде с помощью метода Select.scalar_subquery(), как показано ниже. В строковой форме по умолчанию, когда строка строится сама по себе, она выглядит как обычный оператор SELECT, который выбирает из двух таблиц:

>>> subq = (
...     select(func.count(address_table.c.id))
...     .where(user_table.c.id == address_table.c.user_id)
...     .scalar_subquery()
... )
>>> print(subq)
(SELECT count(address.id) AS count_1 FROM address, user_account WHERE user_account.id = address.user_id)

Приведенный выше объект subq теперь относится к иерархии выражений SQL ColumnElement, и его можно использовать как любое другое выражение для столбцов:

>>> print(subq == 5)
(SELECT count(address.id) AS count_1 FROM address, user_account WHERE user_account.id = address.user_id) = :param_1

Хотя скалярный подзапрос сам по себе отображает и user_account, и address в предложении FROM, когда строится сам по себе, при встраивании его во вложенную конструкцию select(), которая работает с таблицей user_account, таблица user_account автоматически становится коррелированной, то есть она не отображается в предложении FROM подзапроса:

>>> stmt = select(user_table.c.name, subq.label("address_count"))
>>> print(stmt)
SELECT user_account.name, (SELECT count(address.id) AS count_1 FROM address WHERE user_account.id = address.user_id) AS address_count FROM user_account

Простые корреляционные подзапросы обычно выполняют то, что требуется. Однако в случае, когда корреляция неоднозначна, SQLAlchemy даст нам знать, что требуется больше ясности:

>>> stmt = (
...     select(
...         user_table.c.name, address_table.c.email_address, subq.label("address_count")
...     )
...     .join_from(user_table, address_table)
...     .order_by(user_table.c.id, address_table.c.id)
... )
>>> print(stmt)
Traceback (most recent call last):
...
InvalidRequestError: Select statement '<... Select object at ...>' returned
no FROM clauses due to auto-correlation; specify correlate(<tables>) to
control correlation manually.

Чтобы указать, что user_table является тем, с которым мы ищем корреляцию, мы указываем это с помощью методов ScalarSelect.correlate() или ScalarSelect.correlate_except():

>>> subq = (
...     select(func.count(address_table.c.id))
...     .where(user_table.c.id == address_table.c.user_id)
...     .scalar_subquery()
...     .correlate(user_table)
... )

Затем оператор может вернуть данные для этого столбца, как и для любого другого:

>>> with engine.connect() as conn:
...     result = conn.execute(
...         select(
...             user_table.c.name,
...             address_table.c.email_address,
...             subq.label("address_count"),
...         )
...         .join_from(user_table, address_table)
...         .order_by(user_table.c.id, address_table.c.id)
...     )
...     print(result.all())
BEGIN (implicit) SELECT user_account.name, address.email_address, (SELECT count(address.id) AS count_1 FROM address WHERE user_account.id = address.user_id) AS address_count FROM user_account JOIN address ON user_account.id = address.user_id ORDER BY user_account.id, address.id [...] ()
[('spongebob', 'spongebob@sqlalchemy.org', 1), ('sandy', 'sandy@sqlalchemy.org', 2), ('sandy', 'sandy@squirrelpower.org', 2)]
ROLLBACK

ЛАТЕРАЛЬНАЯ корреляция

ЛАТЕРАЛЬНАЯ корреляция - это особая подкатегория SQL-корреляции, которая позволяет выбираемой единице ссылаться на другую выбираемую единицу в одном предложении FROM. Это чрезвычайно специальный случай использования, который, хотя и является частью стандарта SQL, поддерживается только в последних версиях PostgreSQL.

Обычно, если оператор SELECT ссылается на table1 JOIN (SELECT ...) AS subquery в своем предложении FROM, подзапрос с правой стороны не может ссылаться на выражение «table1» с левой стороны; корреляция может ссылаться только на таблицу, которая является частью другого SELECT, полностью охватывающего этот SELECT. Ключевое слово LATERAL позволяет нам перевернуть это поведение и разрешить корреляцию из правого JOIN.

SQLAlchemy поддерживает эту возможность с помощью метода Select.lateral(), который создает объект, известный как Lateral. Lateral относится к тому же семейству, что и Subquery и Alias, но также включает корреляционное поведение, когда конструкция добавляется в предложение FROM вложенного SELECT. Следующий пример иллюстрирует SQL-запрос, в котором используется LATERAL, выбирая данные «учетная запись пользователя / количество адресов электронной почты», как обсуждалось в предыдущем разделе:

>>> subq = (
...     select(
...         func.count(address_table.c.id).label("address_count"),
...         address_table.c.email_address,
...         address_table.c.user_id,
...     )
...     .where(user_table.c.id == address_table.c.user_id)
...     .lateral()
... )
>>> stmt = (
...     select(user_table.c.name, subq.c.address_count, subq.c.email_address)
...     .join_from(user_table, subq)
...     .order_by(user_table.c.id, subq.c.email_address)
... )
>>> print(stmt)
SELECT user_account.name, anon_1.address_count, anon_1.email_address FROM user_account JOIN LATERAL (SELECT count(address.id) AS address_count, address.email_address AS email_address, address.user_id AS user_id FROM address WHERE user_account.id = address.user_id) AS anon_1 ON user_account.id = anon_1.user_id ORDER BY user_account.id, anon_1.email_address

Выше, правая сторона JOIN является подзапросом, который соотносится с таблицей user_account, находящейся на левой стороне соединения.

При использовании Select.lateral() поведение методов Select.correlate() и Select.correlate_except() применяется и к конструкции Lateral.

См.также

Lateral

Select.lateral()

UNION, UNION ALL и другие операции с наборами

В SQL операторы SELECT могут быть объединены вместе с помощью операции UNION или UNION ALL SQL, которая создает набор всех строк, созданных одним или несколькими операторами вместе. Возможны и другие операции с наборами, такие как INTERSECT [ALL] и EXCEPT [ALL].

Конструкция SQLAlchemy Select поддерживает композиции такого рода с помощью таких функций, как union(), intersect() и except_(), а также аналогов «все» union_all(), intersect_all() и except_all(). Все эти функции принимают произвольное количество подселектов, которые обычно являются конструкциями Select, но могут быть и существующей композицией.

Конструкция, создаваемая этими функциями, - это CompoundSelect, которая используется так же, как и конструкция Select, за исключением того, что у нее меньше методов. Например, конструкция CompoundSelect, создаваемая функцией union_all(), может быть вызвана непосредственно с помощью Connection.execute():

>>> from sqlalchemy import union_all
>>> stmt1 = select(user_table).where(user_table.c.name == "sandy")
>>> stmt2 = select(user_table).where(user_table.c.name == "spongebob")
>>> u = union_all(stmt1, stmt2)
>>> with engine.connect() as conn:
...     result = conn.execute(u)
...     print(result.all())
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? UNION ALL SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [generated in ...] ('sandy', 'spongebob')
[(2, 'sandy', 'Sandy Cheeks'), (1, 'spongebob', 'Spongebob Squarepants')]
ROLLBACK

Для использования CompoundSelect в качестве подзапроса, как и Select, он предоставляет метод SelectBase.subquery(), который создает объект Subquery с коллекцией FromClause.c, на которую можно ссылаться в объемлющем select():

>>> u_subq = u.subquery()
>>> stmt = (
...     select(u_subq.c.name, address_table.c.email_address)
...     .join_from(address_table, u_subq)
...     .order_by(u_subq.c.name, address_table.c.email_address)
... )
>>> with engine.connect() as conn:
...     result = conn.execute(stmt)
...     print(result.all())
BEGIN (implicit) SELECT anon_1.name, address.email_address FROM address JOIN (SELECT user_account.id AS id, user_account.name AS name, user_account.fullname AS fullname FROM user_account WHERE user_account.name = ? UNION ALL SELECT user_account.id AS id, user_account.name AS name, user_account.fullname AS fullname FROM user_account WHERE user_account.name = ?) AS anon_1 ON anon_1.id = address.user_id ORDER BY anon_1.name, address.email_address [generated in ...] ('sandy', 'spongebob')
[('sandy', 'sandy@sqlalchemy.org'), ('sandy', 'sandy@squirrelpower.org'), ('spongebob', 'spongebob@sqlalchemy.org')]
ROLLBACK

Выбор объектов ORM из объединений

В предыдущих примерах было показано, как построить UNION на основе двух объектов Table, чтобы затем вернуть строки базы данных. Если мы хотим использовать UNION или другую операцию набора для выбора строк, которые мы затем получаем как объекты ORM, можно использовать два подхода. В обоих случаях мы сначала создаем объект select() или CompoundSelect, который представляет оператор SELECT / UNION / etc, который мы хотим выполнить; этот оператор должен быть составлен против целевых объектов ORM или их базовых сопоставленных объектов Table:

>>> stmt1 = select(User).where(User.name == "sandy")
>>> stmt2 = select(User).where(User.name == "spongebob")
>>> u = union_all(stmt1, stmt2)

Для простого SELECT с UNION, который еще не вложен в подзапрос, их часто можно использовать в контексте выборки объектов ORM с помощью метода Select.from_statement(). При таком подходе оператор UNION представляет весь запрос; никакие дополнительные критерии не могут быть добавлены после использования Select.from_statement():

>>> orm_stmt = select(User).from_statement(u)
>>> with Session(engine) as session:
...     for obj in session.execute(orm_stmt).scalars():
...         print(obj)
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? UNION ALL SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [generated in ...] ('sandy', 'spongebob')
User(id=2, name='sandy', fullname='Sandy Cheeks') User(id=1, name='spongebob', fullname='Spongebob Squarepants')
ROLLBACK

Для более гибкого использования UNION или другой конструкции, связанной с множеством, в качестве компонента, связанного с сущностью, конструкция CompoundSelect может быть организована в подзапрос с помощью CompoundSelect.subquery(), который затем связывается с объектами ORM с помощью функции aliased(). Это работает так же, как и в ORM Entity Subqueries/CTEs, сначала создается специальное «отображение» нужной нам сущности на подзапрос, а затем из него выбирается новая сущность, как если бы это был любой другой отображенный класс. В примере ниже мы можем добавить дополнительные критерии, такие как ORDER BY, вне самого UNION, так как мы можем фильтровать или упорядочивать по столбцам, экспортированным подзапросом:

>>> user_alias = aliased(User, u.subquery())
>>> orm_stmt = select(user_alias).order_by(user_alias.id)
>>> with Session(engine) as session:
...     for obj in session.execute(orm_stmt).scalars():
...         print(obj)
BEGIN (implicit) SELECT anon_1.id, anon_1.name, anon_1.fullname FROM (SELECT user_account.id AS id, user_account.name AS name, user_account.fullname AS fullname FROM user_account WHERE user_account.name = ? UNION ALL SELECT user_account.id AS id, user_account.name AS name, user_account.fullname AS fullname FROM user_account WHERE user_account.name = ?) AS anon_1 ORDER BY anon_1.id [generated in ...] ('sandy', 'spongebob')
User(id=1, name='spongebob', fullname='Spongebob Squarepants') User(id=2, name='sandy', fullname='Sandy Cheeks')
ROLLBACK

подзапросы EXISTS

Ключевое слово SQL EXISTS - это оператор, который используется вместе с scalar subqueries для возврата булевой истины или лжи в зависимости от того, вернет ли оператор SELECT строку. SQLAlchemy включает вариант объекта ScalarSelect под названием Exists, который будет генерировать подзапрос EXISTS и наиболее удобно генерируется с помощью метода SelectBase.exists(). Ниже мы создадим EXISTS, чтобы вернуть строки user_account, которые имеют более одной связанной строки в address:

>>> subq = (
...     select(func.count(address_table.c.id))
...     .where(user_table.c.id == address_table.c.user_id)
...     .group_by(address_table.c.user_id)
...     .having(func.count(address_table.c.id) > 1)
... ).exists()
>>> with engine.connect() as conn:
...     result = conn.execute(select(user_table.c.name).where(subq))
...     print(result.all())
BEGIN (implicit) SELECT user_account.name FROM user_account WHERE EXISTS (SELECT count(address.id) AS count_1 FROM address WHERE user_account.id = address.user_id GROUP BY address.user_id HAVING count(address.id) > ?) [...] (1,)
[('sandy',)]
ROLLBACK

Конструкция EXISTS чаще всего используется в качестве отрицания, например, NOT EXISTS, поскольку она обеспечивает эффективную с точки зрения SQL форму поиска строк, для которых в связанной таблице нет строк. Ниже мы выбираем имена пользователей, у которых нет адресов электронной почты; обратите внимание на двоичный оператор отрицания (~), используемый во втором предложении WHERE:

>>> subq = (
...     select(address_table.c.id).where(user_table.c.id == address_table.c.user_id)
... ).exists()
>>> with engine.connect() as conn:
...     result = conn.execute(select(user_table.c.name).where(~subq))
...     print(result.all())
BEGIN (implicit) SELECT user_account.name FROM user_account WHERE NOT (EXISTS (SELECT address.id FROM address WHERE user_account.id = address.user_id)) [...] ()
[('patrick',)]
ROLLBACK

Работа с функциями SQL

Впервые представленный ранее в этом разделе в Агрегатные функции с GROUP BY / HAVING, объект func служит фабрикой для создания новых объектов Function, которые при использовании в конструкции типа select() создают отображение SQL-функции, обычно состоящее из имени, некоторых скобок (хотя и не всегда) и, возможно, некоторых аргументов. Примеры типичных SQL-функций включают:

  • функция count(), агрегатная функция, которая подсчитывает, сколько строк возвращается:

    >>> print(select(func.count()).select_from(user_table))
    SELECT count(*) AS count_1
    FROM user_account
  • функция lower(), строковая функция, которая преобразует строку в нижний регистр:

    >>> print(select(func.lower("A String With Much UPPERCASE")))
    SELECT lower(:lower_2) AS lower_1
  • функция now(), которая предоставляет текущую дату и время; поскольку это общая функция, SQLAlchemy знает, как отобразить ее по-разному для каждого бэкенда, в случае SQLite используя функцию CURRENT_TIMESTAMP:

    >>> stmt = select(func.now())
    >>> with engine.connect() as conn:
    ...     result = conn.execute(stmt)
    ...     print(result.all())
    
    BEGIN (implicit) SELECT CURRENT_TIMESTAMP AS now_1 [...] () [(datetime.datetime(...),)] ROLLBACK

Поскольку большинство бэкендов баз данных содержат десятки, а то и сотни различных SQL-функций, func старается быть максимально либеральным в том, что он принимает. Любое имя, к которому обращаются из этого пространства имен, автоматически считается SQL-функцией, которая будет отображаться общим образом:

>>> print(select(func.some_crazy_function(user_table.c.name, 17)))
SELECT some_crazy_function(user_account.name, :some_crazy_function_2) AS some_crazy_function_1
FROM user_account

В то же время, относительно небольшой набор чрезвычайно распространенных функций SQL, таких как count, now, max, concat, включает в себя предварительно упакованные версии, которые обеспечивают надлежащую информацию о типизации, а также в некоторых случаях генерацию SQL, специфичную для бэкенда. В приведенном ниже примере показано, как происходит генерация SQL для диалекта PostgreSQL по сравнению с диалектом Oracle для функции now:

>>> from sqlalchemy.dialects import postgresql
>>> print(select(func.now()).compile(dialect=postgresql.dialect()))
SELECT now() AS now_1

>>> from sqlalchemy.dialects import oracle
>>> print(select(func.now()).compile(dialect=oracle.dialect()))
SELECT CURRENT_TIMESTAMP AS now_1 FROM DUAL

Функции имеют возвратные типы

Поскольку функции являются выражениями столбцов, они также имеют SQL datatypes, которые описывают тип данных сгенерированного SQL-выражения. Здесь мы называем эти типы «возвращаемыми типами SQL», имея в виду тип значения SQL, возвращаемого функцией в контексте SQL-выражения на стороне базы данных, в отличие от «возвращаемого типа» функции Python.

Тип возврата любой функции SQL может быть доступен, обычно в целях отладки, путем обращения к атрибуту Function.type:

>>> func.now().type
DateTime()

Эти типы возврата SQL имеют значение при использовании выражения функции в контексте более крупного выражения; то есть, математические операторы будут работать лучше, если тип данных выражения будет таким, как Integer или Numeric, а аксессоры JSON для работы должны использовать такой тип, как JSON. Некоторые классы функций возвращают целые строки вместо значений столбцов, когда необходимо обратиться к конкретным столбцам; такие функции относятся к типу table valued functions.

Тип SQL-возврата функции также может иметь значение при выполнении оператора и получении строк обратно, в тех случаях, когда SQLAlchemy приходится применять обработку набора результатов. Ярким примером этого являются функции, связанные с датой на SQLite, где SQLAlchemy DateTime и связанные с ним типы данных берут на себя роль преобразования строковых значений в объекты Python datetime() по мере получения строк результата.

Чтобы применить определенный тип к создаваемой функции, мы передаем его с помощью параметра Function.type_; аргументом типа может быть либо класс TypeEngine, либо экземпляр. В примере ниже мы передаем класс JSON для создания функции PostgreSQL json_object(), отмечая, что возвращаемый тип SQL будет иметь тип JSON:

>>> from sqlalchemy import JSON
>>> function_expr = func.json_object('{a, 1, b, "def", c, 3.5}', type_=JSON)

Создавая нашу функцию JSON с типом данных JSON, объект выражения SQL приобретает возможности, связанные с JSON, например, доступ к элементам:

>>> stmt = select(function_expr["def"])
>>> print(stmt)
SELECT json_object(:json_object_1)[:json_object_2] AS anon_1

Встроенные функции имеют заранее настроенные типы возврата

Для распространенных агрегатных функций, таких как count, max, min, а также очень небольшого числа функций даты, таких как now, и строковых функций, таких как concat, тип возврата SQL устанавливается соответствующим образом, иногда на основе использования. Функция max и аналогичные функции агрегатной фильтрации устанавливают тип возврата SQL на основе заданного аргумента:

>>> m1 = func.max(Column("some_int", Integer))
>>> m1.type
Integer()

>>> m2 = func.max(Column("some_str", String))
>>> m2.type
String()

Функции даты и времени обычно соответствуют выражениям SQL, описываемым символами DateTime, Date или Time:

>>> func.now().type
DateTime()
>>> func.current_date().type
Date()

Известная строковая функция, такая как concat, будет знать, что выражение SQL будет иметь тип String:

>>> func.concat("x", "y").type
String()

Однако для подавляющего большинства функций SQL SQLAlchemy не имеет их явного присутствия в своем очень небольшом списке известных функций. Например, хотя обычно нет проблем с использованием SQL-функций func.lower() и func.upper() для преобразования регистра строк, SQLAlchemy на самом деле не знает об этих функциях, поэтому они имеют «нулевой» тип возврата SQL:

>>> func.upper("lowercase").type
NullType()

Для простых функций, таких как upper и lower, проблема обычно не существенна, поскольку строковые значения могут быть получены из базы данных без какой-либо специальной обработки типов на стороне SQLAlchemy, а правила когеренции типов SQLAlchemy часто могут правильно определить намерение; например, оператор Python + будет правильно интерпретирован как оператор конкатенации строк на основе рассмотрения обеих сторон выражения:

>>> print(select(func.upper("lowercase") + " suffix"))
SELECT upper(:upper_1) || :upper_2 AS anon_1

В целом, сценарий, в котором параметр Function.type_, скорее всего, необходим, таков:

  1. функция еще не является встроенной функцией SQLAlchemy; об этом можно судить, создав функцию и заметив атрибут Function.type, то есть:

    >>> func.count().type
    Integer()

    против:

    >>> func.json_object('{"a", "b"}').type
    NullType()
  2. Необходима поддержка выражений с поддержкой функций; чаще всего это относится к специальным операторам, связанным с типами данных, таким как JSON или ARRAY.

  3. Необходима обработка значения результата, который может включать такие типы, как DateTime, Boolean, Enum, или снова специальные типы данных, такие как JSON, ARRAY.

Расширенные методы работы с функциями SQL

Следующие подразделы иллюстрируют больше вещей, которые можно сделать с помощью функций SQL. Хотя эти методы менее распространены и более сложны, чем использование базовых функций SQL, они, тем не менее, чрезвычайно популярны, во многом благодаря тому, что PostgreSQL уделяет особое внимание более сложным формам функций, включая формы с табличными и столбцовыми значениями, которые популярны при работе с данными JSON.

Использование функций окна

Оконная функция - это специальное использование агрегатной функции SQL, которая вычисляет суммарное значение по строкам, возвращаемым в группе, по мере обработки отдельных строк результата. В то время как функция типа MAX() даст вам наибольшее значение столбца в наборе строк, использование той же функции в качестве «оконной функции» даст вам наибольшее значение для каждой строки, начиная с этой строки.

В SQL оконные функции позволяют указать строки, над которыми должна быть применена функция, значение «partition», которое рассматривает окно над различными подмножествами строк, и выражение «order by», которое указывает порядок, в котором строки должны быть применены к агрегатной функции.

В SQLAlchemy все SQL-функции, генерируемые пространством имен func, включают метод FunctionElement.over(), который предоставляет синтаксис оконной функции, или «OVER»; создаваемая конструкция - это конструкция Over.

Обычной функцией, используемой с оконными функциями, является функция row_number(), которая просто подсчитывает строки. Мы можем разделить это количество строк по имени пользователя, чтобы пронумеровать адреса электронной почты отдельных пользователей:

>>> stmt = (
...     select(
...         func.row_number().over(partition_by=user_table.c.name),
...         user_table.c.name,
...         address_table.c.email_address,
...     )
...     .select_from(user_table)
...     .join(address_table)
... )
>>> with engine.connect() as conn:  
...     result = conn.execute(stmt)
...     print(result.all())
BEGIN (implicit) SELECT row_number() OVER (PARTITION BY user_account.name) AS anon_1, user_account.name, address.email_address FROM user_account JOIN address ON user_account.id = address.user_id [...] () [(1, 'sandy', 'sandy@sqlalchemy.org'), (2, 'sandy', 'sandy@squirrelpower.org'), (1, 'spongebob', 'spongebob@sqlalchemy.org')] ROLLBACK

Выше использован параметр FunctionElement.over.partition_by, чтобы предложение PARTITION BY было выведено внутри предложения OVER. Мы также можем использовать предложение ORDER BY с помощью параметра FunctionElement.over.order_by:

>>> stmt = (
...     select(
...         func.count().over(order_by=user_table.c.name),
...         user_table.c.name,
...         address_table.c.email_address,
...     )
...     .select_from(user_table)
...     .join(address_table)
... )
>>> with engine.connect() as conn:  
...     result = conn.execute(stmt)
...     print(result.all())
BEGIN (implicit) SELECT count(*) OVER (ORDER BY user_account.name) AS anon_1, user_account.name, address.email_address FROM user_account JOIN address ON user_account.id = address.user_id [...] () [(2, 'sandy', 'sandy@sqlalchemy.org'), (2, 'sandy', 'sandy@squirrelpower.org'), (3, 'spongebob', 'spongebob@sqlalchemy.org')] ROLLBACK

Дополнительные опции для оконных функций включают использование диапазонов; примеры см. в разделе over().

Совет

Важно отметить, что метод FunctionElement.over() применяется только к тем SQL-функциям, которые на самом деле являются агрегатными функциями; в то время как конструкция Over с радостью выполнится для любой заданной SQL-функции, база данных отвергнет выражение, если сама функция не является агрегатной функцией SQL.

Специальные модификаторы WITHIN GROUP, FILTER

Синтаксис SQL «WITHIN GROUP» используется в сочетании с агрегатной функцией «упорядоченный набор» или «гипотетический набор». Обычные функции «упорядоченного множества» включают percentile_cont() и rank(). SQLAlchemy включает встроенные реализации rank, dense_rank, mode, percentile_cont и percentile_disc, которые включают метод FunctionElement.within_group():

>>> print(
...     func.unnest(
...         func.percentile_disc([0.25, 0.5, 0.75, 1]).within_group(user_table.c.name)
...     )
... )
unnest(percentile_disc(:percentile_disc_1) WITHIN GROUP (ORDER BY user_account.name))

«FILTER» поддерживается некоторыми бэкендами для ограничения диапазона агрегатной функции определенным подмножеством строк по сравнению с общим диапазоном возвращаемых строк, доступно с помощью метода FunctionElement.filter():

>>> stmt = (
...     select(
...         func.count(address_table.c.email_address).filter(user_table.c.name == "sandy"),
...         func.count(address_table.c.email_address).filter(
...             user_table.c.name == "spongebob"
...         ),
...     )
...     .select_from(user_table)
...     .join(address_table)
... )
>>> with engine.connect() as conn:  
...     result = conn.execute(stmt)
...     print(result.all())
BEGIN (implicit) SELECT count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_1, count(address.email_address) FILTER (WHERE user_account.name = ?) AS anon_2 FROM user_account JOIN address ON user_account.id = address.user_id [...] ('sandy', 'spongebob')
[(2, 1)]
ROLLBACK

Функции с табличными значениями

Таблично-значные функции SQL поддерживают скалярное представление, содержащее именованные подэлементы. Часто используется для JSON и ARRAY-ориентированных функций, а также функций типа generate_series(), таблично-оцениваемая функция указывается в предложении FROM, а затем обозначается как таблица или иногда даже как столбец. Функции такой формы широко распространены в базе данных PostgreSQL, однако некоторые формы таблично-оцениваемых функций также поддерживаются SQLite, Oracle и SQL Server.

См.также

postgresql_table_valued_overview - в документации PostgreSQL.

Хотя многие базы данных поддерживают табличные значения и другие специальные формы, PostgreSQL, как правило, пользуется наибольшим спросом. Дополнительные примеры синтаксиса PostgreSQL, а также дополнительные возможности смотрите в этом разделе.

SQLAlchemy предоставляет метод FunctionElement.table_valued() как базовую конструкцию «функция, оцениваемая таблицей», которая преобразует объект func в предложение FROM, содержащее ряд именованных столбцов, основанных на строковых именах, переданных позиционно. В результате возвращается объект TableValuedAlias, который представляет собой конструкцию Alias с функцией, которая может быть использована как любое другое предложение FROM, представленное в Использование псевдонимов. Ниже мы проиллюстрируем функцию json_each(), которая, будучи распространенной в PostgreSQL, также поддерживается современными версиями SQLite:

>>> onetwothree = func.json_each('["one", "two", "three"]').table_valued("value")
>>> stmt = select(onetwothree).where(onetwothree.c.value.in_(["two", "three"]))
>>> with engine.connect() as conn:
...     result = conn.execute(stmt)
...     result.all()
BEGIN (implicit) SELECT anon_1.value FROM json_each(?) AS anon_1 WHERE anon_1.value IN (?, ?) [...] ('["one", "two", "three"]', 'two', 'three')
[('two',), ('three',)]
ROLLBACK

Выше мы использовали функцию json_each() JSON, поддерживаемую SQLite и PostgreSQL, для создания табличного ценностного выражения с одним столбцом, обозначаемым как value, а затем выбрали две из трех его строк.

См.также

postgresql_table_valued - в документации PostgreSQL - в этом разделе будут подробно описаны дополнительные синтаксисы, такие как специальные производные столбцов и «WITH ORDINALITY», которые, как известно, работают с PostgreSQL.

Функции, оцениваемые по столбцу - Функция, оцениваемая по таблице, как скалярный столбец

Специальный синтаксис, поддерживаемый PostgreSQL и Oracle, заключается в ссылке на функцию в предложении FROM, которая затем передается как один столбец в предложении columns оператора SELECT или другом контексте выражения столбца. PostgreSQL широко использует этот синтаксис для таких функций, как json_array_elements(), json_object_keys(), json_each_text(), json_each() и т.д.

SQLAlchemy называет это «функцией, оцениваемой по столбцам», и она доступна путем применения модификатора FunctionElement.column_valued() к конструкции Function:

>>> from sqlalchemy import select, func
>>> stmt = select(func.json_array_elements('["one", "two"]').column_valued("x"))
>>> print(stmt)
SELECT x
FROM json_array_elements(:json_array_elements_1) AS x

Форма «column valued» также поддерживается диалектом Oracle, где она используется для пользовательских функций SQL:

>>> from sqlalchemy.dialects import oracle
>>> stmt = select(func.scalar_strings(5).column_valued("s"))
>>> print(stmt.compile(dialect=oracle.dialect()))
SELECT COLUMN_VALUE s
FROM TABLE (scalar_strings(:scalar_strings_1)) s

См.также

postgresql_column_valued - в документации PostgreSQL.

Приведение данных и принуждение типов

В SQL нам часто требуется явно указать тип данных выражения, либо чтобы сообщить базе данных, какой тип ожидается в неоднозначном выражении, либо в некоторых случаях, когда мы хотим преобразовать подразумеваемый тип данных выражения SQL во что-то другое. Для этой задачи используется ключевое слово SQL CAST, которое в SQLAlchemy обеспечивается функцией cast(). Эта функция принимает в качестве аргументов выражение столбца и объект типа данных, как показано ниже, где мы получаем SQL-выражение CAST(user_account.id AS VARCHAR) из объекта столбца user_table.c.id:

>>> from sqlalchemy import cast
>>> stmt = select(cast(user_table.c.id, String))
>>> with engine.connect() as conn:
...     result = conn.execute(stmt)
...     result.all()
BEGIN (implicit) SELECT CAST(user_account.id AS VARCHAR) AS id FROM user_account [...] ()
[('1',), ('2',), ('3',)]
ROLLBACK

Функция cast() не только отображает синтаксис SQL CAST, но и создает выражение столбца SQLAlchemy, которое будет действовать как данный тип данных на стороне Python. Строковое выражение от cast() до JSON получит JSON subscript и операторы сравнения, например:

>>> from sqlalchemy import JSON
>>> print(cast("{'a': 'b'}", JSON)["a"])
CAST(:param_1 AS JSON)[:param_2]

type_coerce() - «приведение» только в Python

Иногда возникает необходимость, чтобы SQLAlchemy знал тип данных выражения по всем вышеупомянутым причинам, но не отображал само выражение CAST на стороне SQL, где оно может помешать SQL-операции, которая уже работает без него. Для этого довольно распространенного случая использования существует еще одна функция type_coerce(), которая тесно связана с cast(), в том смысле, что она устанавливает выражение Python как имеющее определенный тип базы данных SQL, но не отображает ключевое слово CAST или тип данных на стороне базы данных. type_coerce() особенно важно при работе с типом данных JSON, который обычно имеет сложную связь со строково-ориентированными типами данных на разных платформах и может даже не быть явным типом данных, как, например, в SQLite и MariaDB. Ниже мы используем type_coerce() для передачи структуры Python в виде строки JSON в одну из функций MySQL JSON:

>>> import json
>>> from sqlalchemy import JSON
>>> from sqlalchemy import type_coerce
>>> from sqlalchemy.dialects import mysql
>>> s = select(type_coerce({"some_key": {"foo": "bar"}}, JSON)["some_key"])
>>> print(s.compile(dialect=mysql.dialect()))
SELECT JSON_EXTRACT(%s, %s) AS anon_1

Выше SQL-функция MySQL JSON_EXTRACT была вызвана, поскольку мы использовали type_coerce(), чтобы указать, что наш словарь Python должен рассматриваться как JSON. В результате оператор Python __getitem__, ['some_key'] в данном случае, стал доступен и позволил отобразить выражение пути JSON_EXTRACT (не показано, однако в данном случае оно в конечном итоге будет '$."some_key"').

Вернуться на верх