pgsql 1 to n relation into json - sql

Long story short, how can i use 1 to n select data to build json like shown in example:
SELECT table1.id AS id1,table2.id AS id2,t_id,label
FROM table1 LEFT JOIN table2 ON table2.t_id = table1.id
result
|id1|id2|t_id|label|
+---+---+----+-----+
|1 | 1 | 1 | a |
| | 2 | 1 | b |
| | 3 | 1 | c |
| | 4 | 1 | d |
|2 | 5 | 2 | x |
| | 6 | 2 | y |
turn into this
SELECT table1.id, build_json(table2.id,table2.label) AS json_data
FROM table1 JOIN table2 ON table2.t_id = table1.id
GROUP BY table1.id
|id1|json_data
+--+-----------------
|1 |{"1":"a","2":"b","3":"c","4":"d"}
|2 |{"5":"x","6":"y"}
My guess the best start woulb be building an array from columns
Hstore instead of json would be ok too

well your table structure is a bit strange (is looks more like report than table), so I see two tasks here:
Replace nulls with correct id1. You can do it like this
with cte1 as (
select
sum(case when id1 is null then 0 else 1 end) over (order by t_id) as id1_partition,
id1, id2, label
from Table1
), cte2 as (
select
first_value(id1) over(partition by id1_partition) as id1,
id2, label
from cte1
)
select *
from cte2
Now you have to aggregate data into json. As far as I remember, there's no such a function in PostgreSQL, so you have to concatenate data manually:
with cte1 as (
select
sum(case when id1 is null then 0 else 1 end) over (order by t_id) as id1_partition,
id1, id2, label
from Table1
), cte2 as (
select
first_value(id1) over(partition by id1_partition) as id1,
id2, label
from cte1
)
select
id1,
('{' || string_agg('"' || id2 || '":' || to_json(label), ',') || '}')::json as json_data
from cte2
group by id1
sql fiddle demo
And if you want to convert into hstore:
with cte1 as (
select
sum(case when id1 is null then 0 else 1 end) over (order by t_id) as id1_partition,
id1, id2, label
from Table1
), cte2 as (
select
first_value(id1) over(partition by id1_partition) as id1,
id2, label
from cte1
)
select
c.id1, hstore(array_agg(c.id2)::text[], array_agg(c.label)::text[])
from cte2 as c
group by c.id1
sql fiddle demo

Related

Redshift, count items in column seperated with comma

I have data that a column saved a group of number
| user | col |
| ------- | ------- |
| 1 | 3,7,11,25,44,56,77,32,34,55 |
| 2 | 3,7,25,44,37,89,56,99,103,13 |
| 1 | 3,10,11,25,44,56,33,32,34,55 |
I know I can split part the columns and count but do we have any different way to count the numbers?
|user| new-col | count|
| ------- | ------- |
| 1 | 3 | 2 |
| 1 | 7 | 1 |
| 1 | 11 | 2 |
| 1 | 25 | 2 |
| 1 | 44 |2 |
| 1 | 56 |1 |
| 1 | 77 | 1 |
| 1 | 32 | 2 |
You could use a union query along with SPLIT_PART:
WITH cte AS (
SELECT user, SPLIT_PART(col, ',', 1) AS val FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 2) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 3) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 4) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 5) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 6) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 7) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 8) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 9) FROM yourTable UNION ALL
SELECT user, SPLIT_PART(col, ',', 10) FROM yourTable
)
SELECT
user,
val,
COUNT(*) AS cnt
FROM cte
GROUP BY
user,
val;
But note that all we are doing above in the CTE is really just normalizing your data so that each user-value relationship occupies a separate record. Ideally you should change your table design and move away from storing CSV.
If you instead want just the count of numbers per user, then use:
SELECT
user,
COUNT(*) AS cnt
FROM cte
GROUP BY
user;
Query.
with t as (
select 1 as user, '3,7,11,25,44,56,77,32,34,55' as col
union all
select 2 as user, '3,7,25,44,37,89,56,99,103,13' as col
union all
select 1 as user, '3,10,11,25,44,56,33,32,34,55' as col
)
select a.user, a.val, count(*) as cnt
from (
select a.user
, SPLIT_PART(a.col, ',', b.no) as val
from t a
cross join (
select * from generate_series(1,10) as no
) b
) a
group by a.user, a.val
order by a.user, a.val
Count the number of commas in the string using REGEXP_COUNT and add 1.
CREATE TEMP TABLE examples (
user_id INT
, value_list VARCHAR
);
INSERT INTO examples
SELECT 1 , '3,7,11,25,44,56,77,32,34,55'
UNION ALL SELECT 2 , '3,7,25,44,37,89,56,99,103,13'
UNION ALL SELECT 1 , '3,10,11,25,44,56,33,32,34,55'
;
SELECT user_id
, SUM(REGEXP_COUNT(value_list,',')+1) value_count
FROM examples
GROUP BY 1
;
Output
user_id | value_count
---------+-------------
1 | 20
2 | 10
This answers the original version of the question.
You can count the number of comma-delimited values with:
select (case when col = '' then 0
else length(col) - length(replace(col, ',', '')) + 1
end) as values_count
from t;
That said, you should fix your data model so you are not storing multiple values in a column. It is particularly irksome that you are storing numbers as strings, as well. You want a junction/association table.

