具有动态列数

时间:2017-06-23 20:50:24

标签: sql postgresql pivot crosstab postgresql-9.5

我正在尝试找到一种方法来返回具有动态列数的记录集。我可以编写一个查询来生成我需要的列名列表:

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);

如果我针对2017-06-21到2017-06-23的数据范围运行查询,我希望获得如下结果,包括在那段时间运行的任何测试的矩阵:

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。

3 个答案:

答案 0 :(得分:1)

启用'tablefunc'扩展名,然后使用'crosstab'功能;请参阅PG文档:https://www.postgresql.org/docs/current/static/tablefunc.html。交叉表函数的参数应该是生成三列的查询文本:日期,测试名称和测试成功,按顺序。

答案 1 :(得分:1)

使用了不同的方法,这里提到的一些方法就像交叉表一样。此外,您可以构建一个自己的函数来动态构建查询,并返回TABLE和更多的方法。

但是所有这些都要求您预定义确切数量的输出及其数据类型。

如果我理解你的情况,那就是你提到的那些你不想要的东西:

  

如果我们现在需要每天开始踢设备,我们需要更新   查询。

使用交叉表和其他方式几乎是同样的缺点。

所以有一种方法可以使用Cursors。这可能不是最好的方法,如果你可以使用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)

正如我所提到的,这不是完美的方式,但它可以完成工作:)

答案 2 :(得分:0)

我会想象这样的查询:

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;

最好在应用程序级别转动数据。