postgresql 栏杆、立柱:如何在不对列进行硬编码的情况下创建透视表并将其左联接到另一个表

rqqzpn5f  于 2022-12-22  发布在  PostgreSQL
关注(0)|答案(2)|浏览(113)

我在Rails和Postgres中工作,有一个表Problems,它有几列,还有一个表ExtraInfos,它引用Problems,有三列:问题ID、信息类型、信息值。
例如:

问题:

| 身份证|问题类型|问题组|
| - ------| - ------| - ------|
| 无|类型_x|组_a|
| 1个|类型_y|组_b|
| 第二章|类型_z|组_c|

额外信息:

| 身份证|问题ID|信息类型:字符串|信息值|
| - ------| - ------| - ------| - ------|
| 无|无|信息_1|第1版|
| 1个|无|信息_2|第2版|
| 第二章|无|信息_3|第3版|
| 三个|1个|信息_1|第四版|
| 四个|1个|信息_3|第五版|
正如你所看到的,每个问题都有不同数量的额外信息。
连接这两个表以创建如下内容的最佳方法是什么:
| 身份证|问题类型|问题组|信息_1|信息_2|信息_3|
| - ------| - ------| - ------| - ------| - ------| - ------|
| 无|类型_x|组_a|第1版|第2版|第3版|
| 1个|类型_y|组_b|第四版||第五版|
| 第二章|类型_z|组_c||||
我使用了ruby pivot_table gem,并且成功地创建了所需的 * view *,方法是

@table = PivotTable::Grid.new do |g|
  g.source_data  = ExtraInfos.all.includes(:problem))
  g.column_name  = :info_type
  g.row_name     = :problem
  g.field_name   = :info_value
end
@table.build

然后通过以下方式迭代

...
<% @table.columns.each do |col| %>
  <th><%= col.header %></th>
<% end %>
...
<% if @table.row_headers.include? problem %>
  <% table.rows[table.row_headers.index(problem)].data.each do |cell| %>
    <td><%= cell %></td>
  <% end %>
<% end %>
...

但这非常笨拙,而且没有给我留下好的方法,例如,按这些额外的列排序。据我所知,表只是一个网格,一个对象,不能用我的Problems.all表来LEFT JOIN,这将是理想的解决方案。
我尝试过查找各种纯SQL方法,但所有方法似乎都是从假设这些额外的列将被硬编码开始的,这是我试图避免的。我遇到了crosstab,但我还没有设法让它正常工作。

sql = "CREATE EXTENSION IF NOT EXISTS tablefunc;
    SELECT * FROM crosstab(
      'SELECT problem_id, info_type, info_value
      FROM pre_maslas
      ORDER BY 1,2'
    ) AS ct(problem_id bigint, info_type varchar(255), info_value varchar(255))"

@try = ActiveRecord::Base.connection.execute(sql)

这样得到的结果{"problem_id"=>44, "info_type"=>"6", "info_value"=>"15"} {"problem_id"=>45, "info_type"=>"6", "info_value"=>"15"}显然是不正确的。
另一种方法似乎是创建一个单独的引用表,其中包含所有可能的infoType列表,然后ExtraInfos表将引用该表,从而使表的连接更加容易。然而,我根本不希望编写infoType。我希望用户能够给我 * any * 类型和值字符串,我的表应该能够处理这个问题。
实现这一目标的最佳解决方案是什么?

mftmpeh8

mftmpeh81#

ActiveRecord构建在AST查询汇编器Arel之上。
基本上,如果您可以手动键入这个汇编器,就可以使用它来构建所需的动态查询,因为SQL查询可以由Arel构建。
在这种情况下,下面将根据文章中提供的表结构构建您想要的交叉表查询。

# Get all distinct info_types to build columns
cols = ExtraInfo.distinct.pluck(:info_type).sort
# extra_info Arel::Table
extra_infos_tbl = ExtraInfo.arel_table
# Arel::Table to use for querying 
tbl = Arel::Table.new('ct')

# SQL data type for the extra_infos.info_type column 
info_type_sql_type = ExtraInfo.columns.find {|c| c.name == 'info_type' }&.sql_type

# Part 1 of crosstab 
qry_txt = extra_infos_tbl.project( 
  extra_infos_tbl[:problem_id],
  extra_infos_tbl[:info_type],
  extra_infos_tbl[:info_value]
) 
# Part 2 of the crosstab  
cats =  extra_infos_tbl.project(extra_infos_tbl[:info_type]).distinct

