SQL: Group By and create a new json column - sql

I have a table
-----------------------------------------------------
| id | name | result |
-----------------------------------------------------
| 1 | Maria | {type: download, status: pass} |
| 1 | Maria | {type: login, status: fail} |
| 2 | Tony | {type: download, status: fail} |
| 3 | Kate | {type: download, status: pass} |
| 3 | Kate | {type: login, status: pass} |
| 3 | Kate | {type: logout, status: pass} |
| 4 | Tom | {type: download, status: pass} |
-----------------------------------------------------
And I want to try to get this but not sure that it possible:
-----------------------------------------------------------
| id | name | download | action |
-----------------------------------------------------------
| 1 | Maria | pass | {login: fail} |
| 2 | Tony | fail | |
| 3 | Kate | pass | {login: pass, logout: pass} |
| 4 | Tom | pass | |
-----------------------------------------------------------
I found way to create a new simple column:
SELECT id, name
results.download AS download
FROM
(
SELECT id, name jsonb_agg(result) AS results
FROM my_table
GROUP BY id, name
) AS r
ORDER BY id, name;
but also I want to move login and logout field to action column. Is it possible?

The formatting is not exactly as you want but we have the right information in the right place.
create table my_table (
id int,
name varchar(10),
result text
);
insert into my_table values
(1,'Maria','{ "type": "download", "status": "pass"}'),
(1,'Maria','{ "type": "login", "status": "fail"}'),
(2,'Tony','{ "type": "download", "status": "fail"}'),
(3,'Kate','{ "type": "download", "status": "pass"}'),
(3,'Kate','{ "type": "login", "status": "pass"}'),
(3,'Kate','{ "type": "logout", "status": "pass"}'),
(4,'Tom','{ "type": "download", "status": "pass"}');
✓
7 rows affected
SELECT id, name
results.download AS download
FROM
(
SELECT id, name jsonb_agg(result) AS results
FROM my_table
GROUP BY id, name
) AS r
ORDER BY id, name;
ERROR: syntax error at or near "."
LINE 2: results.download AS download
^
select
id,
name,
result::json->'type' AS type,
result::json->'status' AS status
FROM my_table
id | name | type | status
-: | :---- | :--------- | :-----
1 | Maria | "download" | "pass"
1 | Maria | "login" | "fail"
2 | Tony | "download" | "fail"
3 | Kate | "download" | "pass"
3 | Kate | "login" | "pass"
3 | Kate | "logout" | "pass"
4 | Tom | "download" | "pass"
with cte as
(select
id,
name,
case when cast(result::json->'type' as text) = '"download"'
then cast(result::json->'status' as text) end as "download" ,
case when cast(result::json->'type' as text) in ('"login"','"logout"')
then concat(result::json->'type',': ',result::json->'status') end as "action"
from my_table)
select
id,
name,
max(download),
string_agg(action,', ') "action"
from cte
group by
id,
name
order by id
id | name | max | action
-: | :---- | :----- | :--------------------------------
1 | Maria | "pass" | "login": "fail"
2 | Tony | "fail" | null
3 | Kate | "pass" | "login": "pass", "logout": "pass"
4 | Tom | "pass" | null
db<>fiddle here

You can aggregate the downloads and the "other" items into two separate columns:
with data as (
select id,
name,
jsonb_object_agg('download', result ->> 'status') filter (where result ->> 'type' = 'download') as download,
jsonb_object_agg(result ->> 'type', result ->> 'status') filter (where result ->> 'type' <> 'download') as result
from my_table
group by id, name
)
select id, name,
download ->> 'download' as download,
result
from data;
The CTE returns this:
id | name | download | results
---+-------+----------------------+------------------------------------
1 | Maria | {"download": "pass"} | {"login": "fail"}
2 | Tony | {"download": "fail"} |
3 | Kate | {"download": "pass"} | {"login": "pass", "logout": "pass"}
4 | Tom | {"download": "pass"} |
And the final then just extracts the value for the download key.
Online example

Related

Get all records that have ALL values passed as parameters

