JSONB select result in one row - postgresql

I need your help on this. I'm trying to achieve a query for a jsonb column information I have in a table. My jsonb is an array of objects and in every object I have two key/value pairs. In this case, I have a key/value to exclude and only get the another one key without it value. So, I figure it out how to do it like:
jsonb : '[{"track":"value","location":"value"},{"extra":"value","location":"value"},...{"another":"value","location":"value"}]'
SELECT id, jsonb_object_keys((item::jsonb - 'location')::jsonb)
FROM mytable, jsonb_array_elements(theJsonB) with ordinality arr(item,position)
WHERE offer = '0001'
This query, get me the result like
id | jsonb_object_keys
-----------------------
1 | track
1 | extra
... |
1 | another
But I need to get the result in only one row for each id like
id | column1 | column2 | ... | column+
------------------------
1 | track | extra | ... | another
2 | track | extra | ... | another
3 | track | extra | ... | another
4 | track | extra | ... | another
How I could solve this? Thanks in advance, I'm a pretty newbie in SQL but I'm working hard ;-)

If you know the list of the resulting columns only at the runtime then you need some piece of dynamic sql code.
Here is a full dynamic solution which relies on the creation of a user-defined composite type and on the standard functions jsonb_populate_record and jsonb_object_agg :
First you dynamically create the list of keys as a new composite type :
CREATE OR REPLACE PROCEDURE key_list (NewJsonB jsonb) LANGUAGE plpgsql AS
$$
DECLARE key_list text ;
BEGIN
IF NewJsonB IS NULL
THEN
SELECT string_agg(DISTINCT k.object->>'key' || ' text', ',')
INTO key_list
FROM mytable
CROSS JOIN LATERAL jsonb_path_query(theJsonB, '$[*].keyvalue()[*] ? (#.key != "location")') AS k(object) ;
ELSE SELECT string_agg(DISTINCT k.key :: text || ' text', ',')
FROM (SELECT jsonb_object_keys(to_jsonb(a.*)) AS key FROM (SELECT(null :: key_list).*) AS a
UNION ALL
SELECT jsonb_path_query(NewJsonB, '$[*].keyvalue()[*] ? (#.key != "location")')->>'key'
) AS k
INTO key_list ;
END IF ;
EXECUTE 'DROP TYPE IF EXISTS key_list ' ;
EXECUTE 'CREATE TYPE key_list AS (' || COALESCE(key_list, '') || ')' ;
END ;
$$ ;
CALL key_list(NULL) ;
Then you call the procedure key_list() by trigger when the list of keys is supposed to be modified :
CREATE OR REPLACE FUNCTION mytable_insert_update()
RETURNS trigger LANGUAGE plpgsql VOLATILE AS
$$
BEGIN
IF NOT EXISTS (SELECT jsonb_object_keys(to_jsonb(a.*)) FROM (SELECT(null :: key_list).*) AS a)
THEN CALL key_list(NULL) ;
ELSIF EXISTS ( SELECT jsonb_path_query(NEW.theJsonB, '$[*].keyvalue()[*] ? (#.key != "location")')->>'key'
EXCEPT ALL
SELECT jsonb_object_keys(to_jsonb(a.*)) FROM (SELECT(null :: key_list).*) AS a
)
THEN CALL key_list(NEW.theJsonB) ;
END IF ;
RETURN NEW ;
END ;
$$ ;
CREATE OR REPLACE TRIGGER mytable_insert_update AFTER INSERT OR UPDATE OF theJsonB ON mytable
FOR EACH ROW EXECUTE FUNCTION mytable_insert_update() ;
CREATE OR REPLACE FUNCTION mytable_delete()
RETURNS trigger LANGUAGE plpgsql VOLATILE AS
$$
BEGIN
CALL key_list (NULL) ;
RETURN OLD ;
END ;
$$ ;
CREATE OR REPLACE TRIGGER mytable_delete AFTER DELETE ON mytable
FOR EACH ROW EXECUTE FUNCTION mytable_delete() ;
Finally, you should get the expected result with the following query :
SELECT (jsonb_populate_record(NULL :: key_list, jsonb_object_agg(lower(c.object->>'key'), c.object->'key'))).*
FROM mytable AS t
CROSS JOIN LATERAL jsonb_path_query(t.theJsonB, '$[*].keyvalue()[*] ? (#.key != "location")') AS c(object)
GROUP BY t
full test result in dbfiddle.

