Postgres group by empty string question to include empty string in output - sql

I have following table in Postgres
| phone | group | spec |
| 1 | 1 | 'Lock' |
| 1 | 2 | 'Full' |
| 1 | 3 | 'Face' |
| 2 | 1 | 'Lock' |
| 2 | 3 | 'Face' |
| 3 | 2 | 'Scan' |
Tried this
SELECT phone, string_agg(spec, ', ')
FROM mytable
GROUP BY phone;
Need this ouput for each phone where there is empty string for missing group.
| phone | spec
| 1 | Lock, Full, Face
| 2 | Lock, '' , Face
| 3 | '', Scan ,''

You need a CTE which returns all possible combinations of phone and group and a left join to the table so you can group by phone:
with cte as (
select *
from (
select distinct phone from mytable
) m cross join (
select distinct "group" from mytable
) g
)
select c.phone, string_agg(coalesce(t.spec, ''''''), ',') spec
from cte c left join mytable t
on t.phone = c.phone and t."group" = c."group"
group by c.phone
See the demo.
Results:
| phone | spec |
| ----- | -------------- |
| 1 | Lock,Full,Face |
| 2 | Lock,'',Face |
| 3 | '',Scan,'' |

You can use conditional aggregation:
select phone,
(max(case when group = 1 then spec else '''''' end) || ', ' ||
max(case when group = 2 then spec else '''''' end) || ', ' ||
max(case when group = 3 then spec else '''''' end)
) as specs
from mytable t
group by phone;
Alternatively, you can general all the groups using generate_series() and then aggregation:
select p.phone,
string_agg(coalesce(t.spec, ''''''), ', ') as specs
from (select distinct phone from mytable) p cross join
generate_series(1, 3, 1) gs(grp) left join
mytable t
on t.phone = p.phone and t.group = gs.grp
group by p.phone

You can consider using a self - (RIGHT/LEFT)JOIN with all three distinct groups (which's stated within the subquery just after RIGHT JOIN keywords ) and a correlated query for your table :
WITH mytable1 AS
(
SELECT distinct t1.phone, t2."group",
( SELECT spec FROM mytable WHERE phone = t1.phone AND "group"=t2."group" )
FROM mytable t1
RIGHT JOIN ( SELECT distinct "group" FROM mytable ) t2
ON t2."group" = coalesce(t2."group",t1."group")
)
SELECT phone, string_agg(coalesce(spec,''''''), ', ') as spec
FROM mytable1
GROUP BY phone;
Demo

Related

SQL Server recursive query to show path of parents

I am working with SQL Server statements and have one table like:
| item | value | parentItem |
+------+-------+------------+
| 1 | 2test | 2 |
| 2 | 3test | 3 |
| 3 | 4test | 4 |
| 5 | 1test | 1 |
| 6 | 3test | 3 |
| 7 | 2test | 2 |
And I would like to get the below result using a SQL Server statement:
| item1 | value1 |
+-------+--------------------------+
| 1 | /4test/3test/2test |
| 2 | /4test/3test |
| 3 | /4test |
| 5 | /4test/3test/2test/1test |
| 6 | /4test/3test |
| 7 | /4test/3test/2test |
I didn't figure out the correct SQL to get all the values for all the ids according to parentItem.
I have tried this SQL :
with all_path as
(
select item, value, parentItem
from table
union all
select a.item, a.value, a.parentItem
from table a, all_path b
where a.item = b.parentItem
)
select
item as item1,
stuff(select '/' + value
from all_path
order by item asc
for xml path ('')), 1, 0, '') as value1
from
all_path
But got the "value1" column in result like
/4test/4test/4test/3test/3test/3test/3test/2test/2test/2test/2test
Could you please help me with that? Thanks a lot.
based on the expected output you gave, use the recursive part to concatenate the value
;with yourTable as (
select item, value, parentItem
from (values
(1,'2test',2)
,(2,'3test',3)
,(3,'4test',4)
,(5,'1test',1)
,(6,'3test',3)
,(7,'2test',2)
)x (item,value,parentItem)
)
, DoRecursivePart as (
select 1 as Pos, item, convert(varchar(max),value) value, parentItem
from yourTable
union all
select drp.pos +1, drp.item, convert(varchar(max), yt.value + '/' + drp.value), yt.parentItem
from yourTable yt
inner join DoRecursivePart drp on drp.parentItem = yt.item
)
select drp.item, '/' + drp.value
from DoRecursivePart drp
inner join (select item, max(pos) mpos
from DoRecursivePart
group by item) [filter] on [filter].item = drp.item and [filter].mpos = drp.Pos
order by item
gives
item value
----------- ------------------
1 /4test/3test/2test
2 /4test/3test
3 /4test
5 /4test/3test/2test/1test
6 /4test/3test
7 /4test/3test/2test
Here's the sample data
drop table if exists dbo.test_table;
go
create table dbo.test_table(
item int not null,
[value] varchar(100) not null,
parentItem int not null);
insert dbo.test_table values
(1,'test1',2),
(2,'test2',3),
(3,'test3',4),
(5,'test4',1),
(6,'test5',3),
(7,'test6',2);
Here's the query
;with recur_cte(item, [value], parentItem, h_level) as (
select item, [value], parentItem, 1
from dbo.test_table tt
union all
select rc.item, tt.[value], tt.parentItem, rc.h_level+1
from dbo.test_table tt join recur_cte rc on tt.item=rc.parentItem)
select rc.item,
stuff((select '/' + cast(parentItem as varchar)
from recur_cte c2
where rc.item = c2.item
order by h_level desc FOR XML PATH('')), 1, 1, '') [value1]
from recur_cte rc
group by item;
Here's the results
item value1
1 4/3/2
2 4/3
3 4
5 4/3/2/1
6 4/3
7 4/3/2

