Subquery as a JSON field - postgresql

Using the hypothetical schema:
CREATE TABLE obj (id INT, name VARCHAR);
CREATE TABLE objprop (obj_id INT, key VARCHAR, value VARCHAR);
INSERT INTO obj VALUES
(1, 'Object 1'),
(2, 'Object 2'),
(3, 'Object 3');
INSERT INTO objprop VALUES
(1, 'created', '2020-02-16'),
(1, 'updated', '2020-02-28'),
(2, 'created', '2020-02-01');
Could I obtain a list of objects (one per row), and a JSON field that represents object's properties?
I know I can use the ARRAY() function with a subquery to retrieve an array of values, for example:
SELECT id, name, ARRAY(SELECT value FROM objprop where obj_id=id) values FROM obj;
+----+----------+------------------------------+
| id | name | values |
+----+----------+------------------------------+
| 1 | Object 1 | {'2020-02-16', '2020-02-28'} |
| 2 | Object 2 | {'2020-02-01'} |
| 3 | Object 3 | {} |
+----+----------+------------------------------+
But could I make a query that instead of an ARRAY, it would return me a JSON column with the subquery in it? My goal is to obtain for example:
+---+----------+----------------------------------------------------------------------------------------+
| 1 | Object 1 | [{"key": "created", "value": "2020-02-16"}, {"key": "updated", "value": "2020-02-28"}] |
| 2 | Object 2 | [{"key": "created", "value": "2020-02-01"}] |
| 3 | Object 3 | [] |
+---+----------+----------------------------------------------------------------------------------------+

SELECT
id,
name,
COALESCE((
SELECT json_agg(json_build_object('key', key, 'value', value))
FROM objprop where obj_id=id
), '[]'::json) vals
FROM
obj;

Related

How do I select a postgres Many-to-One relationship as a single row? [duplicate]

This question already has answers here:
PostgreSQL Crosstab Query
(7 answers)
Closed 3 years ago.
I have a many-to-one relationship between Animals and their attributes. Because different Animals have different attributes, I want to be able to select all animals with their attribute name as a column header and NULL values where that animal does not have that attribute.
Like so...
TABLE_ANIMALS
ID | ANIMAL | DATE | MORE COLS....
1 | CAT | 2012-01-10 | ....
2 | DOG | 2012-01-10 | ....
3 | FROG | 2012-01-10 | ....
...
TABLE_ATTRIBUTES
ID | ANIMAL_ID | ATTRIBUE_NAME | ATTRIBUTE_VALUE
1 | 1 | noise | meow
2 | 1 | legs | 4
3 | 1 | has_fur | TRUE
4 | 2 | noise | woof
5 | 2 | legs | 4
6 | 3 | noise | croak
7 | 3 | legs | 2
8 | 3 | has_fur | FALSE
...
QUERY RESULT
ID | ANIMAL | NOISE | LEGS | HAS_FUR
1 | CAT | meow | 4 | TRUE
2 | DOG | woof | 4 | NULL
3 | FROG | croak | 2 | FALSE
How would I do this? To reiterate, it's important that all the columns are there even if one Animal doesn't have that attribute, such as "DOG" and "HAS_FUR" in this example. If it doesn't have the attribute, it should just be null.
How about a simple join, aggregation and group by?
create table table_animals(id int, animal varchar(10), date date);
create table table_attributes(id varchar(10), animal_id int, attribute_name varchar(10), attribute_value varchar(10));
insert into table_animals values (1, 'CAT', '2012-01-10'),
(2, 'DOG', '2012-01-10'),
(3, 'FROG', '2012-01-10');
insert into table_attributes values (1, 1, 'noise', 'meow'),
(2, 1, 'legs', 4),
(3, 1, 'has_fur', TRUE),
(4, 2, 'noise', 'woof'),
(5, 2, 'legs', 4),
(6, 3, 'noise', 'croak'),
(7, 3, 'legs', 2),
(8, 3, 'has_fur', FALSE);
select ta.animal,
max(attribute_value) filter (where attribute_name = 'noise') as noise,
max(attribute_value) filter (where attribute_name = 'legs') as legs,
max(attribute_value) filter (where attribute_name = 'has_fur') as has_fur
from table_animals ta
left join table_attributes tat on tat.animal_id = ta.id
group by ta.animal
Here's a rextester sample
Additionally you can change the aggregation to MAX CASE WHEN... but MAX FILTER WHERE has better performance.

