Как сделать вложенный SELECT без JOIN в SqlAlchemy? - PullRequest
0 голосов
/ 10 июня 2019

У меня есть запрос Postgres (через SQLAlchemy), который выбирает совпадающие строки, используя сложные критерии:

original_query = session.query(SomeTable).filter(*complex_filters)

Я не знаю точно, как создается запрос, у меня есть доступ только к полученному экземпляру Query.

Теперь я хочу использовать этот «непрозрачный» запрос (черный ящик для целей этого вопроса) для построения других запросов из той же таблицы, используя точно такие же критерии, но с дополнительной логикой поверхсогласованные original_query строк.Например, с SELECT DISTINCT(column) сверху:

another_query = session.query(SomeTable.column).distinct().?select_from_query?(original_query)

или

SELECT SUM(tab_value) FROM (
    SELECT tab.key AS tab_key, tab.value AS tab_value -- inner query, fixed
    FROM tab
    WHERE tab.product_id IN (1, 2)  -- simplified; the inner query is quite complex
) AS tbl
WHERE tab_key = 'length';

или

SELECT tab_key, COUNT(*) FROM (
    SELECT tab.key AS tab_key, tab.value AS tab_value
    FROM tab
    WHERE tab.product_id IN (1, 2)
) AS tbl
GROUP BY tab_key;

и т. Д.

Как реализовать это?select_from_query? расстаться чисто, в SQLAlchemy? В основном, как сделать SELECT dynamic FROM (SELECT fixed) в SqlAlchemy?


Мотивация: внутренний объект Query происходит из другой части кода.У меня нет контроля над тем, как он построен, и я хочу избежать дублирования его логики ad-hoc для каждого SELECT, который мне нужно выполнить поверх него.Я хочу повторно использовать этот запрос, но добавлю дополнительную логику сверху (согласно приведенным выше примерам).

1 Ответ

2 голосов
/ 10 июня 2019

original_query - это просто объект API запроса SQLAlchemy , к нему можно применить дополнительные фильтры и критерии.API запроса генеративный ;каждая Query() операция экземпляра возвращает новый (неизменяемый) экземпляр, и ваша начальная точка (original_query) не изменяется.

Это включает использование Query.distinct() для добавления предложения DISTINCT(), Query.with_entities() для изменения того, какие столбцы являются частью запроса, и Query.values(), чтобы выполнить ваш запрос, но вернуть только определенные значения в одном столбце.

Используйте либо .distinct(<column>).with_entities(<column>) для создания нового объекта запроса (который может быть использован повторно):

another_query = original_query.distinct(SomeTable.column).with_entities(SomeTable.column)

или просто используйте .distinct(<column>).values(<column>), чтобы сразу получить итератор (column_value,) результатов кортежа:

distinct_values = original_query.distinct(SomeTable.column).values(SomeTable.column)

Обратите внимание, что .values() выполняет запрос немедленно, как .all(), а.with_entities() возвращает вам новый Query объект только с одним столбцом (и тогда .all() или итерация или нарезка будут выполнять и возвращать результаты).

Демонстрация с использованием надуманной Foo модели(выполняется для sqlite, чтобы упростить демонстрацию):

>>> from sqlalchemy import *
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy.orm import sessionmaker
>>> Base = declarative_base()
>>> class Foo(Base):
...     __tablename__ = "foo"
...     id = Column(Integer, primary_key=True)
...     bar = Column(String)
...     spam = Column(String)
...
>>> engine = create_engine('sqlite:///:memory:', echo=True)
>>> session = sessionmaker(bind=engine)()
>>> Base.metadata.create_all(engine)
2019-06-10 13:10:43,910 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("foo")
2019-06-10 13:10:43,910 INFO sqlalchemy.engine.base.Engine ()
2019-06-10 13:10:43,911 INFO sqlalchemy.engine.base.Engine
CREATE TABLE foo (
    id INTEGER NOT NULL,
    bar VARCHAR,
    spam VARCHAR,
    PRIMARY KEY (id)
)


2019-06-10 13:10:43,911 INFO sqlalchemy.engine.base.Engine ()
2019-06-10 13:10:43,913 INFO sqlalchemy.engine.base.Engine COMMIT
>>> original_query = session.query(Foo).filter(Foo.id.between(17, 42))
>>> print(original_query)  # show what SQL would be executed for this query
SELECT foo.id AS foo_id, foo.bar AS foo_bar, foo.spam AS foo_spam
FROM foo
WHERE foo.id BETWEEN ? AND ?
>>> another_query = original_query.distinct(Foo.bar).with_entities(Foo.bar)
>>> print(another_query)  # print the SQL again, don't execute
SELECT DISTINCT foo.bar AS foo_bar
FROM foo
WHERE foo.id BETWEEN ? AND ?
>>> distinct_values = original_query.distinct(Foo.bar).values(Foo.bar)  # executes!
2019-06-10 13:10:48,470 INFO sqlalchemy.engine.base.Engine SELECT DISTINCT foo.bar AS foo_bar
FROM foo
WHERE foo.id BETWEEN ? AND ?
2019-06-10 13:10:48,470 INFO sqlalchemy.engine.base.Engine (17, 42)

