7

I have a data model like the following which is simplified to show you only this problem (SQL Fiddle Link at the bottom):

enter image description here

A person is represented in the database as a meta table row with a name and with multiple attributes which are stored in the data table as key-value pair (key and value are in separate columns).

Expected Result

Now I would like to retrieve all users with all their attributes. The attributes should be returned as json object in a separate column. For example:

name, data
Florian, { "age":23, "color":"blue" }
Markus, { "age":24, "color":"green" }

My Approach

Now my problem is, that I couldn't find a way to create a key-value pair in postgres. I tried following:

SELECT
  name,
  array_to_json(array_agg(row(d.key, d.value))) AS data
FROM meta AS m
JOIN (
  SELECT d.fk_id, d.key, d.value AS value FROM data AS d
  ) AS d
ON d.fk_id = m.id
GROUP BY m.name;

But it returns this as data column:

[{"f1":"age","f2":24},{"f1":"color","f2":"blue"}]

Other Solutions

I know there is the function crosstab which enables me to turn the data table into a key as column and value as row table. But this is not dynamic. And I don't know how many attributes a person has in the data table. So this is not an option.

I could also create a json like string with the two row values and aggregate them. But maybe there is a nicer solution.

And no, it is not possible to change the data-model because the real data model is already in use of multiple parties.

SQLFiddle

Check and test out the fiddle i've created for this question: http://sqlfiddle.com/#!15/bd579/14

3 Answers 3

15

Use the aggregate function json_object_agg(key, value):

select 
    name, 
    json_object_agg(key, value) as data
from data
join meta on fk_id = id
group by 1;

Db<>Fiddle.

The function was introduced in Postgres 9.4.

Sign up to request clarification or add additional context in comments.

Comments

0

I found a way to return crosstab data with dynamic columns. Maybe rewriting this will be better to suit your needs:

CREATE OR REPLACE FUNCTION report.usp_pivot_query_amount_generate(
    i_group_id INT[],
    i_start_date TIMESTAMPTZ,
    i_end_date TIMESTAMPTZ,
    i_interval INT
    ) RETURNS TABLE (
    tab TEXT
    ) AS $ab$
DECLARE
    _key_id TEXT;
    _text_op TEXT = '';
    _ret TEXT;
BEGIN
    -- SELECT DISTNICT for query results
    FOR _key_id IN
    SELECT DISTINCT at_name
      FROM report.company_data_date cd 
      JOIN report.company_data_amount cda ON cd.id = cda.company_data_date_id 
      JOIN report.amount_types at ON cda.amount_type_id  = at.id 
     WHERE date_start BETWEEN i_start_date AND i_end_date
       AND group_id = ANY (i_group_id)
       AND interval_type_id = i_interval
    LOOP
    -- build function_call with datatype of column
        IF char_length(_text_op) > 1 THEN
            _text_op := _text_op || ', ' || _key_id || ' NUMERIC(20,2)';
        ELSE
            _text_op := _text_op || _key_id || ' NUMERIC(20,2)';
        END IF;
    END LOOP;
    -- build query with parameter filters
    _ret = '
        SELECT * FROM crosstab(''SELECT date_start, at.at_name,  cda.amount ct 
          FROM report.company_data_date cd 
          JOIN report.company_data_amount cda ON cd.id = cda.company_data_date_id 
          JOIN report.amount_types at ON cda.amount_type_id  = at.id 
         WHERE date_start between $$' || i_start_date::TEXT || '$$ AND $$' || i_end_date::TEXT || '$$ 
           AND interval_type_id = ' || i_interval::TEXT || ' 
           AND group_id = ANY (ARRAY[' || array_to_string(i_group_id, ',') || '])
         ORDER BY date_start'') 
            AS ct (date_start timestamptz, ' || _text_op || ')';
    RETURN QUERY
    SELECT _ret;
END;
$ab$ LANGUAGE 'plpgsql';

Call the function to get the string, then execute the string. I think I tried executing it in the function, but it didn't work well.

Comments

0

I ran into the same problem when I needed to update some JSON and remove a few elements in my database. This query below worked well enough for me, as it preserves the string quotes but does not add them to numbers.

select
  '{' || substr(x.arr, 3, length(x.arr) - 4) || '}'
from
(
  select
     replace(replace(cast(array_agg(xx) as varchar), '\"', '"'), '","', ', ') as arr
  from
  (
  select
    elem.key,
    elem.value,
    '"' || elem.key || '":' || elem.value as xx
  from
    quote q
      cross join
    json_each(q.detail::json -> 'bQuoteDetail'-> 'quoteHC'->0)  as elem  
  where
    elem.key != 'allRiskItems'  
  ) f 
) x

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.