I have a table of properties:
+----+-----------------------------+
| prop_id | prop_name |
+---------+------------------------+
| 1 | Cottage |
+---------+------------------------+
| 2 | Mountain House |
+---------+------------------------+
| 3 | Beach house |
+---------+------------------------+
A table of accessories:
+----+-----------------------------+
| acc_id | acc_name |
+---------+------------------------+
| GAR | With garden |
+---------+------------------------+
| TER | With terrace |
+---------+------------------------+
| REN | Recently renovated |
+---------+------------------------+
A table that relates properties and accessories (properties2accessories):
+----+--------------+
| prop_id | acc_id |
+---------+---------+
| 1 | GAR |
+---------+---------+
| 1 | REN |
+---------+---------+
| 2 | GAR |
+---------+---------+
| 2 | REN |
+---------+---------+
| 2 | TER |
+---------+---------+
| 3 | GAR |
+---------+---------+
| 3 | TER |
+---------+---------+
I need all the properties that have ALL the accessories that I pass as parameters.
Correct examples:
a) Properties with "Garden" and "Recently renovated":
I should get props: 1, 2
b) Properties with "Garden" and "Terrace":
I should get props: 2, 3
I try:
SELECT *
FROM properties2accessories
WHERE acc_id IN ('GAR', 'REN');
but this get prop 3 too, that not has "Recently renovated"
I'm using Postgres 13
Any helps?
You could do something like this:
SELECT prop_id from (
select prop_id, array_agg(acc_id) acc_array
FROM properties2accessories
group by prop_id) d
WHERE array['GAR', 'REN'] <# acc_array;
That's what HAVING can do for you, it's a WHERE condition over the entire group:
SELECT prop_id
FROM properties2accessories
WHERE acc_id IN ('GAR', 'REN')
GROUP BY prop_id
HAVING ARRAY_AGG(acc_id) #> ARRAY['GAR', 'REN'];
You con check with exists if another condition is present for a specific id
SELECT
"prop_id"
FROM properties2accessories p
WHERE ("acc_id" = 'GAR')
AND EXISTS (SELECT 1 FROM properties2accessories WHERE "acc_id" = 'REN' AND "prop_id" = p."prop_id")
prop_id
1
2
SELECT 2
fiddle

aggregate 3 Tables to json postgresql

I have 3 tables:
customers:
_______________
| id | name |
|-----|-------|
| 1 | John |
| 2 | Adam |
| 3 | Smith |
---------------
cities:
__________________
| id | city |
|-----|----------|
| 1 | LA |
| 2 | PHI |
| 3 | HOU |
| 4 | DET |
------------------
customers_cities:
________________________________
| id | customer_id | city_id |
|-----|--------------|----------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 3 |
| 4 | 3 | 2 |
| 5 | 1 | 3 |
--------------------------------
I wanted to aggregate and show in a json format all the cities and their customers listed in them. Some customers can be in different cities too.
Output should look like:
- LA:
|- John
- PHI
|- John
|- Smith
- Hou
|- Adam
|-John
The JSON output looks like this:
"data":{
"customers_in_cities": [
"LA" : [
{
"id": 1,
"name": "John"
}],
"PHI" : [
{
"id": 1,
"name": "John"
},
{
"id": 3,
"name": "Adam"
}],
"HOU" : [
{
"id": 3,
"name": "Adam"
},
{
"id": 1,
"name": "John"
}],
]
}
Something like the above json output.
I tried:
SELECT cus.name AS customer_name, cty.city AS city_name
FROM cities cty
INNER JOIN customers_cities cc ON cc.city_id = cty.id
INNER JOIN customers cus ON cc.customer_id = cus.id;
will appreciate any help
I am going to post the SQL query that took me a couple of hours to find out! actually paying the credit to this post Return as array of JSON objects in SQL (Postgres)
e by Erwin Brandstetter
the sql query goes like this:
SELECT c.*,
json_agg(row_to_json(c)) AS customers_in_cities
FROM cities c
LEFT JOIN customers_cities cc ON cc.city_id = c.id
LEFT JOIN customers cust ON cust.id = cc.customer_id
GROUP BY c.id;
thank you

How to take the smallest date of a group?