SQL join tables and CASE with CONCAT

I'm a SQL newb... and I need to join two tables (see below)
Table A
| id | Recipe_Web_Codes |
|----|--------------------|
| 1 | GF VGT |
| 2 | |
| 3 | VGN |
Table B
| id | Recipe_Web_Code | Webcode_Fullname | Color |
|----|-----------------|------------------------|---------|
| 1 | VGT | Vegetarian | #ff6038 |
| 2 | VGN | Vegan Friendly | #97002d |
| 3 | GF | Gluten Friendly | #6ca4b6 |
and produce the following table:
RESULT
| id | Recipe_Web_Codes | Wecode_Fullname | Color |
|-------------------------------------------------------|------------------|
| 1 | GF VGT | Gluten Friendly, Vegetarian | #6ca4b6, #ff6038 |
| 2 | | | |
| 3 | VGN | Vegan Friendly | #97002d |
I honestly don't know where to begin. I tried this but got stuck on how to concatenate case results into a single field. Am I on the right track?
select Recipe_Web_Codes, Webcode_Fullname =
case
when Recipe_Web_Codes like '%VGT%' then (select Webcode_Fullname FROM TABLE_B where Recipe_Web_Code = 'VGT')
when Recipe_Web_Codes like '%VGN%' then (select Webcode_Fullname FROM TABLE_B where Recipe_Web_Code = 'VGN')
when Recipe_Web_Codes like '%GF%' then (select Webcode_Fullname FROM TABLE_B where Recipe_Web_Code = 'GF')
end,
Color =
case
when Recipe_Web_Codes like '%VGT%' then (select Color FROM TABLE_B where Recipe_Web_Code = 'VGT')
when Recipe_Web_Codes like '%VGN%' then (select Color FROM TABLE_B where Recipe_Web_Code = 'VGN')
when Recipe_Web_Codes like '%GF%' then (select Color FROM TABLE_B where Recipe_Web_Code = 'GF')
end
from TABLE_A
EDIT: It just clicked on me that I missed a very important point as to why I need to aggregate these strings. The resulting table is going to be exported to JSON by another separate process, so no point to mention database normalization. Also, this is SQL 2016 SP2 so I don't have the fuction for String_agg available.
You should not be storing multiple values in a single column, so I would advise you to fix your data model.
That said, you can do what you want by pulling apart the strings and re-aggregating:
select a.*, b.*
from a outer apply
(select string_agg(b.Webcode_Fullname, ',') as Webcode_Fullname,
string_agg(b.Webcode_Fullname, ',') as Colors
from string_split(a.recipe_web_codes, ' ') s join
b
on s.value = b.Recipe_Web_Code
) b;
Very important: string_split() does not guarantee the ordering of the values. If the ordering of the resulting strings is important, you can handle this -- assuming you have no duplicates -- by using logic such as:
select a.*, b.*
from a outer apply
(select string_agg(b.Webcode_Fullname, ',') within group (order by charindex(b.Recipe_Web_Codeas, a.recipe_web_codes)) as Webcode_Fullname,
string_agg(b.Webcode_Fullname, ',') within group (order by charindex(b.Recipe_Web_Codeas, a.recipe_web_codes)) as Colors
from string_split(a.recipe_web_codes, ' ') s join
b
on s.value = b.Recipe_Web_Code
) b;
Let me emphasize again that you should put your effort into fixing your data model, by having a separate table for the recipe web codes, with one row per code.
EDIT:
One solution for older versions is a recursive CTE:
with bs as (
select b.*, row_number() over (order by id) as seqnum
from b
),
cte as (
select a.id, convert(varchar(max), Recipe_Web_Codes) as fullnames, convert(varchar(max), Recipe_Web_Codes) as colors, 1 as ind
from a
union all
select cte.id,
replace(cte.fullnames, bs.Recipe_Web_Code, bs.Webcode_Fullname),
replace(cte.colors, bs.Recipe_Web_Code, bs.color),
1 + cte.ind
from cte join
bs
on cte.ind = bs.seqnum and ind < 10
)
select cte.id, cte.fullnames, cte.colors
from (select cte.*, max(ind) over (partition by id) as max_ind
from cte
) cte
where ind = max_ind ;
Here is a db<>fiddle.
You can do it with a left join of the tables and the aggregate function string_agg() (works in SQL Server 2017+):
select a.id, a.Recipe_Web_Codes,
string_agg(b.Webcode_Fullname, ', ') Webcode_Fullname,
string_agg(b.Color, ', ') Color
from Table_A a left join Table_B b
on ' ' + a.Recipe_Web_Codes + ' ' like '% ' + b.Recipe_Web_Code + ' %'
group by a.id, a.Recipe_Web_Codes
order by a.id
See the demo.
Results:
> id | Recipe_Web_Codes | Webcode_Fullname | Color
> -: | :--------------- | :-------------------------- | :---------------
> 1 | GF VGT | Vegetarian, Gluten Friendly | #ff6038, #6ca4b6
> 2 | null | null | null
> 3 | VGN | Vegan Friendly | #97002d

