PostgreSQL 交叉表查询

Posted

技术标签:

【中文标题】PostgreSQL 交叉表查询【英文标题】:PostgreSQL Crosstab Query 【发布时间】:2010-06-09 01:13:03 【问题描述】:

有人知道如何在 PostgreSQL 中创建交叉表查询吗? 例如我有下表:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

我希望查询返回以下交叉表:

Section    Active    Inactive
A          1         2
B          4         5

这可能吗?

【问题讨论】:

我的结构略有不同,发现这个例子有点难以理解,所以我记录了我对这个***.com/q/49051959/808723 的思考方式。也许它对任何人都有帮助。 【参考方案1】:

为每个数据库安装additional module tablefunc一次,它提供了函数crosstab()。从 Postgres 9.1 开始,您可以为此使用 CREATE EXTENSION

CREATE EXTENSION IF NOT EXISTS tablefunc;

改进的测试用例

CREATE TABLE tbl (
   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

简单的形式 - 不适合缺少的属性

crosstab(text)1 输入参数:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

返回:

部分 |活跃 |不活跃 ---------+--------+---------- 一个 | 1 | 2 乙| 4 | 5 C | 7 | ——!! 无需转换和重命名。 注意C不正确结果:值7 被填入第一列。有时,这种行为是可取的,但不适用于此用例。 在提供的输入查询中,简单形式也仅限于恰好三列:row_namecategoryvalue时间>。 额外的列没有空间,就像下面的 2 参数替代方案一样。

安全表单

crosstab(text, text) 带有 2 输入参数:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

返回:

部分 |活跃 |不活跃 ---------+--------+---------- 一个 | 1 | 2 乙| 4 | 5 C | | 7——!!

注意C 的正确结果。

第二个参数 可以是任何查询,每个属性返回一个,匹配最后列定义的顺序。通常,您会希望像这样从基础表中查询不同的属性:

  'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

手册上有。

由于无论如何您都必须拼出列定义列表中的所有列(预定义的crosstab<i>N</i>() 变体除外),因此在VALUES 表达式中提供简短列表通常更有效,如下所示:

    $$VALUES ('Active'::text), ('Inactive')$$)

或者(不在手册中):

    $$SELECT unnest('Active,Inactive'::text[])$$  -- short syntax for long lists

我使用dollar quoting 来简化引用。

您甚至可以使用crosstab(text, text) 输出具有不同 数据类型的列 - 只要值列的文本表示是目标类型的有效输入。这样,您可能拥有不同种类的属性,并为各自的属性输出textdatenumeric 等。 chapter crosstab(text, text) in the manual末尾有代码示例。

db小提琴here

过多输入行的影响

多余的输入行以不同方式处理 - 相同(“row_name”,“category”)组合的重复行 - 在上面的示例中为(section, status)

1-parameter 表单从左到右填充可用值列。多余的值将被丢弃。较早的输入行获胜。

2-parameter 表单将每个输入值分配给其专用列,覆盖之前的任何分配。后面的输入行获胜。

通常,您一开始就没有重复项。但如果您这样做了,请根据您的要求仔细调整排序顺序 - 并记录发生的情况。 如果您不在乎,也可以快速获得任意结果。请注意效果。

高级示例

Pivot on Multiple Columns using Tablefunc - 还展示了提到的“额外列”

Dynamic alternative to pivot with CASE and GROUP BY

\crosstabview 在 psql 中

Postgres 9.6 将此元命令添加到其默认交互式终端 psql。您可以运行将用作第一个 crosstab() 参数的查询并将其提供给 \crosstabview(立即或在下一步中)。喜欢:

db=> SELECT section, status, ct FROM tbl \crosstabview

与上面的结果类似,但它是客户端的表示功能。输入行的处理方式略有不同,因此不需要ORDER BY\crosstabview in the manual. 的详细信息该页面底部有更多代码示例。

Daniel Vérité(psql 功能的作者)对 dba.SE 的相关回答:

How do I generate a pivoted CROSS JOIN where the resulting table definition is unknown?

【讨论】:

