Show filterable rows count - sql

These are my tables:
CREATE TABLE product (
product_id serial PRIMARY KEY,
name VARCHAR ( 50 ),
size VARCHAR ( 50 ),
)
CREATE TABLE country (
country_id serial PRIMARY KEY,
name VARCHAR ( 50 ),
product_id INT
)
CREATE TABLE color (
color_id serial PRIMARY KEY,
name VARCHAR ( 50 ),
product_id INT
)
I want my query to return the list of product in this way.
Query result needs to have to objects: meta and result
The result needs to be paginated with 10 objects. And meta should include the total count of filtered products, count of attributes of products.
When country is filtered, I want to see other country choices' names and counts as well, not only the country filtered (same for the color).
If the color is filtered, I don't want to see the countries that are not available with this color for the products we have (and vice versa):
{
"meta": {
"count" : 200,
"next_page": true,
"colors": [
{"id": 1, "name": "red", "count": 5},
{"id": 2, "name": "white", "count": 10}
],
"countries": [
{"id": 1, "name": "Germany", "count": 120},
{"id": 2, "name": "Albania", "count": 201}
],
"sizes": [
{"id": 1, "name": "Big", "count": 45},
{"id": 2, "name": "Small", "count": 63}
]
},
"result": [
{
"product_name" : "Milk",
"color": "White",
"country": "Germany"
},
{
"product_name" : "Milk2",
"color": "White",
"country": "Germany"
},
{
"product_name" : "Milk3",
"color": "White",
"country": "Germany"
}
]
}
This is what I've done:
WITH results as (
SELECT
product.id,
product.name,
product.size,
color.name,
country.name
FROM product
LEFT JOIN color ON color.product_id = product.id
LEFT JOIN country ON country.product_id = product.id
WHERE color.name = ANY('{White}')
)
SELECT
(
SELECT
jsonb_build_object(
'count', count.full_count,
'next_page', count.full_count - (1 * 10) > 0
)
FROM (SELECT count(id) AS full_count FROM results) AS count
) AS meta,
(
SELECT jsonb_agg(result_rows)
FROM
(SELECT * FROM results
LIMIT 10
OFFSET (1-1) * 10) AS result_rows
) AS result
I've tried lot's of thing and did not get the result of getting name and counts of country and colors. So I didn't include that part of query. BTW, the the slight change in query returning result is acceptable.
Any help is highly appreciated. I'm using the latest version of PostgreSQL. You can see this type of query used in Ebay (search results page) where page filter properties change while you select different filters to correspond the available choices and counts depending your current filters.