MySQL - Grouping using MIN not working as expected

I have the following union query where i'm setting a dummy row number:
SELECT #row_num:=2 as row_num, id, name, admin1, admin3, admin4
FROM locations
WHERE feature_class IN ('P', 'A')
AND name LIKE 'cornwall%'
UNION ALL
SELECT #row_num:=1 as row_num, id, name, admin1, admin3, admin4
FROM locations
WHERE feature_class = 'L' AND feature_code = 'RGN'
AND name LIKE 'cornwall%'
This returns the following results:
row_num | id | name | admin1 | admin3 | admin4
-------------------------------------------------------------------------
2 | 2652355 | Cornwall | ENG | |
1 | 11609029 | Cornwall | ENG | |
Now when I add this query as a subquery and use MIN to select row with the lowest row_num I get incorrect results:
SELECT MIN(t0.row_num),
t0.id,
t0.name,
t4.name AS town_admin4,
t3.name AS county_admin3,
t1.name AS admin1
FROM (
SELECT #row_num:=2 as row_num, id, name, admin1, admin3, admin4
FROM locations
WHERE feature_class IN ('P', 'A')
AND name LIKE 'cornwall%'
UNION ALL
SELECT #row_num:=1 as row_num , id, name, admin1, admin3, admin4
FROM locations
WHERE feature_class = 'L' AND feature_code = 'RGN'
AND name LIKE 'cornwall%'
) t0
LEFT JOIN locations t1 ON t1.admin1 = t0.admin1 AND t1.feature_code = 'ADM1'
LEFT JOIN locations t3 ON t3.admin3 = t0.admin3 AND t3.feature_code = 'ADM3'
LEFT JOIN locations t4 ON t4.admin4 = t0.admin4 AND t4.feature_code = 'ADM4'
GROUP BY t0.name,
t4.name,
t3.name,
t1.name
ORDER BY t0.name
I get the following results:
MIN(t0.row_num) | id | name | town_admin4 | county_admin3 | admin1
--------------------------------------------------------------------------------------------
1 | 2652355 | Cornwall | | | England
As you can see the above is incorrect. Row 1 should be record with id 11609029
Why is it behaving like this - why is the MIN operator not working as expected?