SQL - Left Join many-to-many only once

I have a two tables that are setup like the following examples
tablea
ID | Name
1 | val1
1 | val2
1 | val3
2 | other1
3 | other
tableb
ID | Amount
1 | $100
2 | $50
My desired output would be to left join tableb to tablea but only join tableb once on each value. ID is the only relationship
tablea.ID | tablea.Name | tableb.id | tableb.amount
1 | val1 | 1 | $100
1 | val2
1 | val3
2 | other1 | 2 | $50
3 | other
Microsoft SQL
You can do the following:
select ROW_NUMBER() OVER(ORDER BY RowID ASC) as RowNum, ID , Name
from tablea
which gives you :
RowNum | RowID | Name
1 | 1 | val1
2 |1 | val2
3 |1 | val3
4 |2 | other1
5 |3 | other
You then get the minimum row number for each RowID:
Select RowId, min(RowNum)
From (
select ROW_NUMBER() OVER(ORDER BY RowID ASC) as RowNum, ID , Name
from tablea )
Group By RowId
Once you have this you can then join tableb onto tablea only where the RowId is the minimum
WITH cteTableA As (
select ROW_NUMBER() OVER(ORDER BY RowID ASC) as RowNum, ID , Name
from tablea ),
cteTableAMin As (
Select RowId, min(RowNum) as RowNumMin
From cteTableA
Group By RowId
)
Select a.RowID, a.Name, b.Amount
From cteTableA a
Left join cteTableAMin amin on a.RowNum = amin.RowNumMin
and a.ID = amin.RowId
Left join tableb b on amin.ID = b.ID
This can be tidied up... but helps to show whats going on.
Then you MUST specify which row in tableA you wish to join to. If there are more than one row in the other table, How can the query processor know which one you want ?
If you want the one with the lowest value of name, then you might do this:
Select * from tableB b
join tableA a
on a.id = b.Id
and a.name =
(Select min(name) from tableA
where id = b.id)
but even that won't work if there multiple rows with the same values for both id AND name. What you might really need is a Primary Key on tableA.
Use:
select
a.id,
a.name,
b.amount
from
(select
id,
name,
row_number() over (partition by id order by name) as rn
from tablea) a
left join (
select
id,
amount,
row_number() over (partition by id order by amount) as rn
from tableb) b
on a.id = b.id
and a.rn = b.rn
order by a.id, a.name

How to comapre two columns of a table in sql?