# construct the ct portion of the crosstab query
ct = Arel::Nodes::NamedFunction.new('ct',[
  Arel::Nodes::TableAlias.new(Arel.sql('problem_id'), Arel.sql('bigint')),
  *cols.map {|name|  Arel::Nodes::TableAlias.new(Arel.sql(name), Arel.sql(info_type_sql_type))}
])

# build the crosstab(...) AS ct(...) statement
crosstab = Arel::Nodes::As.new(
  Arel::Nodes::NamedFunction.new('crosstab', [Arel.sql("'#{qry_txt.to_sql}'"),
    Arel.sql("'#{cats.to_sql}'")]),
  ct
)

# final query construction
q = tbl.project(tbl[Arel.star]).from(crosstab)

使用此q.to_sql将生成:

SELECT 
  ct.* 
FROM 
  crosstab('SELECT 
              extra_infos.problem_id, 
              extra_infos.info_type, 
              extra_infos.info_value 
            FROM 
              extra_infos', 
           'SELECT DISTINCT 
              extra_infos.info_type 
            FROM 
              extra_infos') AS ct(problem_id bigint, 
                                  info_1 varchar(255), 
                                  info_2 varchar(255), 
                                  info_3 varchar(255))

结果是
| 问题ID|信息_1|信息_2|信息_3|
| - ------|- ------|- ------|- ------|
| 无|第1版|第2版|第3版|
| 1个|第四版||第五版|
我们可以将其连接到问题表,如下所示

sub = Arel::Table.new('subq')
sub_q = Arel::Nodes::As.new(q,Arel.sql(sub.name)) 

out = Problem
  .joins(Arel::Nodes::InnerJoin.new(sub_q,            
            Arel::Nodes::On.new(Problem.arel_table[:id].eq(sub[:problem_id]))
  )).select(
     Problem.arel_table[Arel.star],
     *cols.map {|c| sub[c.intern]}
  )

这将返回Problem对象,其中info_type列是虚拟属性。例如out.first.info_1 #=> 'v1'

**注:**我个人会将部件分解为一个类,以使装配更清晰,但上述内容将产生预期结果

5lhxktic

5lhxktic2#

在postgres中,当列列表随时间变化时,数据透视表或交叉表是不相关的,即列info_type中的值列表可能增加或减少。
还有另一种解决方案,包括动态创建composite type,然后使用标准函数jsonb_build_aggjsonb_populate_record
动态创建复合类型column_list

CREATE OR REPLACE PROCEDURE column_list() LANGUAGE plpgsql AS $$
DECLARE
  clist text ;
BEGIN
  SELECT string_agg(DISTINCT info_type || ' text', ',')
    INTO clist
    FROM ExtraInfos ;
 
  EXECUTE 'DROP TYPE IF EXISTS column_list' ;
  EXECUTE 'CREATE TYPE column_list AS (' || clist || ')' ;
END ; $$ ;

然后第一次设置复合类型column_list

CALL column_list() ;

但是这个复合类型必须在ExtraInfos列的每一次改变之后更新,这可以通过一个触发器函数来实现:

CREATE OR REPLACE FUNCTION After_Insert_Update_Delete_ExtraInfos () RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  CALL column_list() ;
  RETURN NULL ;
END ; $$ ;

CREATE OR REPLACE TRIGGER After_Insert_Update_Delete_ExtraInfos AFTER INSERT OR UPDATE OF info_type OR DELETE ON ExtraInfos
FOR EACH STATEMENT EXECUTE FUNCTION After_Insert_Update_Delete_ExtraInfos () ;

最终查询为:

SELECT p.id, p. problem_type, p.problem_group, (jsonb_populate_record(NULL :: column_list, jsonb_object_agg(info_type, info_value))).*
  FROM Problems AS p
 INNER JOIN ExtraInfos AS ei
    ON ei.problem_id = p.id
 GROUP BY p.id, p. problem_type, p.problem_group

其给出结果:
| 身份证|问题类型|问题组|信息_1|信息_2|信息_3|
| - ------|- ------|- ------|- ------|- ------|- ------|
| 无|类型_x|组_a|第1版|第2版|第3版|
| 1个|类型_y|组_B|第四版|零|第五版|
参见dbfiddle中的测试结果

相关问题