Related

How to return result of dynamic SELECT inside a function in PostgreSQL?

A very similar question here but not quite the same as this one.
I have a function that uses IF statements to determine what type of SELECT query to return.
How can I declare what a CREATE FUNCTION statment should return when I will never know the exact columns a SELECT query within it might return? That is, I can't setup a RETURNS TABLE declaration with a list of columns because I don't know which columns might come back. All I know is that I definitely will want a table of results to be returned.
Here is my function (uncompleted, pseudo):
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS -- what type? if TABLE how do I know what columns to specify
LANGUAGE plpgsql
AS
$$
DECLARE
_urltypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN QUERY
SELECT location, auxiliary, response FROM tablea -- unique columns from one table
END IF;
IF _urltypeid = 2
THEN
RETURN QUERY
SELECT ip, location, host, authority FROM tableb -- unique columns from another table
END IF;
END IF;
END;
$$;
I come from a MS SQL Server background where I don't have to specify in the CREATE FUNCTIONstatement what I'm returning, hence this is very confusing for me.
Not an answer, but an explanation of why answer from #JonathanJacobson will not work using a simple example:
\d animals
Table "public.animals"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | integer | | not null |
cond | character varying(200) | | not null |
animal | character varying(200) | | not null |
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS record
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT row(id, cond, animal) FROM animals where animal = a_type;
END;
$function$
select * from animal('cat');
ERROR: a column definition list is required for functions returning "record"
LINE 1: select * from animal('cat');
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS SETOF record
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT id, cond, animal FROM animals where animal = a_type;
END;
$function$
;
select * from animal('cat') as t(i integer, c varchar, a varchar);
i | c | a
---+------+-----
1 | fat | cat
2 | slim | cat
6 | big | cat
In order to use the output of a function returning a record or setof record you need to declare the output fields and types when you run the function.
You could use the record type. Not tested.
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS record
LANGUAGE plpgsql
AS
$$
DECLARE
_broadcasttypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN
(SELECT row(location, auxiliary, response) FROM tablea);
END IF;
IF _urltypeid = 2
THEN
RETURN
(SELECT row(ip, location, host, authority) FROM tableb);
END IF;
END IF;
END;
$$;
Other composite types, such as jsonb and hstore are also a solution.

How to execute an (expression involving columns of the same table) stored in another column of the table?

My table will look like this-
id | expression | unit_cost | demand |total_cost|
------ | -------------------------------------| ----------|--------|----------|
1 | (unit_cost*4)*demand | 5 |100 | |
2 | (unit_cost*(8/100)demand)*demand | 10 |50 | |
Now, I want to calculate total_cost based on the expression column using the other columns as specified in the expression. Changes in schema can be done, its just a sample to show what i actually want to do.
Note: expressions will be different for each row
You can use a function like this:
create or replace function eval(p_row the_table)
returns integer
as
$body$
declare
l_result integer;
l_sql text;
begin
l_sql := format('select %s from (values ($1, $2) ) as t(unit_cost, demand)',
p_row.expression);
execute l_sql
into l_result
using p_row.unit_cost, p_row.demand;
return l_result;
end;
$body$
language plpgsql;
(You need to replace the_table with the actual name of your table)
I decided to pass the complete row of the table as the parameter, so that you don't need to change the anything if you decide to use more columns from the table in your expressions.
The generated SQL looks like this (e.g. for the first row):
select (unit_cost*4)*demand
from ( values ($1, $2)) as t(unit_cost, demand);
The parameters in the values clause are then passed with the using ... part to make sure they are treated with the correct data types, which means it's executed as:
select (unit_cost*4)*demand
from ( values (5, 100)) as t(unit_cost, demand);
You can use it like this:
select t.id, t.unit_cost, t.demand, eval(t) as total_cost
from the_table t;
Note the table alias that is used to pass the row to the function.
If you know that the input values never change, you can also pass them directly:
create or replace function eval(p_expression text, p_demand int, p_unit_cost int)
returns integer
as
$body$
declare
l_result integer;
l_sql text;
begin
l_sql := format('select %s from (values ($1, $2) ) as t(unit_cost, demand)',
p_expression);
execute l_sql
into l_result
using p_unit_cost, p_demand;
return l_result;
end;
$body$
language plpgsql;
Then call it like this:
select id, unit_cost, demand, eval(t.expression, t.demand, t.unit_cost) as total_cost
from the_table t;
The first version (where you pass the complete row) has the advantage that you can never mix up the order of the parameters and accidentally pass the demand as the unit cost.
Online example