I have a dataset which looks like that:
| id | status | open_date | name |
| 8 | active | 2019-3-2 | blab |
| 8 | active | 2019-3-8 | blub |
| 8 | inactive | 2019-3-9 | hans |
| 8 | active | 2019-3-10 | ana |
| 9 | active | 2019-3-4 | mars |
I want to achieve the following:
| id | status | open_date | name | status_change_date |
| 8 | active | 2019-3-2 | blab | 2019-3-2
| 8 | active | 2019-3-8 | blub | 2019-3-2
| 8 | inactive | 2019-3-9 | Hans | 2019-3-9
| 8 | active | 2019-3-10 | ana | 2019-3-10
| 9 | active | 2019-3-4 | mars | 2019-3-4
for each id I like to calculate when the status has last changed
I already tried with groupBy, but the problem is I only want to group by the rows with Active and Inactive which are next to each other. If there is an INACTIVE between ACTIVE I like to make a new group for the new ACTIVE.
Someone has an idea to solve that?
Here is a pure SQL solution that uses window functions. This works by generating a partition that contains consecutive records that have the same id and status.
SELECT
id,
status,
open_date,
name,
MIN(open_date) OVER(PARTITION BY id, rn1 - rn2 ORDER BY open_date) status_change_date
FROM (
SELECT
t.*,
ROW_NUMBER() OVER(PARTITION BY id ORDER BY open_date) rn1,
ROW_NUMBER() OVER(PARTITION BY id, status ORDER BY open_date) rn2
FROM mytable t
) x
ORDER BY id, open_date
Demo on DB Fiddle:
| id | status | open_date | name | status_change_date |
| --- | -------- | ---------- | ---- | ------------------ |
| 8 | active | 2019-03-02 | blab | 2019-03-02 |
| 8 | active | 2019-03-08 | blub | 2019-03-02 |
| 8 | inactive | 2019-03-09 | hans | 2019-03-09 |
| 8 | active | 2019-03-10 | ana | 2019-03-10 |
| 9 | active | 2019-03-04 | mars | 2019-03-04 |
Thats the answer on How to take the smallest date of a group?
let minDate = new Date('0001-01-01T00:00:00Z');
dataset.forEach(x => if( x.date > this.minDate) { this.minDate = x.date } )
console.log(this.minDate);
You can try this:
var movies = [
{title: 'The Godfather', rating: 9.2, release: '24 March 1972'},
{title: 'The Godfather: Part II', rating: 9.0, release: '20 December 1972'},
{title: 'The Shawshank Redemption', rating: 9.3, release: '14 October 1994'},
];
movies.sort(function(a, b) {
var dateA = new Date(a.release), dateB = new Date(b.release);
return dateA - dateB;
});
This sortby works because js lets you compare arithmetic on date objects, which are automatically converted to numeric representations first.
In SQL use MIN function:
ORDER
Id
OrderDate
OrderNumber
CustomerId
TotalAmount
SELECT MIN(OrderDate)
FROM [Order]
WHERE YEAR(OrderDate) = 2013

SQL - Rows that are repetitive with a particular condition