+1,写得很好,感谢关注In practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered 我在使用 $$VALUES .. $$ 时遇到了一些问题。我改用了 'VALUES ('''':: ), .. ' @ErwinBrandstetter 这是您以非常有能力、深思熟虑且易于掌握的方式解释复杂事物的另一个例子。如果帮助堆栈溢出有诺贝尔奖,你应该得到它 @AndreSilva:同一集合的所有行必须具有相同的列类型。在 one 行中显式转换就足够了,其余的将保持一致。相关:***.com/a/30204394/939860; ***.com/a/12427434/939860 非常感谢您指出 $$ 引用可用于使用 dbeaver 等工具保持内部 sql“独立可执行”(仅通过选择内部 sql 文本);更不用说保留编辑器为 sql 提供的任何颜色编码。【参考方案2】:
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

FROM t
GROUP BY section

【讨论】:

有人能解释一下 tablefunc 模块中的交叉表函数在这个答案中增加了什么,它既能完成手头的工作,又在我看来更容易理解? @JohnBarça:像这样的简单案例可以通过 CASE 语句轻松解决。但是,如果属性和/或其他数据类型不仅仅是整数,这会很快变得笨拙。顺便说一句:此表单使用聚合函数sum(),最好使用min()max(),而不是ELSE,它也适用于text。但这与corosstab() 的效果略有不同,corosstab() 仅使用每个属性的“第一个”值。没关系,只要只有一个。最后,性能也很重要。 crosstab() 用 C 语言编写并针对任务进行了优化。 考虑添加解释而不是仅仅添加一段代码 在我的 postgresql 中由于某种原因未定义 tablefunc 和 crosstab,我也不允许定义它们。这个直观的解决方案对我有用,所以赞一个!【参考方案3】:

您可以使用additional module tablefunc 的crosstab() 功能 - 您必须为每个数据库安装一次一次。从 PostgreSQL 9.1 开始,您可以使用CREATE EXTENSION

CREATE EXTENSION tablefunc;

在你的情况下,我相信它看起来像这样:

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);

【讨论】:

如果您在交叉表查询中使用参数,则必须正确转义它。示例:(从上面)说你只想要活动的: SELECT ... FROM crosstab('select section::text, status, count::text from t where status=''active''', 2) AS 。 ..(注意双引号)。如果用户在运行时传递参数(例如作为函数参数),您可以说: SELECT ... FROM crosstab('select section::text, status, count::text from t where status='' ' || par_active || '''', 2) AS ...(此处为三引号!)。在 BIRT 中,这也适用于 ?占位符。【参考方案4】:

使用 JSON 聚合的解决方案:

CREATE TEMP TABLE t (
  section   text
, status    text
, ct        integer  -- don't use "count" as column name.
);

INSERT INTO t VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 


SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section
     )X

【讨论】:

谢谢,这帮助我解决了一个相关问题。【参考方案5】:

抱歉,这并不完整,因为我无法在此处对其进行测试,但它可能会让您朝着正确的方向前进。我正在翻译我使用的类似查询的东西:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
         mt1.count,
         mt2.count
order by mt.section asc;

我正在使用的代码是:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
         m1.highBid, 
         m2.lowAsk
order by diffPercent desc;

这将返回一个 typeID、最高出价和最低要价以及两者之间的差异(正数差异意味着可以以低于可以出售的价格购买某物)。

【讨论】:

您缺少一个 from 子句,否则这是正确的。解释计划在我的系统上大不相同——交叉表函数的成本为 22.5,而 LEFT JOIN 方法的成本大约是 91.38 的 4 倍。它还产生大约两倍的物理读取并执行散列连接 - 与其他连接类型相比,这可能非常昂贵。 谢谢耶利米,很高兴知道。我赞成另一个答案,但你的评论值得保留,所以我不会删除这个。【参考方案6】:

Crosstab 函数在 tablefunc 扩展名下可用。您必须为数据库创建一次此扩展。

创建扩展tablefunc;

您可以使用以下代码通过交叉表创建数据透视表:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>,status
    <br/>,count
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)

【讨论】:

这个答案没有添加任何已经存在的答案。

以上是关于PostgreSQL 交叉表查询的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 交叉表查询

PostgreSQL 交叉表查询

PostgreSQL 交叉表查询从帮助查询中提取列

PostgreSQL 交叉表转置行到列

如何在 Postgresql 交叉表中获取动态列数

使用 PostgreSQL 创建数据透视表