I have these tables:
names
id | name
7 | 'a'
8 | 'b'
9 | 'c'
group_names
id | group_of_names
1 | '7'
2 | '9,8'
3 | '7,8,9'
how to build a select that returns their names instead, separated by semicolon, preserving the original order as in:
id | single_text
1 | 'a'
2 | 'c;b'
3 | 'a;b;c'
I managed to do
select g.id, string_to_array(g.group_of_names,',')::int[] from group_names g;
id | string_to_array
1 | {7}
2 | {9,8}
3 | {7,8,9}
but i don't know how to, returning several arrays, for each of them, concatenate texts based on their ids
If the order of resulting strings is irrelevant:
select g.id, string_agg(n.name, ';')
from group_names g
join names n
on n.id = any(string_to_array(g.group_of_names, ',')::int[])
group by g.id
otherwise:
select g.id, string_agg(n.name, ';' order by ord)
from names n
join (
select id, elem, ord
from group_names
cross join regexp_split_to_table(group_of_names, ',')
with ordinality as arr(elem, ord)
) g
on n.id = g.elem::int
group by g.id
Test it in db<>fiddle.
In Postgres 14+ you can use string_to_table() instead of regexp_split_to_table().
You can try this way: Using the ANY operator to check if n.id value is in the array of group_of_names.
SELECT gn.id, string_agg(n.name, ';') AS single_text
FROM names n
INNER JOIN group_names gn ON n.id::text = ANY(string_to_array(gn.group_of_names, ','))
GROUP BY gn.id
ORDER BY gn.id;
Or this way: using your query and unnest() function to expand an array to a set of rows.
SELECT gn.id, string_agg(n.name, ';')
FROM names n
INNER JOIN (SELECT g.id, unnest(string_to_array(g.group_of_names, ',')::int[]) AS name_id
FROM group_names g) AS gn ON n.id = gn.name_id
GROUP BY gn.id
ORDER BY gn.id;
SELECT g.id, string_agg(n.name,';') AS single_text
FROM group_names AS g
CROSS JOIN LATERAL regexp_split_to_table (g.group_of_names, ',') AS gn(element)
INNER JOIN names AS n
ON n.id :: text = gn.element
GROUP BY g.id
Related
I'm working on a query to return a report that will group log records per business unit along with a comma separated list of managers assigned to each business unit, so basically there are 4 tables.
[dbo].[Log]
[dbo].[BusinessUnit]
[dbo].[Manager]
[dbo].[BusinessUnitManager]
BusinessUnit, Manager, and BusinessUnitManager for the sake of the report are only tables to gain access to basic info, the real info I need to group is contained in the Log table, my final result set should look like this:
BusinessUnit.Name,
Log.Code,
Count(*) LogRecordsCount,
Emails (Comma separated list of Managers assigned to the Business Unit)
I've been trying to get the comma separated column using CROSS APPLY but can't get it working, my current query looks like this:
SELECT
L.BusinessUnit,
L.Code,
COUNT(*) AS RecordsCount,
F.Emails
FROM [dbo].[LOG] AS L
INNER JOIN [dbo].[BusinessUnit] AS D ON L.BusinessUnit = D.ID
INNER JOIN [dbo].[BusinessUnitManager] AS DSO ON D.ID = DSO.BusinessUnit
CROSS APPLY
(
SELECT STUFF((
SELECT ',' + O.Email
FROM [dbo].[Manager] AS O
WHERE O.Id = DSO.IdManager
ORDER BY O.Email ASC
FOR XML PATH ('')), 1, 1, '')
) F(Emails)
GROUP BY L.BusinessUnit, L.Code, F.Emails
The problem with this query is returning duplicated rows, one row per each manager email assigned to the business unit and my expected result should be only one row with a column containing a comma separated list of all managers email.
Current result:
+--------------+------+--------------+----------------------------+
| BusinessUnit | Code | RecordsCount | Emails |
+--------------+------+--------------+----------------------------+
| Americas | 00 | 21 | americasmanager1#email.com |
| Americas | 00 | 21 | americasmanager2#email.com |
| Asia | 10 | 5 | asiamanager1#email.com |
| Asia | 10 | 5 | asiamanager2#email.com |
+--------------+------+--------------+----------------------------+
Expected result:
+--------------+------+--------------+-------------------------------------------------------+
| BusinessUnit | Code | RecordsCount | Emails |
+--------------+------+--------------+-------------------------------------------------------+
| Americas | 00 | 21 | americasmanager1#email.com,americasmanager2#email.com |
| Asia | 10 | 5 | asiamanager1#email.com,asiamanager2#email.com |
+--------------+------+--------------+-------------------------------------------------------+
I'll appreciate your help
You are close, just do this in two steps:
1) store your stuff into a temp table
SELECT
L.BusinessUnit,
L.Code,
O.Emails
into #tmp
FROM [dbo].[LOG] AS L
INNER JOIN [dbo].[BusinessUnit] AS D ON L.BusinessUnit = D.ID
INNER JOIN [dbo].[BusinessUnitManager] AS DSO ON D.ID = DSO.BusinessUnit
JOIN [Manager] AS O ON O.Id = DSO.IdManager
GROUP BY L.BusinessUnit, L.Code, O.Emails
2) use outer apply on yourself to stuff emails in one column
select BusinessUnit,Code, F.Emails from #tmp t
outer apply(
SELECT STUFF(( SELECT ',' + inner.Email
FROM #tmp inner
WHERE t.Id = inner.
ORDER BY O.Email ASC
FOR XML PATH ('')), 1, 1, '')
) F(Emails)
group by BusinessUnit,Code
SELECT
L.BusinessUnit,
L.Code,
COUNT(*) AS RecordsCount,
(
SELECT STUFF((
SELECT ',' + O.Email
FROM [dbo].[Manager] AS O
INNER JOIN [dbo].[BusinessUnitManager] AS DSO ON O.Id = DSO.IdManager
WHERE
DSO.BusinessUnit = D.ID
ORDER BY O.Email ASC
FOR XML PATH ('')), 1, 1, '')
) as Emails
FROM [dbo].[LOG] AS L
INNER JOIN [dbo].[BusinessUnit] AS D ON L.BusinessUnit = D.ID
GROUP BY L.BusinessUnit, L.Code, D.ID
I have 2 connected tables: Producto and Productos_ProductosRelacionados
Producto Productos_ProductosRelacionados
|id|referencia| |id|idProducto|idProductoRelacionado|
|1 | A | |1 | 1 | 2 |
|2 | B | |2 | 1 | 3 |
|3 | C | |3 | 3 | 4 |
|4 | D | |4 | 3 | 5 |
|5 | E |
I need this:
|idProducto|referencia|
| 1 | B,C |
| 2 | |
| 3 | D,E |
I have older SQL server so can not use STRING_AGG.
So far I only achieve concatenating idProductoRelacionado:
|idProducto|idProductoRelacionado|
| 1 | 2,3 |
with:
SELECT pr1.idProducto
,STUFF((
SELECT ',' + CONVERT(varchar, pr.idProductoRelacionado)
FROM [Productos_ProductosRelacionados] as pr
WHERE pr.idProducto = pr1.idProducto
FOR XML PATH('')), 1, 1, '') as RelacionadosID
FROM [dbo].[Producto] as p1
join [Productos_ProductosRelacionados] as pr1 on p1.id = pr1.idProductoRelacionado
GROUP BY pr1.idProducto
If I try the same approach to concatenate referencia column it gives me: "Column 'dbo.Producto.id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause."
select pr1.idProducto
,STUFF((
SELECT ',' + p.referencia
FROM [dbo].[Producto] as p
WHERE p.id = p1.id
FOR XML PATH('')), 1, 1, '') as RelacionadosREF
from [dbo].[Producto] as p1
join [Productos_ProductosRelacionados] as pr1 on p1.id = pr1.idProductoRelacionado
GROUP BY pr1.idProducto
I do not understand the difference between 2 queries, why first one is working and second is not.
Simple Join should work. Check SQLFiddle
SELECT
id,
(SELECT
cast (p1.referencia as varchar(100)) + ','
FROM producto p
LEFT JOIN Productos_ProductosRelacionados pr
on pr.idProduct = p.id
LEFT JOIN producto p1
on p1.id = pr.idProductoRelacionado
WHERE p.id = src.id
FOR XML PATH('')) as referencia
FROM
producto src;
I ran into this a while back. You have to add a join to your stuff statement:
create table #prod (
id int,
ref varchar(1)
)
insert into #prod values
(1,'A'),
(2,'B'),
(3,'C'),
(4,'D'),
(5,'E')
create table #prod_rel (
id int,
pid int,
id_rel int
)
insert into #prod_rel values
(1,1,2),
(2,1,3),
(3,3,4),
(4,3,5)
select distinct
pid
,STUFF(
(SELECT ',' + c.ref FROM #prod_rel b inner join #prod c on c.id = b.id_rel where b.pid = a.pid FOR XML PATH ('')), 1, 1, ''
)
from #prod_rel a
drop table #prod
,#prod_rel
You can use recursive query method, like this:
--Create tables for example
select * into #Producto from (
select 1 id,'A' referential union all select 2,'B' union all select 3,'C' union all select 4,'D' union all select 5,'E'
) tmp;
select * into #Productos_ProductosRelacionados from (
select 1 id,1 idProducto,2 idProductoRelacionado union all select 2,1,3 union all select 3,3,4 union all select 4,3,5
) tmp;
-- Recurse query
With tmp as (
select ROW_NUMBER() over(partition by f1.ID order by f1.id, f3.referential desc) RangID,
count(*) over(partition by f1.ID order by f1.id) NbID,
f1.id, f3.referential
from #Producto f1
left outer join #Productos_ProductosRelacionados f2 on f1.id=f2.idProducto
left outer join #Producto f3 on f2.idProductoRelacionado=f3.id
),
Recurse as (
select f1.id, f1.RangID, f1.NbID, cast(f1.referential as varchar(2000)) referential, 1 rangrecur from tmp f1 where RangID=1
union all
select f1.id, f1.RangID, f1.NbID, cast(isnull(f1.referential, '') + ',' + isnull(f2.referential, '') as varchar(2000)) referential, f2.rangrecur + 1 as rangrecur
from tmp f1 inner join Recurse f2 on f1.id=f2.id and f1.RangID-1=f2.RangID
)
select ID, Referential from recurse
where NbID=rangrecur
order by ID;
I have two tables. One holds Objects and the other holds Settings about each object. Not all of the rows in the Objects table have a corresponding row in the Settings table. There is a special row in the Settings table that is supposed to be used for the "Other" objects.
How can I create a join between Objects and Settings such that I get the given setting if there is one or the "Other" setting if there isn't?
For example consider the following script:
CREATE TABLE #Objects (Code nvarchar(20) not null);
CREATE TABLE #Settings (Code nvarchar(20) not null, Value int not null);
INSERT INTO #Objects
VALUES
('A'),
('B'),
('D')
INSERT INTO #Settings
VALUES
('A', 1),
('B', 2),
('C', 3),
('Other', 4)
SELECT
#Objects.Code,
#Settings.Value
FROM
#Objects
JOIN #Settings
ON #Objects.Code = #Settings.Code
OR #Settings.Code = 'Other'
DROP TABLE #Settings, #Objects
I'm wanting to get this:
Code | Value
---- | -----
A | 1
B | 2
D | 4
What I'm actually getting is:
Code | Value
----- | -----
A | 1
A | 4
B | 2
B | 4
D | 4
You can do this with an APPLY:
SELECT o.Code, s.Value
FROM #Objects o
CROSS APPLY (
SELECT TOP 1 *
FROM #Settings s
WHERE s.Code = o.Code or s.Code = 'Other'
ORDER BY case when s.Code = o.Code then 0 else 1 end
) s
For fun: a hybrid from answers by Gurv, jyao and SqlZim, which are all variations on the same basic theme:
SELECT o.Code, s2.Value
FROM #Objects o
LEFT JOIN #Settings s1 on s1.Code = o.Code
INNER JOIN #Settings s2 on s2.Code = coalesce(s1.Code, 'Other')
So far, this approach (LEFT JOIN + the INNER JOIN ON COALESCE() ) is my favorite option.
Note that this only works if there can be only one Settings record per Object record. If that ever changes, the APPLY answer still works, but other answers here might not work.
Another way is to use CTE to add an additional column [Alternative_code] for [#Object] table that has value "Other" for [Code] not existing in [#Settings]
and then using this CTE to join with #Settings table as shown below
; with c as (
select alternative_Code = isnull(s.code, 'Other'), o.Code
from #Objects o
left join #Settings s
on o.Code = s.Code)
select c.Code, s.value
from c
inner join #Settings s
on c.alternative_Code = s.Code
Using a left join to get null where o.Code has no match in #Settings
, and using coalesce() to return the designated replacement value
from #Settings when s.Value is null.
You could use isnull() instead of coalesce, the result would be the same in this instance.
I am not sure if this acceptable, but it returns the correct results:
select
o.Code
, coalesce(s.Value,x.Value) as Value
from #Objects o
left join #Settings s
on o.Code = s.Code
cross join (
select top 1 value
from #Settings
where Code = 'Other'
) x
rextester demo: http://rextester.com/EBUG86037
returns:
+------+-------+
| Code | Value |
+------+-------+
| A | 1 |
| B | 2 |
| D | 4 |
+------+-------+
In the form #RBarryYoung prefers:
select
o.Code
, coalesce(s.Value,x.Value) as Value
from #Objects o
left join #Settings s
on o.Code = s.Code
inner join #Settings x
on x.Code = 'Other'
This is more concise (saves you many keystrokes) and generates the same execution plan as my initial answer. Whether it is more or less clear about what it is doing is up to you, I like both.
If there is going to be one "Other" value then you can just do the join twice - a left join and another one which is effectively a cross join:
select o.Code,
coalesce(s.Value, s2.value) as value
from #Objects o
left join #Settings s on o.Code = s.Code
join #Settings s2 on s2.Code = 'Other'
So I have two hypothetical tables
Country (CountryCode, CountryName)
Groups (GroupId, GroupName, CountryCode)
I know that group is a reserved word but it's just for the sake of the example
What I want to get is the countries with 3 or more groups without the use of another referential table.
I have tried the following
select *
from Country c
where CountryCode in (select g.CountryCode
from Group g
where g.CountryCode=c.CountryCode
group by g.CountryCode
having count(*) > 3)
But I get no results given I have the following data in my Groups table:
|GroupId|GroupName|CountryCode|
| 1 | 'asd' | USA |
| 4 | 'fgh' | USA |
| 3 | 'jkl' | USA |
| 4 | 'zxc' | ARG |
The result I want is:
|CountryCode| CountryName|
| USA |UnitedStates|
because there 3 groups with the CountryCode = USA
Get those country code by using GROUP BY and Having then Join the result with Country table, you will get your expected result.
select C.*
from
(
select g.CountryCode
from Group g
group by g.CountryCode
having count(*) >= 3
) CC
INNER JOIN Country C ON C.CountryCode = CC.CountryCode
UPDATE Without JOIN
select C.*
from Country C
WHERE C.CountryCode IN
(
select g.CountryCode
from Group g
group by g.CountryCode
having count(*) >= 3
)
You are almost true without where clause But Answer of #Mahedi Sabuj is better for performance
SELECT *
FROM Country c
WHERE CountryCode IN
(
SELECT g.CountryCode
FROM Group g
GROUP BY g.CountryCode
HAVING COUNT(*) > 3
)
Select c.CountryCode, c.CountryName
FROM [Country] c
INNER JOIN [Group] g ON c.CountryCode = g.CountryCode
GROUP BY c.CountryCode, c.CountryName
HAVING COUNT(DISTINCT GroupName) >= 3
Before I have a question about (Reverse query for Hierarchical Data)
Reverse query for Hierarchical Data
I got 90% of my answer.
select i.ID, l.lev1 as Name, NULL as Parent
from IDTable i
join LevelTable l on i.Name = l.lev1
union
select i.ID, l.lev2 as Name, (select j.ID from IDTable j where j.Name = l.lev1)
from IDTable i
join LevelTable l on i.Name = l.lev2
union
select i.ID, l.lev3 as Name, (select j.ID from IDTable j where j.Name = l.lev2)
from IDTable i
join LevelTable l on i.Name = l.lev3
now for completely get my answer I have 1 more question
How I can have also position of each field in my query. For example parent of TUBE, LCD, PLASMA are TELEVISIONS now I need position field value that gives value of each position (0, 1, 2, 3…) according to alphabetic order.
category_id | name | parent |position
+-------------+----------------------+--------+-------
| 1 | ELECTRONICS | NULL |0
| 2 | TELEVISIONS | 1 |0
| 3 | TUBE | 2 |3
| 4 | LCD | 2 |1
| 5 | PLASMA | 2 |2
You should be able to use row_number() function with a partition.
select *, row_number() over (partition by parent order by name) as position from (
select i.ID, l.lev1 as Name, NULL as Parent
from IDTable i
join LevelTable l on i.Name = l.lev1
union
select i.ID, l.lev2 as Name, (select j.ID from IDTable j where j.Name = l.lev1)
from IDTable i
join LevelTable l on i.Name = l.lev2
union
select i.ID, l.lev3 as Name, (select j.ID from IDTable j where j.Name = l.lev2)
from IDTable i
join LevelTable l on i.Name = l.lev3
) as q