I am considering switching to PostgreSQL, because of the JSON support. However, I am wondering, if the following would be possible with a single query:
Let's say there are two tables:
Table 1) organisations:
ID (INT) | members (JSONB) |
------------+---------------------------------------------------------|
1 | [{ id: 23, role: "admin" }, { id: 24, role: "default" }]|
2 | [{ id: 23, role: "user" }]
Table 2) users:
ID (INT) | name TEXT | email TEXT |
------------+-----------+---------------|
23 | Max | max#gmail.com |
24 | Joe | joe#gmail.com |
Now I want to get a result like this (all i have is the ID of the organisation [1]):
ID (INT) | members (JSONB) |
------------+--------------------------------------------------------|
1 | [{ id: 23, name: "Max", email: "max#gmail.com", role:
"admin" },
{ id: 24, name: "Joe", email: "joe#gmail.com ", role:
"default" }]
(1 row)
I know this is not what JSONB is intended for and that there is a better solution for storing this data in SQL, but I am just curious if it would be possible.
Thanks!
Yes it is possible to meet this requirement with Postgres. Here is a solution for 9.6 or higher.
SELECT o.id, JSON_AGG(
JSON_BUILD_OBJECT(
'id' , u.id,
'name' , u.name,
'email' , u.email,
'role' , e.usr->'role'
)
)
FROM organisations o
CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(o.data) AS e(usr)
INNER JOIN users u ON (e.usr->'id')::text::int = u.id
GROUP BY o.id
See this db fiddle.
Explanation :
the JSONB_ARRAY_ELEMENTS function splits the organisation json array into rows (one per user) ; it is usually used in combination with JOIN LATERAL
to join the users table, we access the content of the id field using the -> operator
for each user, the JSONB_BUILD_OBJECT is used to create a new object, by passing a list of values/keys pairs ; most values comes from the users table, excepted the role, that is taken from the organisation json element
the query aggregates by organisation id, using JSONB_AGG to generate a json array by combining above objects
For more information, you may also have a look at Postgres JSON Functions documentation.
There might be more ways to do that. One way would use jsonb_to_recordset() to transform the JSON into a record set you can join. Then create a JSON from the result using jsonb_build_object() for the individual members and jsonb_agg() to aggregate them in a JSON array.
SELECT jsonb_agg(jsonb_build_object('id', "u"."id",
'name', "u"."name",
'email', "u"."email",
'role', "m"."role"))
FROM "organisations" "o"
CROSS JOIN LATERAL jsonb_to_recordset(o."members") "m" ("id" integer,
"role" text)
INNER JOIN "users" "u"
ON "u"."id" = "m"."id";
db<>fiddle
What functions are available in detail depends on the version. But since you said you consider switching, assuming a more recent version should be fair.
Related
I have an Sqlite table triples that contains triple information in { id, rel, tgt } format [1]. I would like to create a view that exposes this triple-format data to "object format", which is more easily consumed by applications reading from this database. In theory sqlite's JSON1 extension would allow me to construct such objects, but I'm struggling.
My current query
select distinct json_object(
'id', id,
rel, json_group_array(distinct tgt)
) as entity from content
group by src, rel, tgt
order by src, rel, tgt
does not work correctly. It produces objects like
{ id: 'a', 'is': ['b'] }
{ id: 'a', 'is': ['c'] }
Rather than
{ id: 'a', 'is': ['b', 'c'] }
It also produces duplicate keys like
{ id: 'a', id: ['a'] }
Edit
This is closer, but does not handle IDs correctly. It constructs an array, not a string
create view if not exists entity as
select distinct json_group_object(
rel, json_array(distinct tgt)
) as entity from content
group by src
I think iif might help
Question;
Can you help me adjust my query to produce correct output (see below)? Please comment if anything needs disambiguation or clarification
Desired Output
Input:
Triple Format:
id | rel | tgt
-----------------------
Bob | is | Bob
Bob | is | Person
Bob | age | 20
Bob | likes | cake
Bob | likes | chocolate
Alice | id | Alice
Alice | is | Person
Alice | hates | chocolate
Output:
Object Format [2]:
{
id: Bob,
is: [ Person ],
age: [ 20 ],
likes: [ cake, chocolate ]
}
{
id: Alice,
is: [ Person ],
hates: [ chocolate ]
}
Details
[1] This dataset has unpredictable structure; I can assume no prior knowledge of what 'rel' keys exist beyond id. A triple <src> id <src> will exist for every src parameter.
[2] The objects should have the following format. id must not be overwritten.
{
id: <id>
<distinct rel>: [
< tgt >
]
}
Relevant Information
https://www.sqlite.org/json1.html
CREATE TABLE content (
id VARCHAR(32),
rel VARCHAR(32),
tgt VARCHAR(32)
);
INSERT INTO
content
VALUES
('Bob' , 'id' , 'Bob'),
('Bob' , 'is' , 'Person'),
('Bob' , 'age' , '20'),
('Bob' , 'likes', 'cake'),
('Bob' , 'likes', 'chocolate'),
('Alice', 'id' , 'Alice'),
('Alice', 'is' , 'Person'),
('Alice', 'hates', 'chocolate')
WITH
id_rel AS
(
SELECT
id,
rel,
JSON_GROUP_ARRAY(tgt) AS tgt
FROM
content
GROUP BY
id,
rel
)
SELECT
JSON_GROUP_OBJECT(
rel,
CASE WHEN rel='id'
THEN JSON(tgt)->0
ELSE JSON(tgt)
END
)
AS entity
FROM
id_rel
GROUP BY
id
ORDER BY
id
entity
{"hates":["chocolate"],"id":"Alice","is":["Person"]}
{"age":["20"],"id":"Bob","is":["Person"],"likes":["cake","chocolate"]}
fiddle
You must aggregate in two steps, as your edited code doesn't combine cake and chocolate in to a single array of two elements...
https://dbfiddle.uk/9ptyuhuj
I have two tables in my postgresql database which I want to join:
Table 1) platforms:
ID (INT) | data (JSONB)
------------+---------------------------------------------------------
1 | {"identity": "1", "platformName": "Teradata" }
2 | {"identity": "2", "platformName": "MSSQL" }
Table 2) users:
ID (INT) | data (JSONB)
------------+-----------+---------------
12 | { "role": "developer", "identity": "12", "accessRights": {"platforms": ["1"]} }
13 | { "role": "admin", "identity": "13", "accessRights": {"platforms": ["1", "2"]}" }
I need to get the list of platforms along with the list of users who has access to them. Something like this:
Platform ID | data (JSONB)
------------+-----------+---------------
1 | [{"role": "developer", "identity": "12"}]
2 | [{"role": "developer", "identity": "12"}, {"role": "admin", "identity": "13"}]
I thought maybe something like this can help:
SELECT p.id, u.id, u.role
FROM users u
INNER JOIN platforms p ON (u.data->>'accessRights'->'platforms')::text::int = p.id
GROUP BY p.id
But I can't make it work. So is there anyway to get the result I need?
A simple join is not enough because you need to aggregate information from the users.data JSON for each platform.
select p.id as platform_id, u.*
from platforms p
cross join lateral (
select jsonb_agg(u.data - 'accessRights') as data
from users u
where u.data -> 'accessRights' -> 'platforms' ? p.id::text
) as u
Note this only works because you stored the platform IDs in the array as strings, not as (JSON) integers, because the ? operator only works on strings.
Online example
A table says products have a JSONB column called identifiers that stores an array of JSON objects.
Sample data in products
id | name | identifiers
-----|-------------|---------------------------------------------------------------------------------------------------------------
1 | umbrella | [{"id": "productID-umbrella-123", "domain": "ecommerce.com"}, {"id": "amzn-123", "domain": "amzn.com"}]
2 | ball | [{"id": "amzn-234", "domain": "amzn.com"}]
3 | bat | [{"id": "productID-bat-234", "domain": "ecommerce.com"}]
Now, I have to write a query that sorts the elements in the table based on the "id" value for the domain "amzn.com"
Expected result
id | name | identifiers
----- |--------------|---------------------------------------------------------------------------------------------------------------
3 | bat | [{"id": "productID-bat-234", "domain": "ecommerce.com"}]
1 | umbrella | [{"id": "productID-umbrella-123", "domain": "ecommerce.com"}, {"id": "amzn-123", "domain": "amzn.com"}]
2 | ball | [{"id": "amzn-234", "domain": "amzn.com"}]
ids of amzn.com are "amzn-123" and "amzn-234".
When sorted by ids of amzn.com "amzn-123" appears first, followed by "amzn-234"
Ordering the table by values of "id" for the domain "amzn.com",
record with id 3 appears first since the id for amzn.com is NULL,
followed by a record with id 1 and 2, which has a valid id that is sorted.
I am genuinely clueless as to how I could write a query for this use case.
If it were a JSONB and not an array of JSON I would have tried.
Is it possible to write a query for such a use case in PostgreSQL?
If yes, please at least give me a pseudo code or the rough query.
As you don't know the position in the array, you will need to iterate over all array elements to find the amazon ID.
Once you have the ID, you can use it with an order by. Using nulls first puts those products at the top that don't have an amazon ID.
select p.*, a.amazon_id
from products p
left join lateral (
select item ->> 'id' as amazon_id
from jsonb_array_elements(p.identifiers) as x(item)
where x.item ->> 'domain' = 'amzn.com'
limit 1 --<< safe guard in case there is more than one amazon id
) a on true --<< we don't really need a join condition
order by a.amazon_id nulls first;
Online example
With Postgres 12 this would be a bit shorter:
select p.*
from products p
order by jsonb_path_query_first(identifiers, '$[*] ? (#.domain == "amzn.com").id') nulls first
After few tweaks, this is the query that finally made it,
select p.*, amzn -> 'id' AS amzn_id
from products p left join lateral JSONB_ARRAY_ELEMENTS(p.identifiers) amzn ON amzn->>'domain' = 'amzn.com'
order by amzn_id nulls first
I have a users table with columns like id, name, email, etc. I want to retrieve information of some users in the following format in a single json object:
{
"userId_1" : {"name" : "A", "email": "A#gmail.com"},
"userId_2" : {"name" : "B", "email": "B#gmail.com"}
}
Wherein the users unique id is the key and a json containing his information is its corresponding value.
I am able to get this information in two separate rows using json_build_object but I would want it get it in a single row in the form of one single json object.
You can use json aggregation functions:
select jsonb_object_agg(id, to_jsonb(t) - 'id') res
from mytable t
jsonb_object_agg() aggregates key/value pairs into a single object. The key is the id of each row, and the values is a jsonb object made of all columns of the table except id.
Demo on DB Fiddle
Sample data:
id | name | email
:------- | :--- | :----------
userid_1 | A | A#gmail.com
userid_2 | B | B#gmail.com
Results:
| res |
| :----------------------------------------------------------------------------------------------------- |
| {"userid_1": {"name": "A", "email": "A#gmail.com"}, "userid_2": {"name": "B", "email": "B#gmail.com"}} |
try -
select row_to_json(col) from T
link below might help https://hashrocket.com/blog/posts/faster-json-generation-with-postgresql
Try this:
SELECT json_object(array_agg(id), array_agg(json::text)) FROM (
SELECT id, json_build_object('name', name, 'email', email) as json
FROM users_table
) some_alias_name
If your id is not of text type then you have to cast it to text too.
Here is a DB example
table "Users"
fname | lname | id | email
Joe | smith | 1 | yadda#goo.com
Bob | smith | 2 | bob#goo.com
Jane | smith | 3 | jane#goo.com
table "Awards"
userId | award
1 | bigaward
1 | smallaward
1 | thisaward
2 | thataward
table "Invites"
userId | invited
1 | true
3 | true
Basically, how do you write a query in PostgreSQL that allows you to create something like this:
[{
fname:"Joe",
lname:"Smith",
id: 1,
email: "yadda#goo.com",
invited: true,
awards: ["bigaward", "smallaward", "thisaward"]
},
{
fname:"Jane",
lname:"Smith",
id: 3,
email: "jane#goo.com",
invited: true,
awards: []
}]
Here is what I am trying to do...
SELECT users.fname, users.lname, users.id, users.email, invites.invited, awards.award(needs to be an array)
FROM users
JOIN awards on ....(unknown)
JOIN invites on invites.userid = users.id
WHERE invited = true
the above array would be the desired output, just can't figure out a good one shot query. I tried the PostgreSQL docs, but to no avail. I think I might need a WITH statement?
Thanks in advance, Postgres guru!
PostgreSQL v. 9.2
Answered by RhodiumToad on the postgresql IRC:
SELECT users.fname, users.lname, .... array(select awards.award from awards where a.id = user.id) as awards
FROM users
JOIN invites on invites.userid = users.id
WHERE invited = true
array() then through a query inside of it... brilliant!
I think it can be as simple as:
SELECT u.fname, u.lname, u.id, u.email, i.invited, array_agg(a.award)
FROM users u
JOIN invites i ON i.userid = u.id AND i.invited
LEFT JOIN awards a ON a.userid = u.id
GROUP BY u.fname, u.lname, u.id, u.email, i.invited
Your display in JSON format just makes it seem more complicated.
Use a basic GROUP BY including all columns not to be aggregated - leaving award, which you aggre3gate into an array with the help of the aggregate function array_agg().
The LEFT JOIN is important, so not to exclude users without any awards by mistake.
Note that I ignored CaMeL-case spelling in my example to avoid the ugly double-quoting.
This is considerably faster for bigger tables than notoriously slow correlated subqueries - like demonstrated in the added example in the question.
->SQLfiddle demo
You will need to use a hasNext or forEach (or similar) method to iterate trough the database. Whilst you're iterating you can add the results to an array and then encode that array to JSON.
I think I may have misunderstood your question. Apologies if inhave