search for multiple values in comma separated column (postgresql)

I need to fetch all records where these (5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5) values exists in categories_id column.
The value that I need to search in categories_id can be multiple because it coming from the form.
+--------------------------------------+-------------+-------------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | name | alias | description | categories_id |
+--------------------------------------+-------------+-------------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 5565c08d-9f18-4b76-9cae-4a8261af48f5 | Honeycolony | honeycolony | null | ["5565bffd-7f64-494c-8950-4bef61af48f5"] |
+--------------------------------------+-------------+-------------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| c8f16660-32cf-11e6-b73c-1924f891ba4d | LOFT | loft | null | ["5565bffd-25bc-4b09-8a83-4bef61af48f5","5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"] |
+--------------------------------------+-------------+-------------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 5565c17f-80d8-4390-aadf-4a8061af48f5 | Fawn Shoppe | fawn-shoppe | null | ["5565bffd-25bc-4b09-8a83-4bef61af48f5","5565bffd-0744-4740-81f5-4bef61af48f5","5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"] |
+--------------------------------------+-------------+-------------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------+
I have this function which work as in_array function php.
CREATE OR REPLACE FUNCTION public.arraycontain(
x json,
y json)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
DECLARE a text;b text;
BEGIN
FOR a IN SELECT json_array_elements_text($1)
LOOP
FOR b IN SELECT json_array_elements_text($2)
LOOP
IF a = b THEN
RETURN TRUE;
END IF;
END LOOP;
END LOOP;
RETURN FALSE ;
END;
$BODY$;
ALTER FUNCTION public.arraycontain(json, json)
OWNER TO postgres;
But when I do this:
select * from "stores"
where arrayContain(stores.categories_id::JSON,'["5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"]')
it shows
ERROR: invalid input syntax for type json
DETAIL: The input string
ended unexpectedly.
CONTEXT: JSON data, line 1: SQL state: 22P02
here is the sqlfiddle (I couldn't update the arraycontain function in fiddle.)
My expected output from the fiddle is it should return last 3 rows that is Furbish Studio,Fawn Shoppe AND LOFT if search using this values ["5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"])
I am open for any recommendation.
I also tried this query below but it returns empty.
select id
from stores
where string_to_array(categories_id,',') && array['5565bffd-cd78-4e6f-ae13-4bef61af48f5','5565bffd-b1c8-4556-ae5d-4bef61af48f5'];
EDIT:
This code is actually a filter to filter data. So if I only filter using categories it didn't work but if there is a query before it it works
select * from "stores"
where name like '%ab%' and arrayContain(stores.categories_id::JSON,'["5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"]')
also the thing that amaze me is that the '%ab%' must contain more than two character if there's below <2 it will throw error. what could be wrong.
Click: demo:db<>fiddle
You can use the ?| operator, which takes a jsonb array (your column in that case) and checks a text array if any elements are included:
SELECT
*
FROM
mytable
WHERE categories_id ?| '{5565bffd-b1c8-4556-ae5d-4bef61af48f5,5565bffd-cd78-4e6f-ae13-4bef61af48f5}'
If your categories_id is not of type json (which is what the error message says) but a simple text array, you can compare two text arrays directly using the && operator:
Click: demo:db<>fiddle
SELECT
*
FROM
mytable
WHERE categories_id && '{5565bffd-b1c8-4556-ae5d-4bef61af48f5,5565bffd-cd78-4e6f-ae13-4bef61af48f5}'
Your code seems to work fine so perhaps sqlfiddle is the problem.
Try changing the separator in the schema building part to / (instead of ;) and make sure you have the correct version for Postgresql. json_array_elements_text is not supported in 9.3 (you can use json_array_elements instead in this case).
Also skip the " in the select statement.
Look here http://sqlfiddle.com/#!17/918b75/1
There might be an error in your data. Perhaps categories_id is an empty string somewhere.
Try this to see the offending data if any.
do $$
declare
r record;
b boolean;
begin
for r in (select * from stores) loop
b:= arrayContain(r.categories_id::JSON,'["5565bffd-b1c8-4556-ae5d-4bef61af48f5","5565bffd-cd78-4e6f-ae13-4bef61af48f5"]') ;
end loop;
exception
when others
then raise notice '%,%',r,r.categories_id;
return;
end;
$$
Best regards.
Bjarni