First of all, your data model looks like strange (or wrong). I would suggest you the following data model instead, so that you can store several products with the same size and/or the same color and/or the same country. The only limitation here is that one product may only have one size, one color, and one country. You will have to create new tables if you want to manage one to many relationships.
CREATE TABLE size (
size_id serial PRIMARY KEY,
name VARCHAR ( 50 )
) ;
CREATE TABLE country (
country_id serial PRIMARY KEY,
name VARCHAR ( 50 )
) ;
CREATE TABLE color (
color_id serial PRIMARY KEY,
name VARCHAR ( 50 )
) ;
CREATE TABLE product (
product_id serial PRIMARY KEY,
name VARCHAR ( 50 ),
size_id INT CONSTRAINT rf_size REFERENCES size (size_id) MATCH SIMPLE,
color_id INT CONSTRAINT rf_color REFERENCES color (color_id) MATCH SIMPLE,
country_id INT CONSTRAINT rf_country REFERENCES country (country_id) MATCH SIMPLE
) ;
Then you can get your expected with the following query :
WITH global_list AS (
SELECT p.size_id, p.color_id, p.country_id
, s.name AS size_name, clr.name AS color_name, cty.name AS country_name
, count(*) AS product_count
FROM product AS p
INNER JOIN country AS cty
ON cty.country_id = p.country_id
INNER JOIN color AS clr
ON clr.color_id = p.color_id
INNER JOIN size AS s
ON s.size_id = p.size_id
GROUP BY p.size_id, size_name, p.color_id, color_name, p.country_id, country_name
), result_list AS (
SELECT jsonb_build_object('product_name',p.name,'color',clr.name,'country',cty.name, 'size', s.name) AS result
, count(*) OVER () AS total_count
FROM product AS p
INNER JOIN country AS cty
ON cty.country_id = p.country_id
INNER JOIN color AS clr
ON clr.color_id = p.color_id
INNER JOIN size AS s
ON s.size_id = p.size_id
WHERE cty.name = COALESCE('Albania', cty.name) -- enter here the country filter criteria if any, like 'Germany', or NULL if no country criteria
AND clr.name = COALESCE(NULL, clr.name) -- enter here the color filter criteria if any, like 'White', or NULL if no color criteria
AND s.name = COALESCE(NULL, s.name) -- enter here the size filter criteria if any, like 'Medium', or NULL if no size criteria
), country_list AS (
SELECT jsonb_build_object('id', gl.country_id, 'name', gl.country_name, 'count', sum(gl.product_count)) AS country
FROM global_list AS gl
WHERE gl.color_name = COALESCE(NULL, gl.color_name) -- same color criteria than above
AND gl.size_name = COALESCE(NULL, gl.size_name) -- same size criteria than above
GROUP BY gl.country_id, gl.country_name
), color_list AS (
SELECT jsonb_build_object('id', gl.color_id, 'name', gl.color_name, 'count', sum(gl.product_count)) AS color
FROM global_list AS gl
WHERE gl.country_name = COALESCE('Albania', gl.country_name) -- same country criteria than above
AND gl.size_name = COALESCE(NULL, gl.size_name) -- same size criteria than above
GROUP BY gl.color_id, gl.color_name
), size_list AS (
SELECT jsonb_build_object('id', gl.size_id, 'name', gl.size_name, 'count', sum(gl.product_count)) AS size
FROM global_list AS gl
WHERE gl.country_name = COALESCE('Albania', gl.country_name) -- same country criteria than above
AND gl.color_name = COALESCE(NULL, gl.color_name) -- same color criteria than above
GROUP BY gl.size_id, gl.size_name
)
SELECT (SELECT jsonb_build_object('result', jsonb_agg(result)) FROM result_list LIMIT 10 OFFSET 0)
|| jsonb_build_object('meta'
, jsonb_build_object( 'count', (SELECT total_count FROM result_list LIMIT 1)
, 'next_page', (SELECT total_count > 10 FROM result_list LIMIT 1)
, 'countries', (SELECT jsonb_agg(country) FROM country_list)
, 'colors', (SELECT jsonb_agg(color) FROM color_list)
, 'sizes', (SELECT jsonb_agg(size) FROM size_list)
)
)
ps : the first query Global_list could be implemented as a view
The result looks like :
{
"meta": {
"count": 2,
"sizes": [
{
"id": 1,
"name": "Small",
"count": 1
},
{
"id": 2,
"name": "Medium",
"count": 1
}
],
"colors": [
{
"id": 1,
"name": "White",
"count": 1
},
{
"id": 3,
"name": "Blue",
"count": 1
}
],
"countries": [
{
"id": 1,
"name": "Germany",
"count": 1
},
{
"id": 3,
"name": "Albania",
"count": 2
}
],
"next_page": false
},
"result": [
{
"size": "Medium",
"color": "White",
"country": "Albania",
"product_name": "Milk2"
},
{
"size": "Small",
"color": "Blue",
"country": "Albania",
"product_name": "Milk3"
}
]
}
All details in dbfiddle

Related

Group a record under a static type using sql

StudentType
ID
Name
1
Hostel
2
Home
Students
ID
Name
studentTypeId
deletedAt
1
First
1
null.
2
Second
1
2022-02-07
3
Third
null.
4
Fourth
2
null
5
Fifth
2
null
So, I have two tables studentTypes and students. The student table could exist without the studentTypeId.
Using sql I want to be able to get the following json. The student with no id needs to be under unassigned type.
{
"studentTypes": [{
"id": 1,
"name": "Hostel",
"students": [{
"id": 1,
"name": "First",
"studentTypeId": 1
}
]
},
{
"id": 2,
"name": "Home Going",
"students": [{
"id": 1,
"name": "Fourth",
"studentTypeId": 2
},
{
"id": 1,
"name": "Fifth",
"studentTypeId": 2
}
]
},
{
"name": "Unassigned",
"students": [{
"id": 3,
"name": "Third",
"studentTypeId": null
},
{
"id": 1,
"name": "Second",
"studentTypeId": 1
}]
}
]
}
I want the last type as Unassigned which will have students with no studentTypeId as well as which has deletedAt set. I cant figure-out how to get it.
The Unassigned student types can be included via a UNION ALL.
Then the students can be joined lateral via an IS NOT DISTINCT FROM.
If you want a pretty json, you could use the jsonb_pretty function.
WITH CTE_STUDENTTYPES AS (
SELECT id, name
FROM StudentType
UNION ALL
SELECT NULL, 'Unassigned'
)
SELECT
JSON_BUILD_OBJECT ('studentTypes',
JSON_AGG (
CASE WHEN t.ID IS NULL
THEN JSON_BUILD_OBJECT ('name', t.name, 'students', l.Students)
ELSE JSON_BUILD_OBJECT ('id', t.ID, 'name', t.name, 'students', l.Students)
END
))::json as StudentTypes
FROM CTE_STUDENTTYPES t
CROSS JOIN LATERAL (
SELECT JSON_AGG (
JSON_BUILD_OBJECT ('id', s.ID, 'name', s.name, 'studentTypeId', s.studentTypeId)
) AS Students
FROM Students s
WHERE (CASE WHEN s.deletedAt IS NULL
THEN s.studentTypeId
END) IS NOT DISTINCT FROM t.ID
) l;
studenttypes
{"studentTypes" : [{"id" : 1, "name" : "Hostel", "students" : [{"id" : 1, "name" : "First", "studentTypeId" : 1}]}, {"id" : 2, "name" : "Home", "students" : [{"id" : 4, "name" : "Fourth", "studentTypeId" : 2}, {"id" : 5, "name" : "Fifth", "studentTypeId" : 2}]}, {"name" : "Unassigned", "students" : [{"id" : 3, "name" : "Third", "studentTypeId" : null}, {"id" : 2, "name" : "Second", "studentTypeId" : 1}]}]}
Demo on db<>fiddle here