In a table there are two columns:
-----------
| A | B |
-----------
| 1 | 5 |
| 2 | 1 |
| 3 | 2 |
| 4 | 1 |
-----------
Want a table where if A=B then
-------------------
|Match | notMatch|
-------------------
| 1 | 5 |
| 2 | 3 |
| Null | 4 |
-------------------
How can i do this?
I tried something which shows the Matched part
select distinct C.A as A from Table c inner join Table d on c.A=d.B
Try this:
;WITH TempTable(A, B) AS(
SELECT 1, 5 UNION ALL
SELECT 2, 1 UNION ALL
SELECT 3, 2 UNION ALL
SELECT 4, 1
)
,CTE(Val) AS(
SELECT A FROM TempTable UNION ALL
SELECT B FROM TempTable
)
,Match AS(
SELECT
Rn = ROW_NUMBER() OVER(ORDER BY Val),
Val
FROM CTE c
GROUP BY Val
HAVING COUNT(Val) > 1
)
,NotMatch AS(
SELECT
Rn = ROW_NUMBER() OVER(ORDER BY Val),
Val
FROM CTE c
GROUP BY Val
HAVING COUNT(Val) = 1
)
SELECT
Match = m.Val,
NotMatch= n.Val
FROM Match m
FULL JOIN NotMatch n
ON n.Rn = m.Rn
Try with EXCEPT, MINUS and INTERSECT Statements.
like this:
SELECT A FROM TABLE1 INTERSECT SELECT B FROM TABLE1;
You might want this:
SELECT DISTINCT
C.A as A
FROM
Table c
LEFT OUTER JOIN
Table d
ON
c.A=d.B
WHERE
d.ID IS NULL
Please Note that I use d.ID as an example because I don't see your schema. An alternate is to explicitly state all d.columns IS NULL in WHERE clause.
Your requirement is kind of - let's call it - interesting. Here is a way to solve it using pivot. Personally I would have chosen a different table structure and another way to select data:
Test data:
DECLARE #t table(A TINYINT, B TINYINT)
INSERT #t values
(1,5),(2,1),
(3,2),(4,1)
Query:
;WITH B AS
(
( SELECT A FROM #t
EXCEPT
SELECT B FROM #t)
UNION ALL
( SELECT B FROM #t
EXCEPT
SELECT A FROM #t)
), A AS
(
SELECT A val
FROM #t
INTERSECT
SELECT B
FROM #t
), combine as
(
SELECT val, 'A' col, row_number() over (order by (select 1)) rn FROM A
UNION ALL
SELECT A, 'B' col, row_number() over (order by (select 1)) rn
FROM B
)
SELECT [A], [B]
FROM combine
PIVOT (MAX(val) FOR [col] IN ([A], [B])) AS pvt
Result:
A B
1 3
2 4
NULL 5

tSQL UNPIVOT of comma concatenated column into multiple rows

I have a table that has a value column. The value could be one value or it could be multiple values separated with a comma:
id | assess_id | question_key | item_value
---+-----------+--------------+-----------
1 | 859 | Cust_A_1 | 1,5
2 | 859 | Cust_B_1 | 2
I need to unpivot the data based on the item_value to look like this:
id | assess_id | question_key | item_value
---+-----------+--------------+-----------
1 | 859 | Cust_A_1 | 1
1 | 859 | Cust_A_1 | 5
2 | 859 | Cust_B_1 | 2
How does one do that in tSQL on SQL Server 2012?
We have a user defined function that we use for stuff like this that we called "split_delimiter":
CREATE FUNCTION [dbo].[split_delimiter](#delimited_string VARCHAR(8000), #delimiter_type CHAR(1))
RETURNS TABLE AS
RETURN
WITH cte10(num) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
,cte100(num) AS
(
SELECT 1
FROM cte10 t1, cte10 t2
)
,cte10000(num) AS
(
SELECT 1
FROM cte100 t1, cte100 t2
)
,cte1(num) AS
(
SELECT TOP (ISNULL(DATALENGTH(#delimited_string),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM cte10000
)
,cte2(num) AS
(
SELECT 1
UNION ALL
SELECT t.num+1
FROM cte1 t
WHERE SUBSTRING(#delimited_string,t.num,1) = #delimiter_type
)
,cte3(num,[len]) AS
(
SELECT t.num
,ISNULL(NULLIF(CHARINDEX(#delimiter_type,#delimited_string,t.num),0)-t.num,8000)
FROM cte2 t
)
SELECT delimited_item_num = ROW_NUMBER() OVER(ORDER BY t.num)
,delimited_value = SUBSTRING(#delimited_string, t.num, t.[len])
FROM cte3 t;
GO
It will take a varchar value up to 8000 characters and will return a table with the delimited elements broken into rows. In your example, you'll want to use an outer apply to turn those delimited values into separate rows:
SELECT my_table.id, my_table.assess_id, question_key, my_table.delimited_items.item_value
FROM my_table
OUTER APPLY(
SELECT delimited_value AS item_value
FROM my_database.dbo.split_delimiter(my_table.item_value, ',')
) AS delimited_items

Insert data into temp table from 2 source tables

I have 2 SELECT statements that both return 13 rows from dirrefernt tables
I would like to create 1 temporary table with 2 columns and insert the 2 result rows into the 2 columns. Is there a way to do this?
So
1 - SELECT INPOS FROM TABLE1 returns
1,2,3,4,5,6,7,18,9,10,11,12,13
2 - SELECT CODE FROM TABLE2 returns
CODEA,CODEB,CODEC,CODED,CODEE,CODEF,CODEG,CODEH,CODEI,CODEJ,CODEK,CODEL,CODEM
I would like my temporary table to be
1 | CODEA
2 | CODEB
3 | CODEC
4 | CODED
5 | CODEE
6 | CODEF
7 | CODEG
8 | CODEH
9 | CODEI
10 | CODEJ
11 | CODEK
12 | CODEL
13 | CODEM
Try this:
WITH T1 AS (
SELECT ROW_NUMBER() OVER(ORDER BY INPOS) ID, INPOS FROM TABLE1
),
WITH T2 AS
(
SELECT ROW_NUMBER() OVER(ORDER BY CODE) ID, CODE FROM TABLE2
),
SELECT T1.INPOS, T2.CODE
FROM T1 INNER JOIN T2 ON T1.ID = T2.ID
Try something like this:
SELECT a.impos, b.code
FROM (
(
SELECT impos, RANK() OVER (ORDER BY impos ASC) AS link
FROM table1
) AS a INNER JOIN (
SELECT code, RANK() OVER (ORDER BY code ASC) AS link
FROM table2
) AS b ON a.link = b.link
)
sqlfiddle demo