Create a function with multiple columns as arguments in Postgresql

I am trying to create a function that takes a table and a variable number of columns as arguments, and then returns a table without rows that have duplicates on all of those columns. I am trying to figure out how to have a variable number of columns as arguments, and I have gathered that I will probably need a VARIADIC argument, but I am not sure how to implement it. What I have so far:
CREATE FUNCTION remove_duplicates(orig_table, VARIADIC sel_columns column)
RETURNS table AS $$
SELECT * FROM
(SELECT *,
count(*) over (partition by sel_columns) AS count
FROM orig_table)
WHERE count = 1;
$$ LANGUAGE SQL;
As an example, if I had a table like this:
cola | colb | colc
-------------------
a | b | 1
a | b | 2
a | c | 3
a | d | 4
I would like to run SELECT * FROM remove_duplicates(mytable, cola, colb) and get this result:
cola | colb | colc
-------------------
a | c | 3
a | d | 4
Thank you for the help. I'm using postgresql 9.4.9
You'll cannot get what you want with a simple SQL function, you need the power of a procedural language. A possible solution is:
CREATE OR REPLACE FUNCTION remove_duplicates(orig_table anyelement, VARIADIC sel_columns text[])
RETURNS SETOF anyelement AS $$
DECLARE
orig_table_columns TEXT;
BEGIN
SELECT array_to_string(array_agg(quote_ident(column_name)),',') INTO orig_table_columns FROM information_schema.columns WHERE table_name = CAST(pg_typeof(orig_table) AS TEXT);
RETURN QUERY EXECUTE 'SELECT ' || orig_table_columns || ' FROM '
|| '(SELECT *, '
|| ' count(*) over (partition by ' || array_to_string(sel_columns, ',') || ') AS count '
|| 'FROM ' || pg_typeof(orig_table) || ') AS tmp '
|| ' WHERE count = 1 ';
END
$$ LANGUAGE PLPGSQL;
SELECT * FROM remove_duplicates(NULL::tests, 'cola', 'colb');
Don't forget to do your changes to avoid SQL Injection.
EDIT: For a very good explanation about functions with dynamic return types see Erwin's answer here.

PL/pgSQL: concatenating row values to a JSON-like string

