SQLAlchemy - как рассчитывать разные значения по нескольким столбцам - PullRequest
2 голосов
/ 27 апреля 2020

У меня есть этот запрос:

SELECT COUNT(DISTINCT Serial, DatumOrig, Glucose) FROM values;

Я пытался воссоздать его с помощью SQLAlchemy таким образом:

session.query(Value.Serial, Value.DatumOrig, Value.Glucose).distinct().count()

Но это означает:

SELECT count(*) AS count_1
    FROM (SELECT DISTINCT 
           values.`Serial` AS `values_Serial`, 
           values.`DatumOrig` AS `values_DatumOrig`,
           values.`Glucose` AS `values_Glucose`
          FROM values)
    AS anon_1

Который не вызывает встроенную функцию подсчета, но заключает выделенный отдельный в подзапрос.

Мой вопрос: как SQLAlchemy по-разному подсчитывает отдельный выбор на нескольких столбцах и во что они переводят?

Есть ли какое-нибудь решение, которое могло бы перевести на мой оригинальный запрос? Есть ли серьезная разница в производительности или использовании памяти?

1 Ответ

2 голосов
/ 27 апреля 2020

Прежде всего, я думаю, что COUNT(DISTINCT) с поддержкой более 1 выражения является расширением MySQL. Вы можете добиться того же самого, например, PostgreSQL со значениями ROW, но в отношении NULL поведение не то же самое. В MySQL, если какое-либо из значений выражения оценивается как NULL, строка не квалифицируется. Это также приводит к разнице между двумя запросами в вопросе:

  1. Если любой из Serial, DatumOrig или Glucose равен NULL в запросе COUNT(DISTINCT), эта строка не квалифицируется или другими словами не учитывается.
  2. COUNT(*) - количество подзапросов anon_1, или, другими словами, количество строк. SELECT DISTINCT Serial, DatumOrig, Glucose будет включать в себя (различные) строки с NULL.

Если посмотреть на вывод EXPLAIN для 2 запросов, то выглядит, что подзапрос заставляет MySQL использовать временную таблицу. Скорее всего, это приведет к разнице в производительности, особенно если она материализована на диске.

Создание многозначного запроса COUNT(DISTINCT) в SQLAlchemy немного сложнее, потому что count() является обобщенным Функция c и реализована ближе к стандарту SQL. Он принимает только одно выражение в качестве (необязательного) позиционного аргумента, и то же самое относится к distinct(). Если ничего не помогает, вы всегда можете вернуться к text() фрагментам, как в этом случае:

# NOTE: text() fragments are included in the query as is, so if the text originates
# from an untrusted source, the query cannot be trusted.
session.query(func.count(distinct(text("`Serial`, `DatumOrig`, `Glucose`")))).\
    select_from(Value).\
    scalar()

, который далек от читаемого и обслуживаемого кода, но выполняет работу сейчас , Другой вариант - написать пользовательскую конструкцию, которая реализует расширение MySQL, или переписать запрос, как вы пытались. Один из способов создания пользовательской конструкции, которая выдает требуемый SQL, будет выглядеть так:

from itertools import count
from sqlalchemy import func, distinct as _distinct

def _comma_list(exprs):
    # NOTE: Magic number alert, the precedence value must be large enough to avoid
    # producing parentheses around the "comma list" when passed to distinct()
    ps = count(10 + len(exprs), -1)
    exprs = iter(exprs)
    cl = next(exprs)
    for p, e in zip(ps, exprs):
        cl = cl.op(',', precedence=p)(e)

    return cl

def distinct(*exprs):
    return _distinct(_comma_list(exprs))

session.query(func.count(distinct(
    Value.Serial, Value.DatumOrig, Value.Glucose))).scalar()
...