Match JSONB row where at least one of object's values is not null

In our database we have a data set sorta like the following:
+----+-------------------------------+
| id | stuff |
+----+-------------------------------+
| 1 | {} |
+----+-------------------------------+
| 2 | {"a": "something", "b": null} |
+----+-------------------------------+
| 3 | {"c": null, "d": null} |
+----+-------------------------------+
I would like to match only, in this case, the one with id = 2, reason being that at least one of the values in the object is not null.
How can this be done with PostgreSQL?
I know one can do something like WHERE stuff != '{}' but that of course only checks for an empty object
Or something like WHERE (stuff->>'a') IS NOT NULL, but the thing is the list of keys in the objects are not hardcoded, could be anything
Use the function jsonb_each_text() or json_each_text(), example:
with my_table(id, jdata) as (
values
(1, '{}'::jsonb),
(2, '{"a": "something", "b": null}'),
(3, '{"c": null, "d": null}')
)
select distinct t.*
from my_table t
cross join jsonb_each_text(jdata)
where value is not null;
id | jdata
----+-------------------------------
2 | {"a": "something", "b": null}
(1 row)
This query (proposed by Abelisto, see the comments) should be more performant on a larger dataset:
select t.*
from my_table t
where exists (
select 1
from jsonb_each_text(jdata)
where value is not null);

T-SQL | 3 Tables, Optional Joins, Confused

I have an orders table which lists stores what parts where deleted, removed and installed on each job.
--Deleted Items on a order.
DECLARE #Deleted TABLE (ID int, Item int);
INSERT INTO #Deleted (ID, Item)
VALUES (1, 12345), (2, 12345);
-- Removed Items on a order
DECLARE #Removed TABLE (ID int, Item int);
INSERT INTO #Removed (ID, Item)
VALUES(3, 12345), (4, 67891);
--Installed items on a order
DECLARE #Installed TABLE (ID int, Item int);
INSERT INTO #Installed(ID, Item)
VALUES(1, 12345), (2, 67891), (3, 12345), (4, 12345), (4, 67891);
I need the following result set:
--Required Result Set
/*
ID | DEL / REM | Installed | Match |
1 | 12345 | 12345 | 1 |
2 | 12345 | 67891 | 0 |
3 | 12345 | 12345 | 1 |
4 | 67891 | 67891 | 1 |
4 | NULL | 12345 | 0 |
*/
The main problem I have with all this is order 4. There is a match on one of the parts installed but not the second. So I need to say where ID = ID AND IF POSSIBLE Item = Item OR just Item if it doesn't match.
I know that the maximum number of alternate installed items are two, so for each order, there will be a maximum of 2 different Item number installed (currently).
I know there also may be multiple deleted or removed items (not depicted above).
Any help would be greatly appreciated.

PostgreSQL JSONB query types of each keys