We have a table like this:
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
| ID | Name | RecievedService | FirstZoneTeeth | SecondZoneTeeth | ThirdZoneTeeth | FourthZoneTeeth |
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
| 1 | John | SomeService1 | 13 | | 4 | |
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
| 2 | John | SomeService1 | 34 | | | |
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
| 3 | Steve | SomeService3 | | | | 2 |
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
| 4 | Steve | SomeService4 | | | | 12 |
+----+-------+-----------------+----------------+-----------------+----------------+-----------------+
Every digit in zones is a tooth (dental science) and it means "John" has got "SomeService1" twice for tooth #3.
+----+------+-----------------+----------------+-----------------+----------------+-----------------+
| ID | Name | RecievedService | FirstZoneTeeth | SecondZoneTeeth | ThirdZoneTeeth | FourthZoneTeeth |
+----+------+-----------------+----------------+-----------------+----------------+-----------------+
| 1 | John | SomeService1 | 13 | | 4 | |
+----+------+-----------------+----------------+-----------------+----------------+-----------------+
| 2 | John | SomeService1 | 34 | | | |
+----+------+-----------------+----------------+-----------------+----------------+-----------------+
Note that Steve has received services twice for tooth #2 (4th Zone) but services are not one.
I'd write some code that gives me a table with duplicate rows (Checking the only patient and received service)(using "group by" clause") but I need to check zones too.
I've tried this:
select ROW_NUMBER() over(order by vv.ID_sick) as RowNum,
bb.Radif,
bb.VCount as 'Count',
vv.ID_sick 'ID_Sick',
vv.ID_service 'ID_Service',
sick.FNamesick + ' ' + sick.LNamesick as 'Sick',
serv.NameService as 'Service',
vv.Mab_Service as 'MabService',
vv.Mab_daryafti as 'MabDaryafti',
vv.datevisit as 'DateVisit',
vv.Zone1,
vv.Zone2,
vv.Zone3,
vv.Zone4,
vv.ID_dentist as 'ID_Dentist',
dent.FNamedentist + ' ' + dent.LNamedentist as 'Dentist',
vv.id_do as 'ID_Do',
do.FNamedentist + ' ' + do.LNamedentist as 'Do'
from visiting vv inner join (
select ROW_NUMBER() OVER(ORDER BY a.ID_sick ASC) AS Radif,
count(a.ID_sick) as VCount,
a.ID_sick,
a.ID_service
from visiting a
group by a.ID_sick, a.ID_service, a.Zone1, a.Zone2, a.Zone3, a.Zone4
having count(a.ID_sick)>1)bb
on vv.ID_sick = bb.ID_sick and vv.ID_service = bb.ID_service
left join InfoSick sick on vv.ID_sick = sick.IDsick
left join infoService serv on vv.ID_service = serv.IDService
left join Infodentist dent on vv.ID_dentist = dent.IDdentist
left join infodentist do on vv.id_do = do.IDdentist
order by bb.ID_sick, bb.ID_service,vv.datevisit
But this code only returns rows with all tooths repeated. What I want is even one tooth repeats ...
How can I implement it?
I need to check characters in zones.
**Zone's datatype is varchar
This is a bad datamodel for what you are trying to do. By storing the teeth as a varchar, you have kind of decided that you are not interested in single teeth, but only in the group of teeth. Now, however, you are trying to investigate on single teeth.
You'd want a datamodel like this:
service
+------------+--------+-----------------+
| service_id | Name | RecievedService |
+------------+--------+-----------------+
| 1 | John | SomeService1 |
+------------+--------+-----------------+
| 3 | Steve | SomeService3 |
+------------+--------+-----------------+
| 4 | Steve | SomeService4 |
+------------+-------+-----------------+
service_detail
+------------+------+-------+
| service_id | zone | tooth |
+------------+------+-------+
| 1 | 1 | 1 |
| 1 | 1 | 3 |
| 1 | 3 | 4 |
+------------+------+-------+
| 1 | 1 | 3 |
| 1 | 1 | 4 |
+------------+------+-------+
| 3 | 4 | 2 |
+------------+------+-------+
| 4 | 4 | 1 |
| 4 | 4 | 2 |
+------------+------+-------+
What you can do with the given datamodel is to create such table on-the-fly using a recursive query and string manipulation:
with unpivoted(service_id, name, zone, teeth) as
(
select recievedservice, name, 1, firstzoneteeth
from mytable where len(firstzoneteeth) > 0
union all
select recievedservice, name, 2, secondzoneteeth
from mytable where len(secondzoneteeth) > 0
union all
select recievedservice, name, 3, thirdzoneteeth
from mytable where len(thirdzoneteeth) > 0
union all
select recievedservice, name, 4, fourthzoneteeth
from mytable where len(fourthzoneteeth) > 0
)
, service_details(service_id, name, zone, tooth, teeth) as
(
select
service_id, name, zone, substring(teeth, 1, 1), substring(teeth, 2, 10000)
from unpivoted
union all
select
service_id, name, zone, substring(teeth, 1, 1), substring(teeth, 2, 10000)
from service_details
where len(teeth) > 0
)
, duplicates(service_id, name) as
(
select distinct service_id, name
from service_details
group by service_id, name, zone, tooth
having count(*) > 1
)
select m.*
from mytable m
join duplicates d on d.service_id = m.recievedservice and d.name = m.name;
A lot of work and a rather slow query due to a bad datamodel, but still feasable.
Rextester demo: http://rextester.com/JVWK49901

selecting data with highest field value in a field

I have a table, and I'd like to select rows with the highest value. For example:
----------------
| user | index |
----------------
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
| 3 | 4 |
| 3 | 7 |
| 4 | 1 |
| 5 | 1 |
----------------
Expected result:
----------------
| user | index |
----------------
| 1 | 1 |
| 2 | 2 |
| 3 | 7 |
| 4 | 1 |
| 5 | 1 |
----------------
How may I do so? I assume it can be done by some oracle function I am not aware of?
Thanks in advance :-)
You can use MAX() function for that with grouping user column like this:
SELECT "user"
,MAX("index") AS "index"
FROM Table1
GROUP BY "user"
ORDER BY "user";
Result:
| USER | INDEX |
----------------
| 1 | 1 |
| 2 | 2 |
| 3 | 7 |
| 4 | 1 |
| 5 | 1 |
See this SQLFiddle
if you have more than one column
select user , index
from (
select u.* , row_number() over (partition by user order by index desc) as rnk
from some_table u)
where rnk = 1
user is a reserved word - you should use a different name for the column.
select user,max(index) index from tbl
group by user;
Alternatively, you can use analytic functions:
select user,index, max(index) over (partition by user order by 1 ) highest from YOURTABLE
Note: Try NOT to use words like user, index, date etc.. as your column names, as they are reserved words for Oracle. If you will use, then use them with quotation marks, eg. "index", "date"...