Запрос PostgreSQL с динамическим количеством столбцов

Я пытаюсь найти способ вернуть набор записей с динамическим количеством столбцов. Я могу написать один запрос, который создаст список имен столбцов, которые мне нужны как таковые:

SELECT DISTINCT name FROM tests WHERE group = 'basic';

Это вернет короткий список, такой как «poke», «prod», «hit», «drop» и т. д. Затем я хочу создать таблицу, показывающую серию тестов, в которых выполнялся каждый из этих тестов. Каждое утро мы смотрим на то, что делают разработчики, и проверяем и подталкиваем к этому, чтобы каждый тест выполнялся каждый день. Этот запрос я могу написать статически:

SELECT (SELECT success FROM test_results AS i
        WHERE i.name = 'poke'
        AND i.date = o.date) AS 'poke',
       (SELECT success FROM test_results AS i
        WHERE i.name = 'prod'
        AND i.date = o.date) AS 'prod',
...
FROM test_results AS o GROUP BY date
HAVING date > now() - '1 week'::interval;

Однако это жестко запрограммировано для тестов, которые мы проводим каждый день. Если нам теперь нужно каждый день пинать устройство, нужно обновить запрос. Если мы решим, что тест на падение больше не нужен, через неделю столбец теста на падение должен исчезнуть из отчета, поскольку он больше не встречается в результатах. Возврат NULL для отсутствующих тестов, когда только определенные даты имеют запись результатов, вполне допустимо.

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

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

Изменить. Вот пример данных за последние два дня:

CREATE TABLE test_results (
    name TEXT NOT NULL,
    date DATE default now() NOT NULL,
    success BOOLEAN NOT NULL
);