В вышеприведенной демонстрации исходный запрос выбрал бы определенные Foo экземпляра с фильтром BETWEEN, но добавив .distinct(Foo.bar).values(Foo.bar), затемвыполняет запрос для просто столбец DISTINCT foo.bar, но с тем же BETWEEN фильтр на месте.Точно так же, используя .with_entities(), мы получили новый объект запроса только для этого одного столбца, но фильтр все еще является частью этого нового запроса.

Ваш добавленный пример работает точно так же;вам на самом деле не нужно иметь дополнительный выбор там, так как тот же запрос может быть выражен как:

SELECT sum(tab.value)
FROM tab
WHERE tab.product_id IN (1, 2) AND tab_key = 'length';

, что можно сделать, просто добавив дополнительные фильтры, а затем используйте .with_entities() для заменыстолбцы, выбранные с помощью SUM():

summed_query = (
    original_query
    .filter(Tab.key == 'length')  # add a filter
    .with_entities(func.sum(Tab.value)

или, с точки зрения вышеприведенной демонстрации Foo:

>>> print(original_query.filter(Foo.spam == 42).with_entities(func.sum(Foo.bar)))
SELECT sum(foo.bar) AS sum_1
FROM foo
WHERE foo.id BETWEEN ? AND ? AND foo.spam = ?

Существуют варианты использования для подзапросов (например,ограничение результатов из определенной таблицы в объединении), но это не один из них.

Если вам нужен подзапрос, тогда API запроса имеет Query.from_self() (дляболее простые случаи) и Query.subselect().

Например, если вам нужно было выбрать только агрегированные строки из исходного запроса и отфильтровать агрегированные значения с помощью HAVING, а затем присоединиться крезультаты с другой таблицей для самого высокого идентификатора строки для каждой группы и некоторой дальнейшей фильтрации, а затем вам нужен подзапрос:

summed_col = func.sum(SomeTable.some_column)
max_id = func.max(SomeTable.primary_key)
summed_results_by_eggs = (
    original_query
    .with_entities(max_id, summed_col)      # only select highest id and the sum
    .group_by(SomeTable.other_column)       # per group
    .having(summed_col > 10)                # where the sum is high enough
    .from_self(summed_col)                  # give us the summed value as a subselect
    .join(                                  # join these rows with another table
        OtherTable,
        OtherTable.foreign_key == max_id    # using the highest id
    )
    .filter(OtherTable.some_column < 1000)  # and filter some more
)

Приведенное выше выберет только суммированные значения SomeTable.some_column, где это значение больше 10и где самое высокое значение SomeTable.id в каждой группе.Этот запрос имеет для использования подзапроса, потому что вы хотите ограничить допустимые строки SomeTable перед объединением с другой таблицей.

Чтобы продемонстрировать это, я добавил вторую таблицу Eggs:

>>> from sqlalchemy.orm import relationship
>>> class Eggs(Base):
...     __tablename__ = "eggs"
...     id = Column(Integer, primary_key=True)
...     foo_id = Column(Integer, ForeignKey(Foo.id))
...     foo = relationship(Foo, backref="eggs")
...
>>> summed_col = func.sum(Foo.bar)
>>> max_id = func.max(Foo.id)
>>> print(
...     original_query
...     .with_entities(max_id, summed_col)
...     .group_by(Foo.spam)
...     .having(summed_col > 10)
...     .from_self(summed_col)
...     .join(Eggs, Eggs.foo_id==max_id)
...     .filter(Eggs.id < 1000)
... )
SELECT anon_1.sum_2 AS sum_1
FROM (SELECT max(foo.id) AS max_1, sum(foo.bar) AS sum_2
FROM foo
WHERE foo.id BETWEEN ? AND ? GROUP BY foo.spam
HAVING sum(foo.bar) > ?) AS anon_1 JOIN eggs ON eggs.foo_id = anon_1.max_1
WHERE eggs.id < ?

Метод Query.from_self() принимает новые сущности для использования во внешнем запросе, если вы их опускаете, все столбцы извлекаются.Выше я вытащил суммированное значение столбца;без этого аргумента будет также выбран столбец MAX(Foo.id).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...