I am working in Rails and Postgres.
I have a table Problems, which has a few columns. I have another table ExtraInfos, which references Problems and has three columns: problem_id, info_type, info_value.
For example:
Problems:
id | problem_type | problem_group |
---|---|---|
0 | type_x | grp_a |
1 | type_y | grp_b |
2 | type_z | grp_c |
ExtraInfos:
id | problem_id | info_type:String | info_value |
---|---|---|---|
0 | 0 | info_1 | v1 |
1 | 0 | info_2 | v2 |
2 | 0 | info_3 | v3 |
3 | 1 | info_1 | v4 |
4 | 1 | info_3 | v5 |
As you can see, each problem has a variable number of extra information.
What is the best way to join both tables to create something that looks like:
id | problem_type | problem_group | info_1 | info_2 | info_3 |
---|---|---|---|---|---|
0 | type_x | grp_a | v1 | v2 | v3 |
1 | type_y | grp_b | v4 | v5 | |
2 | type_z | grp_c |
I used the ruby pivot_table gem, and I did manage to create the view that I wanted, by
@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
and then iterating over it by
...
<% @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 %>
...
but this is very clunky and doesn’t leave me with good ways to, for instance, sort by these extra columns. As far as I know, the tables are simply a grid, an object, and can’t LEFT JOIN
with my Problems.all
table, which would be the ideal solution.
I have tried looking up various pure SQL methods, but all seem to start with the assumption that these extra columns will be hard coded in, which is what I am trying to avoid. I came across crosstab, but I haven’t managed to get it working as it should.
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)
This gives me the result {"problem_id"=>44, "info_type"=>"6", "info_value"=>"15"} {"problem_id"=>45, "info_type"=>"6", "info_value"=>"15"}
which is clearly not correct.
Another method seems to be creating a separate reference table containing a list of all possible infoTypes, which will then be referenced by the ExtraInfos table, making it easier to join the tables. However, I don’t want the infoTypes coded in at all. I want the user to be able to give me any type and value strings, and my tables should be able to deal with this.
What is the best solution for accomplishing this?
2
Answers
In postgres, pivot table or crosstab are not relevant when the list of columns may vary in time, ie the list of values in column
info_type
may increase or decrease.There is an other solution which consists in creating a
composite type
dynamically and then using the standard functionsjsonb_build_agg
andjsonb_populate_record
:Creating the composite type
column_list
dynamically :Then setting up the composite type
column_list
for the first time :But this composite type must be updated after every change of column ExtraInfos. This can be achieved with a trigger function :
The final query is :
which gives the result :
see test result in dbfiddle
ActiveRecord is built on top of the AST query assembler
Arel
.You can use this assembler to build dynamic queries as needed basically if you can hand type it as a SQL query Arel can build it.
In this case the following will build your desired crosstab query based on the table structure provided in the post.
Using this
q.to_sql
will produce:And results in
We can join this to the problems table as
This will return
Problem
objects where theinfo_type
columns are virtual attributes. e.g.out.first.info_1 #=> 'v1'
Note: Personally I would break the parts down in a class to make the assembly clearer but the above will produce the desired outcome