SQL Server Query to find records with aggregate funct on one column but multiple columns in select clause

Here is the minimized version of the Customer table. There can be customers having same account number mapped to different Group . I am looking to find out customer numbers which are mapped to more than one group. As I was using sybase my query below was working fine. Same query does not work in SQL Server.
Can I get both custAccnt and corresponding custId in one query as below.
select DISTINCT lt.custAccnt, lt.custId from VAL_CUSTOMERS lt
where lt.eligible = 'Y' group by lt.custAccnt
having count(distinct lt.custId) > 1
+----------+-----------+---------+----------+
| custName | custAccnt | custId | eligible |
+----------+-----------+---------+----------+
| Joe | AB1VU1235 | 43553 | Y |
| Joe | AB1VU1235 | 525577 | Y |
| Lucy | CDNMY4568 | 332875 | Y |
| Lucy | CDNMY4568 | 211574 | Y |
| Lucy | CDNMY4568 | 211345 | Y |
| Manie | TZMM7S009 | 123890 | Y |
| Tom | YFDU1235 | 1928347 | Y |
| Tom | YFDU1235 | 204183 | Y |
| Chef | TNOTE6573 | 734265 | Y |
+----------+-----------+---------+----------+
Result :-
+-----------+---------+
| AB1VU1235 | 43553 |
| AB1VU1235 | 525577 |
| CDNMY4568 | 332875 |
| CDNMY4568 | 211574 |
| CDNMY4568 | 211345 |
| YFDU1235 | 1928347 |
| YFDU1235 | 204183 |
+-----------+---------+
There are many ways to tackle this. Here are a couple of them that should work.
select lt.custAccnt
, lt.custId
from VAL_CUSTOMERS lt
cross apply
(
select c.custAccnt
from VAL_CUSTOMERS c
where c.custAccnt = lt.custAccnt
group by c.custAccnt
having count(*) > 1
) x
where lt.eligible = 'Y'
select lt.custAccnt
, lt.custId
from VAL_CUSTOMERS lt
where lt.eligible = 'Y'
AND lt.custAccnt IN
(
select c.custAccnt
from VAL_CUSTOMERS c
group by c.custAccnt
having count(*) > 1
)
In case of duplicates custAccnt and custId in the table, #Sean query won't work.
WITH cte AS(SELECT *
, COUNT (custId) OVER (PARTITION BY custAccnt) AS CntcustId
, ROW_NUMBER () OVER (PARTITION BY custAccnt, custId ORDER BY custName) AS Rownum
FROM VAL_CUSTOMERS
WHERE eligible = 'Y'
)
SELECT custAccnt, custId
FROM cte
WHERE CntcustId>1
AND Rownum = 1;
Using row number to eliminate the duplicates.
I think this might work...
"...customer numbers which are mapped to more than one group..." , <-- group is custAcct?
select t.custAccnt, t.custId
from VAL_CUSTOMERS t
where (Select count(distinct custAccnt )
from VAL_CUSTOMERS
Where custId = t.custId) > 1
The statement "...customer numbers which are mapped to more than one group..." does not say anything about "eligibility", so I did not mention it. If you really meant to say:
"...eligible customer numbers which are mapped to more than one group...", then try this:
select t.custAccnt, t.custId
from VAL_CUSTOMERS t
where eligible = 'Y'
and (Select count(distinct custAccnt )
from VAL_CUSTOMERS
Where custId = t.custId) > 1
or, this might be faster... it answers a slightly different, but, (I think) equivalent question,
"find ...eligible customer numbers where there is another row for the same customer number mapped to a different custAccnt ..."
select t.custAccnt, t.custId
from VAL_CUSTOMERS t
where eligible = 'Y'
and exists
(Select * from VAL_CUSTOMERS
Where custId = t.custId
and custAccnt != t.custAccnt )
;WITH cte1
( custName , custAccnt , custId , eligible )
As
(
SELECT 'Joe' ,'AB1VU1235' , 43553 , 'Y' UNION ALL
SELECT 'Joe' ,'AB1VU1235' , 525577 , 'Y' UNION ALL
SELECT 'Lucy' ,'CDNMY4568' , 332875 , 'Y' UNION ALL
SELECT 'Lucy' ,'CDNMY4568' , 211574 , 'Y' UNION ALL
SELECT 'Lucy' , 'CDNMY4568' , 211345 , 'Y' UNION ALL
SELECT 'Manie' ,'TZMM7S009' , 123890 , 'Y' UNION ALL
SELECT 'Tom' ,'YFDU1235' , 1928347 , 'Y' UNION ALL
SELECT 'Tom' ,'YFDU1235' , 204183 , 'Y' UNION ALL
SELECT 'Chef' ,'TNOTE6573' , 734265 , 'Y'
)
,cte2 AS (
SELECT custName
,custAccnt
,count(custName) cnt
FROM cte1
GROUP BY custName,custAccnt
)
,cte3 AS (
SELECT custName
,cnt
FROM cte2 WHERE cnt <> 1
)
SELECT custAccnt
,custId
FROM cte1
WHERE custName IN (
SELECT custName
FROM cte3
)