I'm trying to retrieve player statistics for the last 20 weeks:
# select yw, money
from pref_money where id='OK122471020773'
order by yw desc limit 20;
yw | money
---------+-------
2010-52 | 1130
2010-51 | 3848
2010-50 | 4238
2010-49 | 2494
2010-48 | 936
2010-47 | 3453
2010-46 | 3923
2010-45 | 1110
2010-44 | 185
(9 rows)
But I would like to have the result as a string, where all values are concatenated by colons and semicolons like this:
"2010-44:185;2010-45:1110; .... ;2010-52:1130"
So I'm trying to create the following PL/pgSQL procedure:
create or replace function pref_money_stats(_id varchar)
returns varchar as $BODY$
begin
declare stats varchar;
for row in select yw, money from pref_money
where id=_id order by yw desc limit 20 loop
stats := row.id || ':' || row.money || ';' stats;
end loop;
return stats;
end;
$BODY$ language plpgsql;
But I get the syntax error:
ERROR: syntax error at or near "for"
LINE 7: for row in select yw, money from pref_money where id...
Using PostgreSQL 8.4.6 with CentOS 5.5 Linux.
UPDATE:
I'm trying to perform all this string concatenation with PL/pgSQL and not in PHP script, because I already have a main SQL select statement, which returns user information and that information is printed row by row as XML for my mobile app:
select u.id,
u.first_name,
u.female,
u.city,
u.avatar,
m.money,
u.login > u.logout as online
from pref_users u, pref_money m where
m.yw=to_char(current_timestamp, 'YYYY-IW')
and u.id=m.id
order by m.money desc
limit 20 offset ?
Here is the screenshot of the mobile app:
And here is an XML excerpt:
<?xml version="1.0"?>
<pref>
<user id="OK510352632290" name="ирина" money="2067" pos="1" medals="1" female="1" avatar="http://i221.odnoklassniki.ru/getImage?photoId=259607761026&photoType=0" city="староконстантинов" />
<user id="OK19895063121" name="Александр" money="1912" pos="2" online="1" avatar="http://i69.odnoklassniki.ru/getImage?photoId=244173589553&photoType=0" city="Сызрань" />
<user id="OK501875102516" name="Исмаил" money="1608" pos="3" online="1" avatar="http://i102.odnoklassniki.ru/res/stub_128x96.gif" city="Москва" />
.....
</pref>
But my problem is that I have 3 other tables, from which I need that statistics for the last 20 weeks. So I'm hoping to create 3 procedures returning varchars as in my original post and integrate them in this SQL select statement. So that I can add further attributes to the XML data:
<user id="OK12345" .... money_stats="2010-44:185;2010-45:1110; .... ;2010-52:1130" ..... />
Thank you!
Alex
Aggregate functions are good for concatenating values:
create or replace function test
(text, text, text)
returns text as
$$
select $1 || ':' || $2 || ';' || $3
$$
language sql;
drop function test(text, text);
drop aggregate test(text, text) cascade;
create aggregate test(text, text)
(
sfunc = test,
stype = text,
initcond = ''
);
test=# select test(a::text, b::text) from (select generate_series(1,3) as a, generate_series(4,5) a
s b) t;
:1;4:2;5:3;4:1;5:2;4:3;5
(I'll leave it to you to deal with the leading colin :-)
You probably have already found the answer to your problem. Even so, the problem was indeed syntax.
The problem was that the declare statement was misplaced: it should appear before the begin (docs):
create or replace function pref_money_stats(_id varchar)
returns varchar as $BODY$
declare stats varchar;
begin
...
Another detail to take notice of is that you need to declare row as a record:
declare
stats varchar;
row record;
Then this statement will run properly:
for row in select yw, money from pref_money where id=_id order by yw desc limit 20 loop
This is not exactly JSON but pretty close:
SELECT ARRAY
(
SELECT ROW(yw, money)
FROM pref_money
WHERE id = 'OK122471020773'
ORDER BY
yw DESC
LIMIT 20
)::TEXT
This will output this string:
{"(2010-44:185)","(2010-45:1110)",…,"(2010-52:1130)"}
which can later be cast back into the appropriate types.