Get list of user with their latest 2 inputs -(TypeORM ,sql)

How do write a sql query to get at least 2 items for distinct UUIDs
user-table
id name
xxx a
xyx b
zzz e
visitedlocation-table
id startDate userID location
1. 1/2/21 xxx USA
2. 1/3/21 xxx UK
3. 1/2/21 xyx AR
4. 1/3/21 xyx USA
5. 1/5/21 zzz USA
6. 1/6/21 xxx IN
I want to get a list of users with their last two visited locations
Desired output
[
{
id: "xxx",
name: "a",
lastVisits: [
{
id: "6",
startDate: "1/6/21",
location: "IN"
},
{
id: "2",
startDate: "1/3/21",
location: "UK"
}
]
},
{
id: "xyx",
name: "b",
lastVisits: [
{
id: "4",
startDate: "1/3/21",
location: "USA"
},
{
id: "3",
startDate: "1/2/21",
location: "AR"
}
]
},
{
id: "zzz",
name: "b",
lastVisits: [
{
id: "5",
startDate: "1/5/21",
location: "USA"
}
]
}
]
I am using Type Orm and the user entity has a one to many relations with the "visited location" table
repository
.createQueryBuilder('user)
.leftJoinAndSelect(
'user.visitedLocation',
'visitedLocation',
'visitedLocation.userId = user.id'
)
.getRawMany();
I tried using this query but it returns all the visited locations. But I want only the last 2 visited locations.
If it's hard do in query builder please suggest SQL query for this
You can try dense_rank() to rank your rows and only get the last two rows
SELECT userID,startDate,location
FROM
(
SELECT a.id as userID, b.startDate, b.location,
--this will group your rows by user_id and then rank them based on startDate
DENSE_RANK() OVER(PARTITION BY b.userID ORDER BY b.startDate DESC) as
row_rank
FROM user-table a
INNER JOIN visitedlocation-table b
ON (a.id = b.userID)
)T WHERE row_rank <=2 -- fetch only the first two rows
you can take inspiration from the above logic. I'll be posting the JSON based output solution too
Edit
WITH user_visits AS
(
SELECT userID,name,id,startDate,location
FROM
(
SELECT a.id as userID,a.name,b.id, b.startDate, b.location,
--this will group your rows by user_id and then rank them based on startDate
DENSE_RANK() OVER(PARTITION BY b.userID ORDER BY b.startDate DESC) as
row_rank
FROM user_table a
INNER JOIN visitedlocation_table b
ON (a.id = b.userID)
)T WHERE row_rank <=2 -- fetch only the first two rows
)
SELECT jsonb_pretty(array_to_json(array_agg(row_to_json(t)))::jsonb)
FROM(
SELECT userid as id, name,
(
SELECT array_to_json(array_agg(row_to_json(d)))
FROM(
SELECT id,startdate,location
FROM user_visits b
WHERE b.userid = u.userid
)d
) as lastVisits
FROM user_visits u
GROUP BY userid,name
ORDER BY userid
)t;
output of above query
[
{
"id": "xxx",
"name": "a",
"lastvisits": [
{
"id": 6,
"location": "IN",
"startdate": "2021-06-01"
},
{
"id": 2,
"location": "UK",
"startdate": "2021-03-01"
}
]
},
{
"id": "xyz",
"name": "b",
"lastvisits": [
{
"id": 4,
"location": "USA",
"startdate": "2021-03-01"
},
{
"id": 3,
"location": "AR",
"startdate": "2021-02-01"
}
]
},
{
"id": "zzz",
"name": "e",
"lastvisits": [
{
"id": 5,
"location": "USA",
"startdate": "2021-05-01"
}
]
}
]

Reading from JSON column with dynamic element name

I have a table that contains json data that I need to unpick and store in a relational DB. I am using an Oracle DB 19.0 and have created the following table:
CREATE TABLE J_PAGE
(
PK_PAGE_ID NUMBER ( 10 , 0 )
, FK_RESP_ID NUMBER ( 10 , 0 ) DEFAULT -1
, CL_PAGE_ID NUMBER ( 10 , 0 ) DEFAULT -1
, CL_RESP CLOB CONSTRAINT CK_PAGE_01 CHECK ( CL_RESP IS JSON )
, CL_DT DATE DEFAULT SYSDATE
) ;
The column CL_RESP is a clob value that returns the response from a call to UTL_HTTP.GET_RESPONSE.
{
"count": 2,
"results": [
{
"key": "stories",
"id": "10000"
},
{
"key": "stories",
"id": "10001"
}
],
"stories": {
"10000": {
"title": "story1",
"description": null,
"start_date": "2020-04-01",
"id": "10001"
},
"10001": {
"title": "story2",
"description": null,
"start_date": null,
"id": "10001"
}
},
"meta": {
"count": 2,
"page_count": 1,
"page_number": 1,
"page_size": 20
}
}
I need to extract the contents of the "stories" element but I'm getting stuck where the next element doesn't have a static name.
I know I can do the following to return 1 row using story 10001...
SELECT
page.CL_RESP.stories."10001".title[*] CL_TITLE
FROM
J_PAGE page ;
But I need all stories so perhaps a wildcard in place of 10001?
SELECT
page.CL_RESP.stories.*.title[*] CL_TITLE
FROM
J_PAGE page ;
Can anyone help?
Solved it.
SELECT
t.*
FROM
J_PAGE o,
JSON_TABLE ( o.CL_RESP , '$.stories.*'
COLUMNS (
CL_TITLE VARCHAR2 PATH '$.title'
, CL_DESC VARCHAR2 PATH '$.description'
, CL_START_DT TIMESTAMP PATH '$.start_date'
, CL_ID NUMBER PATH '$.id'
)
) t ;

Sql query to json result

I have these tables
destination (id integer, name text)
activity (id integer, name text, destination_id integer)
activity_duration (value numeric, unit text, label text, activity_id integer)
activity_duration_price(amount numeric, currency_id integer, activity_duration_id integer)
Each destination has 1 or more activities, each activity has 1 or more duration and each duration has 1 price.
here is my query
select
d.id destination_id,
d.name destination_name,
count(a.id) as nb_activity,
string_agg('{"id":'||a.id||',"name":"'||a.name||'","pictures":"'||a.pictures||'","meta":{"price":'||amount||',"label":"'||label||'"}}',',') activities
from destination d
left join activity a on (a.destination_id = d.id)
left join activity_duration ad on (activity_id = a.id)
left join activity_duration_price adp on (adp.activity_duration_id = ad.id)
where d.id = any (array[142]) group by d.id, d.name order by nb_activity desc
and this is the result for the string_agg
[{
"id": 2,
"name": "visit tokyo",
"meta": {
"price": 210,
"label": "Adult"
}
}, {
"id": 2,
"name": "visit tokyo",
"pictures": "{}",
"meta": {
"price": 170,
"label": "Children"
}
}]
As you can see, the meta information is there twice for the same item.
I want my result for the string_agg to be like this
[{
"id": 2,
"name": "visit tokyo",
"meta": [{
"price": 210,
"label": "Adult"
},
{
"price": 170,
"label": "Children"
}
]
}]
How can I get this result?
array_agg - to create array of rows
array_to_json - to create JSON array
Then cast the JSON array to text.
SELECT
d.id destination_id,
d.name destination_name,
count(a.id) AS nb_activity,
string_agg('{"id":'||a.id||',"name":"'||a.name||'","pictures":"'||a.pictures||'","meta":'||
(
SELECT array_to_json(array_agg(t))::TEXT --cast to TEXT to concatenate to the string
FROM
(
SELECT
adp.amount,
ad.label
FROM activity a2
LEFT JOIN activity_duration ad ON (ad.activity_id = a2.id)
LEFT JOIN activity_duration_price adp ON (adp.activity_duration_id = ad.id)
) AS t
)
||'}'
,','
) AS activities
FROM destination d
LEFT JOIN activity a ON (a.destination_id = d.id)
WHERE d.id = ANY (ARRAY[142]) GROUP BY d.id, d.name ORDER BY nb_activity DESC

How to join jsonb array elements in Postgres?

I am using Postgres 9.5, and I have the following tables:
Users
id UUID
name TEXT
Images
id UUID
key TEXT
width INTEGER
height INTEGER
Posts
id UUID
title TEXT
author_id UUID
content JSONB
The posts' content is like:
[
{ "type": "text", "text": "learning pg" },
{ "type": "image", "image_id": "8f4422b4-3936-49f5-ab02-50aea5e6755f" },
{ "type": "image", "image_id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835" },
{ "type": "text", "text": "pg is awesome" }
]
Now I want to join the image type of content, and populate them with image_id, like:
{
"id": "cb1267ca-b1ac-4daa-8c7e-72d4c000e9fa",
"title": "Learning join jsonb in Postgres",
"author_id": "deba01b7-ec58-4cc2-b3ae-7dc42e582767",
"content": [
{ "type": "text", "text": "learning pg" },
{
"type": "image",
"image": {
"id": "8f4422b4-3936-49f5-ab02-50aea5e6755f",
"key": "/upload/test1.jpg",
"width": 800,
"height": 600
}
},
{
"type": "image",
"image": {
"id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835",
"key": "/upload/test2.jpg",
"width": 1280,
"height": 720
}
},
{ "type": "text", "text": "pg is awesome" }
]
}
Here is my test sql file:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DROP TABLE IF EXISTS Users;
DROP TABLE IF EXISTS Images;
DROP TABLE IF EXISTS Posts;
CREATE TABLE Users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name text NOT NULL
);
CREATE TABLE Images (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
key TEXT,
width INTEGER,
height INTEGER,
creator_id UUID
);
CREATE TABLE Posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title TEXT,
author_id UUID,
content JSONB
);
DO $$
DECLARE user_id UUID;
DECLARE image1_id UUID;
DECLARE image2_id UUID;
BEGIN
INSERT INTO Users (name) VALUES ('test user') RETURNING id INTO user_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test1.jpg', 800, 600, user_id) RETURNING id INTO image1_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test2.jpg', 600, 400, user_id) RETURNING id INTO image2_id;
INSERT INTO Posts (title, author_id, content) VALUES (
'test post',
user_id,
('[ { "type": "text", "text": "learning pg" }, { "type": "image", "image_id": "' || image1_id || '" }, { "type": "image", "image_id": "' || image2_id || '" }, { "type": "text", "text": "pg is awesome" } ]') :: JSONB
);
END $$;
Is there any way to implement this requirement?
SELECT jsonb_pretty(to_jsonb(p)) AS post_row_as_json
FROM (
SELECT id, title, author_id, c.content
FROM posts p
LEFT JOIN LATERAL (
SELECT jsonb_agg(
CASE WHEN c.elem->>'type' = 'image' AND i.id IS NOT NULL
THEN elem - 'image_id' || jsonb_build_object('image', i)
ELSE c.elem END) AS content
FROM jsonb_array_elements(p.content) AS c(elem)
LEFT JOIN images i ON c.elem->>'type' = 'image'
AND i.id = (elem->>'image_id')::uuid
) c ON true
) p;
How?
Unnest the jsonb array, producing 1 row per array element:
jsonb_array_elements(p.content) AS c(elem)
For each element LEFT JOIN to images on the conditions that
... the key 'type' has the value 'image': c.elem->>'type' = 'image'
... the UUID in image_id matches: i.id = (elem->>'image_id')::uuid
An invalid UUID in content would raise an exception.
For image types, where a matching image was found
c.elem->>'type' = 'image' AND i.id IS NOT NULL
remove the key 'image_id' and add the related image row as jsonb value:
elem - 'image_id' || jsonb_build_object('image', i)
Else keep the original element.
Re-aggregate the modified elements to a new content column with jsonb_agg().
Would work with a plain ARRAY constructor as well.
Unconditionally LEFT JOIN LATERAL the result to posts and select all columns, only replace p.content with the generated replacement c.content
In the outer SELECT, convert the whole row to jsonb with a simple to_jsonb().
jsonb_pretty() is only for human-readable representation and totally optional.
All jsonb functions are documented in the manual.