INSERT INTO test_results (name, date, success) VALUES ('hit',  '2017-06-20', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('poke', '2017-06-20', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('prod', '2017-06-20', TRUE);

INSERT INTO test_results (name, date, success) VALUES ('poke', '2017-06-21', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('prod', '2017-06-21', TRUE);

INSERT INTO test_results (name, date, success) VALUES ('poke', '2017-06-22', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('prod', '2017-06-22', FALSE);

INSERT INTO test_results (name, date, success) VALUES ('poke', '2017-06-23', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('prod', '2017-06-23', TRUE);
INSERT INTO test_results (name, date, success) VALUES ('drop', '2017-06-23', TRUE);

Если я выполню свой запрос для диапазона данных с 21.06.2017 по 23.06.2017, я хотел бы получить такие результаты, как следующие, включая матрицу любых тестов, которые были выполнены за это время:

date        | poke   | prod   | drop
------------+--------+--------+-----
2017-06-21  | TRUE   | TRUE   | NULL
2017-06-22  | TRUE   | FALSE  | NULL
2017-06-23  | TRUE   | TRUE   | TRUE

Названия poke, prod и drop были именами, найденными в поле имени строки в течение этого периода времени. NULL возвращается для подробного запроса для любых тестов, которые не имеют записи на эту дату.


person penguin359    schedule 23.06.2017    source источник
comment
Отредактируйте свой вопрос и предоставьте образцы данных и желаемые результаты.   -  person Gordon Linoff    schedule 23.06.2017
comment
Вам нужно динамически построить свой оператор внутри процедуры, а затем выполнить его. Я делал такие вещи в прошлом, но мне нужно посмотреть это на моем диске. Если никто не даст вам правильный ответ, я посмотрю на него завтра.   -  person Kamil Gosciminski    schedule 24.06.2017


Ответы (3)


Используются различные методы, некоторые из которых уже упоминались здесь, например кросс-таблица. Кроме того, вы можете создать собственную функцию, которая динамически строит запрос и возвращает результат TABLE и еще несколько методов.

Но все требуют, чтобы вы заранее определили точное количество выходов и их типы данных.

Если я понимаю ваш случай, это то, чего вы не хотели бы, как вы упомянули:

Если нам теперь нужно каждый день пинать устройство, нужно обновить запрос.

Это почти тот же недостаток, что и при использовании кросс-таблицы и других способов.

Таким образом, есть способ использовать Курсоры. Вероятно, это не лучший способ, и если вы можете использовать crosstab, то это, вероятно, лучше.
Но, по крайней мере, я добавлю эту опцию с комментариями в код.

Решение:

-- Function for opening cursor
CREATE OR REPLACE
FUNCTION    test_stats(
                c REFCURSOR,    -- cursor name
                sdate date,     -- start date of period wanted (included)
                edate date,     -- end date of period wanted (included)
                gtype text      -- you had in your 'tests' table some group type which I included just in case
            )
RETURNS     REFCURSOR
LANGUAGE    PLPGSQL
AS
$main$
BEGIN
    OPEN    c
    FOR
    -- Following dynamic query building can be
    -- used also if want to go with function that RETURNS TABLE
    EXECUTE format(
            '   SELECT  r.date,
                        %s
                FROM    test_results r
                WHERE   r.date BETWEEN %L AND %L
                GROUP BY 1
            ',
                -- Here we build for each 'name' own statement and 
                -- aggregate together with comma separator to feed
                -- into main query.
                -- P.S. We need to double check result unfortunately
                --      against test_results table once to get pre-filter
                --      for names in specified date range.
                --      With this we eliminate tests that for sure will
                --      not be presented in the range. In given test data
                --      this means eliminating 'hit'.
            (
                SELECT  string_agg(
                            DISTINCT format(
                                '(  SELECT  success
                                    FROM    test_results i
                                    WHERE   i.name = %1$L
                                    AND     i.date = r.date ) AS "%1$s"',
                                t.name
                            ),
                            ','
                        )
                FROM    tests t,
                LATERAL (   SELECT  array_agg( DISTINCT r.name )
                            FROM    test_results r
                            WHERE   r.date BETWEEN sdate AND edate
                        ) a( lst )
                WHERE   t.group = gtype     -- the group type is used here
                AND     t.name = ANY ( a.lst::text[] )
            ),
            sdate,      -- start date for between statement
            edate       -- end date for between statement
        );
    RETURN c;
END;
$main$;

-- Usage example:
BEGIN;
SELECT test_stats( 'teststats1', '2017-06-21'::date, '2017-06-23'::date, 'basic' );
FETCH ALL IN teststats1;
COMMIT;

-- Result (from your given test data set):
    date    | drop | poke | prod
------------+------+------+------
 2017-06-22 |      | t    | f
 2017-06-21 |      | t    | t
 2017-06-23 | t    | t    | t
(3 rows)

Как я уже говорил, это не идеальный способ, но он работает :)

person Kristo Mägi    schedule 24.06.2017
comment
Поскольку теперь стало ясно, что SQL сам по себе является языком статического типа, как C или Java, то, о чем я прошу, невозможно. Я имею в виду языки с динамическими типами, такие как Python или JavaScript. Хотя различные функции могут изменять свою сигнатуру в соответствии с их контекстом, в конечном итоге выбор типов осуществляется при компиляции (подготовке?) исходного оператора SQL. - person penguin359; 28.06.2017

Включите расширение tablefunc, а затем используйте функцию crosstab; см. документы PG: https://www.postgresql.org/docs/current/static/tablefunc.html. Аргументом функции кросс-таблицы должен быть текст запроса, который создает три столбца: дату, имя теста и успех теста в указанном порядке.

person rd_nielsen    schedule 23.06.2017
comment
Основной недостаток crosstab: по-прежнему требуется хардкодить список полей в основном запросе. - person Abelisto; 24.06.2017
comment
Вот сообщение в блоге, показывающее, как использовать динамический SQL для создания списка полей, чтобы не было необходимости в жестком кодировании: splinterofthesingularity.blogspot.com/2021/03/ - person rd_nielsen; 21.03.2021

Я бы представил такой запрос:

SELECT tr.name, tr.date, tr.success
FROM tests t JOIN
     test_results tr
     ON t.testid = tr.testid
WHERE t.group = 'basic' AND tr.date > now() - '1 week'::interval;

Вам, вероятно, лучше сводить данные на уровне приложения.

person Gordon Linoff    schedule 23.06.2017
comment
Сама причина, по которой я пытаюсь сделать это в SQL, заключается в том, что я не контролирую прикладной уровень. Он просто представляет результаты оператора SQL в виде таблицы/матрицы в приложении. - person penguin359; 24.06.2017