how to query range?

Raw Data
| ID | STATUS |
| 1 | A |
| 2 | A |
| 3 | B |
| 4 | B |
| 5 | B |
| 6 | A |
| 7 | A |
| 8 | A |
| 9 | C |
Result
| START | END |
| 1 | 2 |
| 6 | 8 |
Range of STATUS A
How to query ?
This should give you the correct ranges:
SELECT
STATUS,
MIN(ID),
max_id
FROM (
SELECT
t1.STATUS,
t1.ID,
COALESCE(MAX(t2.ID), t1.ID) max_id
FROM
yourtable t1 LEFT JOIN yourtable t2
ON t1.STATUS=t2.STATUS AND t1.ID<t2.ID
WHERE
NOT EXISTS (SELECT NULL
FROM yourtable t3
WHERE
t3.STATUS!=t1.STATUS
AND t3.ID>t1.ID AND t3.ID<t2.ID)
GROUP BY
t1.ID,
t1.STATUS
) s
WHERE
status = 'A'
GROUP BY
STATUS,
max_id
Please see fiddle here.
You are probably better off with a cursor-based solution or a client-side function.
However, if you were using Oracle - the following would work.
WITH LOWER_VALS AS
( -- All the Ids with no immediate predecessor
SELECT ROWNUM AS RN, STATUS, ID AS LOWER FROM
(
SELECT STATUS, ID
FROM RAWDATA RD1
WHERE RD1.ID -1 NOT IN
(SELECT ID FROM RAWDATA PRED_TABLE WHERE PRED_TABLE.STATUS = RD1.STATUS)
ORDER BY STATUS, ID
)
) ,
UPPER_VALS AS
( -- All the Ids with no immediate successor
SELECT ROWNUM AS RN, STATUS, ID AS UPPER FROM
(
SELECT STATUS, ID
FROM RAWDATA RD2
WHERE RD2.ID +1 NOT IN
(SELECT ID FROM RAWDATA SUCC_TABLE WHERE SUCC_TABLE.STATUS = RD2.STATUS)
ORDER BY STATUS, ID
)
)
SELECT
L.STATUS, L.LOWER, U.UPPER
FROM
LOWER_VALS L
JOIN UPPER_VALS U ON
U.RN = L.RN;
Results in the set
A 1 2
A 6 8
B 3 5
C 9 9
http://sqlfiddle.com/#!4/10184/2
There is not a lot to go on from what you put, but I think this might work. I am using T-SQL because I don't know what you are using?
SELECT
min(ID)
, max(ID)
FROM RawData
WHERE [Status] = 'A'