I have certain table:
CREATE TABLE x(
id BIGSERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO x(data)
VALUES( '{"a":"test", "b":123, "c":null, "d":true}' ),
( '{"a":"test", "b":123, "c":null, "d":"yay", "e":"foo", "f":[1,2,3]}' );
How to query types of each key in that table, so it would give an output something like this:
a | string:2
b | number:2
c | null:2
d | boolean:1 string:1
e | string:1
f | jsonb:1 -- or anything
I only know the way to get the keys and count, but don't know how to get the type of each key:
SELECT jsonb_object_keys(data), COUNT(id) FROM x GROUP BY 1 ORDER BY 1
that would give something like:
a | 2
b | 2
c | 2
d | 2
e | 1
f | 1
EDIT:
As pozs points out, there are two typeof functions: one for JSON and one for SQL. This query is the one you're looking for:
SELECT
json_data.key,
jsonb_typeof(json_data.value),
count(*)
FROM x, jsonb_each(x.data) AS json_data
group by key, jsonb_typeof
order by key, jsonb_typeof;
Old Answer: (Hey, it works...)
This query will return the type of the keys:
SELECT
json_data.key,
pg_typeof(json_data.value),
json_data.value
FROM x, jsonb_each(x.data) AS json_data;
... unfortunately, you'll notice that Postgres doesn't differentiate between the different JSON types. it regards it all as jsonb, so the results are:
key1 | value1 | value
------+--------+-----------
a | jsonb | "test"
b | jsonb | 123
c | jsonb | null
d | jsonb | true
a | jsonb | "test"
b | jsonb | 123
c | jsonb | null
d | jsonb | "yay"
e | jsonb | "foo"
f | jsonb | [1, 2, 3]
(10 rows)
However, there aren't that many JSON primitive types, and the output seems to be unambiguous. So this query will do what you're wanting:
with jsontypes as (
SELECT
json_data.key AS key1,
CASE WHEN left(json_data.value::text,1) = '"' THEN 'String'
WHEN json_data.value::text ~ '^-?\d' THEN
CASE WHEN json_data.value::text ~ '\.' THEN 'Number'
ELSE 'Integer'
END
WHEN left(json_data.value::text,1) = '[' THEN 'Array'
WHEN left(json_data.value::text,1) = '{' THEN 'Object'
WHEN json_data.value::text in ('true', 'false') THEN 'Boolean'
WHEN json_data.value::text = 'null' THEN 'Null'
ELSE 'Beats Me'
END as jsontype
FROM x, jsonb_each(x.data) AS json_data -- Note that it won't work if we use jsonb_each_text here because the strings won't have quotes around them, etc.
)
select *, count(*) from jsontypes
group by key1, jsontype
order by key1, jsontype;
Output:
key1 | jsontype | count
------+----------+-------
a | String | 2
b | Integer | 2
c | Null | 2
d | Boolean | 1
d | String | 1
e | String | 1
f | Array | 1
(7 rows)
You can improve your last query with jsonb_typeof
with jsontypes as (
SELECT
json_data.key AS key1,
jsonb_typeof(json_data.value) as jsontype
FROM x, jsonb_each(x.data) AS json_data
)
select *, count(*)
from jsontypes
group by 1, 2
order by 1, 2;

How to create a pivot table from hstore data?

Imagining I have a table cars with a field data inside:
CARS
name | data
car 1 | { "doors" => "5", "engine" => "1.1" }
car 2 | { "doors" => "3", "engine" => "1.1", "air_conditioning" => "true" }
car 3 | { "doors" => "5", "engine" => "1.4" }
Assuming data keys are dynamic (more can be added), how can I create a pivot table from that data like this:
CROSSTAB
name | doors | engine | air_conditioning
car 1 | 5 | 1.1 |
car 2 | 3 | 1.1 | "true"
car 3 | 5 | 1.4 |
Here's how to get the result you asked for:
CREATE TABLE hstore_test (id bigserial primary key, title text, doors integer, engine text, air_conditioning boolean)
INSERT INTO hstore_test (title, doors, engine, air_conditioning)
VALUES ('Car1', 2, '1.1', false), ('Car2', 4, '1.2', true), ('Car3', 3, '1.3', false), ('Car4', 5, '1.4', null);
DROP TABLE IF EXISTS hstore_persist;
CREATE TABLE hstore_persist AS
SELECT hstore(t) car_data FROM hstore_test AS t;
SELECT car_data->'title' "name", car_data->'doors' doors, car_data->'engine' engine, car_data->'air_conditioning' air_conditioning
FROM hstore_persist
This will result in the table
name | doors | engine | air_conditioning
Car1 | 2 | 1.1 | f
Car2 | 4 | 1.2 | t
Car3 | 3 | 1.3 | f
Car4 | 5 | 1.4 |
There is nothing "crosstab" about it, though. This is just using the accessor methods of an hstore to display the data in the way you show in the example.