How to recode a field into this specific structure using T-SQL? - sql

I am using SQL Server 2014 and I have a column (ID) in a table (tbl1). The column ID is a nvarchar field.
Here are some examples of what it contains:
ID
18FD64245
533040174
12AZ61356
19AK13355
18HD24189
I would like to run a T-SQL query to recode those values based on the following logic:
IF THEN IF THEN
A 1 0 3
B 2 1 6
C 3 2 7
D 4 3 1
E 5 4 2
F 6 5 4
G 7 6 8
H 8 7 9
I 9 8 5
J 10 9 0
K 11
L 12
M 13
N 14
O 15
P 16
Q 17
R 18
S 19
T 20
U 21
V 22
W 23
X 24
Y 25
Z 26
Therefore the first 2 values shown above would be recoded as:
ID ID2
18FD64245 656482724
533040174 411323692
I am having a hard time approaching the problem from a T-SQL point of view. I am thinking about using CASE Statements to solve the problem. I also had a look at the REPLACE function.
But I am stuck as to how to go about it since the ID field is an alpha-numeric field.
Any ideas on how I move forward with this?
Edit (to show my sql codes as per answer proposed by #Squirrel):
declare #map table
(
map_fr char(1),
map_to varchar(2)
)
insert into #map
values
('A', '1'),
('B', '2'),
('C', '3'),
('D', '4'),
('E', '5'),
('F', '6'),
('G', '7'),
('H', '8'),
('I', '9'),
('J', '10'),
('K', '11'),
('L', '12'),
('M', '13'),
('N', '14'),
('O', '15'),
('P', '16'),
('Q', '17'),
('R', '18'),
('S', '19'),
('T', '20'),
('U', '21'),
('V', '22'),
('W', '23'),
('X', '24'),
('Y', '25'),
('Z', '26')
; with rcte as
(
select [ID], idx = 1, ch = substring([ID], 1, 1)
from Table1
WHERE [ID] IS NOT NULL
union all
select [ID], idx = idx + 1, ch = substring([ID], idx + 1, 1)
from rcte
where idx < len([ID])
),
cte as
(
select r.[ID], r.idx, m.map_to
from rcte r
inner join #map m on r.ch = m.map_fr
)
select [ID],
(select '' + map_to from cte x where x.[ID] = c.[ID] order by idx for xml path('')) as ID2
from cte c
group by [ID]
order by [ID]

I would create a mapping table like
declare #map table
(
map_fr char(1),
map_to varchar(2)
)
and insert the mapping there
insert into #map
values ('A', '1'), ('B', '2'), ('C', '3'), ('D', '4'), ('E', '5'), ('F', '6'),
('G', '7'), ('H', '8'), ('I', '9'), ('J', '10'),('K', '11'),('L', '12'),
('M', '13'),('N', '14'),('O', '15'),('P', '16'),('Q', '17'),('R', '18'),
('S', '19'),('T', '20'),('U', '21'),('V', '22'),('W', '23'),('X', '24'),
('Y', '25'),('Z', '26'),
('0', '3'), ('1', '6'), ('2', '7'), ('3', '1'), ('4', '2'), ('5', '4'),
('6', '8'), ('7', '9'), ('8', '5'), ('9', '0')
then use recursive CTE to split the character and join to the mapping table. And finally concatenate back the string using the mapped value.
; with rcte as
(
select ID, idx = 1, ch = substring(ID, 1, 1)
from yourtbl
union all
select ID, idx = idx + 1, ch = substring(ID, idx + 1, 1)
from rcte
where idx < len(ID)
),
cte as
(
select r.ID, r.idx, m.map_to
from rcte r
inner join #map m on r.ch = m.map_fr
)
select ID,
(select '' + map_to from cte x where x.ID = c.ID order by idx for xml path('')) as ID2
from cte c
group by ID
order by ID

This is better suited as an scalar function, but if you want to do it all on a single SQL statement, here is a way :
select ID,
case substring(ID, 1, 1) when 'A' then '1'
when 'B' then '2'
...
when '9' then '0'
end
+
case substring(ID, 2, 1) when 'A' then '1'
when 'B' then '2'
...
when '9' then '0'
end
+
...
...
case substring(ID, 9, 1) when 'A' then '1'
when 'B' then '2'
...
when '9' then '0'
end
as ID2
from MY_TABLE

You can also map these using a tally table and some of the new features of SQL Server 2017 (STRING_AGG):
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE IDS
(
ID NVARCHAR(9)
)
INSERT INTO IDS
VALUES ('18FD64245'),
('533040174'),
('12AZ61356'),
('19AK13355'),
('18HD24189');
Query 1:
WITH Tally
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY Nums.Num) AS Number
FROM (VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS Nums(Num)
CROSS APPLY (VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS Nums2(Num)
),
Chars
As
(
-- Turn each character of ID to new row
SELECT ID, SUBSTRING(ID, Number, 1) AS OldChar, Number As Ind
FROM IDS
CROSS APPLY Tally
WHERE SUBSTRING(ID, Number, 1) <> ''
),
NewChars
AS
(
-- Map old characters to new characters
SELECT *,
CASE WHEN ISNumeric(OldChar) = 1 THEN
-- effectively a mapping string to map old characters to new
SUBSTRING('3671248950', CHARINDEX(OldChar, '0123456789'), 1)
ELSE
-- for alphanumeric we can simply make 'A' be 1 and 'B' be 2
-- by subtracting the ASCII value of 'A' from the ASCII of the
-- Character and add 1
ASCII(OldChar) - ASCII('A') + 1
END As NewChar
FROM Chars
)
-- Recombine New Characters to form new Id (SQL Server 2017 only)
SELECT ID, STRING_AGG(NewChar,'') WITHIN GROUP (ORDER BY Ind) AS NewId
FROM NewChars
GROUP BY ID
ORDER BY Id
Results:
| ID | NewId |
|-----------|------------|
| 12AZ61356 | 6712686148 |
| 18FD64245 | 656482724 |
| 18HD24189 | 658472650 |
| 19AK13355 | 6011161144 |
| 533040174 | 411323692 |

Related

SQL query to fetch distinct records

Can someone help me out with this sql query on postgres which I have to write but I just can't come up with, I have tried my best to simplify the problem from 1 million records and more constraints to this, I know this looks easy, but I am still unable to resolve this somehow :-
Table_name = t
Column_1_name = id
Column_2_name = st
Column_1_elements = [1,1,1,1,2,2,2,3,3]
Column_2_elements = [a,b,c,d,a,c,d,b,d]
Now I want to print to those distinct ids from id where they do not have their corresponding st equals to 'b' or 'a'.
For example, for the above example, the ouput should be [2,3] as 2 does not have corresponding 'b' and 3 does not have 'a'. [even though 3 does not have c also, but we are not concerned about 'c']. id=1 is not returned in solution as it has a relation with both 'a' and 'b'.
Let me know if you need more clarity.
Thanks in advance for helping.
edit1:- The number of elements for id = 1,2,3 could be anything. I just want those ids where there corresponding st does not "contain" 'a' or 'b'.
if there is an id=4 which has just one st which is 'r', and there is an id=5 which contains 'a','b','c','d','e','f','k','z'.
Then we want id=4 in the output as well as it does not contain 'a' or 'b'..
You might need to correct the syntax a little bit based on you SQL engine but this one is a working solution in Google BigQuery -
with temp as (
select 1 as id, 'a' as st union all
select 1 as id, 'b' as st union all
select 1 as id, 'c' as st union all
select 1 as id, 'd' as st union all
select 2 as id, 'a' as st union all
select 2 as id, 'c' as st union all
select 2 as id, 'd' as st union all
select 3 as id, 'b' as st union all
select 3 as id, 'd' as st union all
select 4 as id, 'e' as st union all
select 5 as id, 'g' as st union all
select 5 as id, 'h' as st
)
-- add 2 columns for is_a and is_b flags
, temp2 as (
select *
, case when st = 'a' then 1 else 0 end is_a
,case when st = 'b' then 1 else 0 end as is_b
from temp
)
-- IDs that have both the flags as 1 should be filtered out (like ID = 1)
select id
from temp2
group by 1
having max(is_a) + max(is_b) < 2
This solution takes care of the problem you mentioned with ID 4 . Let me know if this works for you.
See if this works:
create table t (id integer, st varchar);
insert into t values (1, 'a'), (1, 'b'), (1, 'c'), (1, 'd'), (2, 'a'), (2, 'c'), (2, 'd'), (3, 'b'), (3, 'd'), (4, 'r');
insert into t values (5, 'a'), (5, 'b'), (5, 'c'), (5, 'd'), (5, 'e'), (5, 'f'), (5, 'k'), (5, 'z');
select id, array['a', 'b'] <# array_agg(st)::text[] as tf from t group by id;
id | tf
----+----
3 | f
5 | t
4 | f
2 | f
1 | t
select * from (select id, array['a', 'b'] <# array_agg(st)::text[] as tf from t group by id) as agg where agg.tf = 'f';
id | tf
----+----
3 | f
4 | f
2 | f
In the first select query the array_agg(st) aggregates all the st values for an id via the group by id. array['a', 'b'] <# array_agg(st)::text[] then asks if the a and b are both in the array_agg.
The query is then turned into a sub-query where the outer query selects those rows that where 'f'(false), in other words did not have both a and b in the aggregated id values.

Selecting rows from a table with specific values per id

I have the below table
Table 1
Id WFID data1 data2
1 12 'd' 'e'
1 13 '3' '4f'
1 15 'e' 'dd'
2 12 'f' 'ee'
3 17 'd' 'f'
2 17 'd' 'f'
4 12 'd' 'f'
5 20 'd' 'f'
From this table I just want to select the rows which has 12 and 17 only exclusively. Like from the table I just want to retrieve the distinct id's 2,3 and 4. 1 is excluded because it has 12 but also has 13 and 15. 5 is excluded because it has 20.
2 in included because it has just 12 and 17.
3 is included because it has just 17
4 is included because it has just 12
If you just want the list of distinct ids that satisfy the conditions, you can use aggregation and filter with a having clause:
select id
from mytable
group by id
having max(case when wfid not in (12, 17) then 1 else 0 end) = 0
This filters out groups that have any wfid other than 12 or 17.
If you want the entire corresponding rows, then window functions are more appropriate:
select
from (
select t.*,
max(case when wfid not in (12, 17) then 1 else 0 end) over(partition by id) flag
from mytable t
) t
where flag = 0
You really need to start thinking in terms of sets. And it helps everyone if you provide a script that can be used to experiment and demonstrate. Here is another approach using the EXCEPT operator. The idea is to first generate a set of IDs that we want based on the filter. You then generate a set of IDs that we do not want. Using EXCEPT we can then remove the 2nd set from the 1st.
declare #x table (Id tinyint, WFID tinyint, data1 char(1), data2 varchar(4));
insert #x (Id, WFID, data1, data2) values
(1, 12, 'd', 'e'),
(1, 13, '3', '4f'),
(1, 15, 'e', 'dd'),
(2, 12, 'f', 'ee'),
(3, 17, 'd', 'f'),
(2, 17, 'd', 'f'),
(4, 12, 'd', 'f'),
(2, 12, 'z', 'ef'),
(5, 20, 'd', 'f');
select * from #x
select id from #x where WFID not in (12, 17);
select id from #x where WFID in (12, 17)
except
select id from #x where WFID not in (12, 17);
Notice the added row to demonstrate what happens when there are "duplicates".

SQL Query for Select Sequence Numbers

SQL Query for Select Sequence Numbers
In SQL server, I want to select rows based on sequence numbers. For example I am having data as below:
ID RowNos
A 1
B 2
X NULL
C 4
D 5
Y NULL
E 7
F 8
G 9
H 11
I 13
Query Should return
ID NextID
A B -- Since RowNos 1,2 is in sequence
C D -- Since RowNos 4,5 is in sequence
E G -- Since RowNos 7,8,9 is in sequence
I don't have idea to start this query. Otherwise I'll post my trial too.
DECLARE #t TABLE (ID CHAR(1), RowNos INT)
INSERT INTO #t
VALUES
('A', 1), ('B', 2), ('X', NULL),
('C', 4), ('D', 5), ('Y', NULL),
('E', 7), ('F', 8), ('G', 9),
('H', 11), ('I', 13)
SELECT MIN(ID), MAX(ID)
FROM (
SELECT *, rn = ROW_NUMBER() OVER (ORDER BY RowNos)
FROM #t
) t
WHERE RowNos IS NOT NULL
GROUP BY RowNos - rn
HAVING MIN(ID) != MAX(ID)
Output:
---- ----
A B
C D
E G
to select them ordered should be something like:
SELECT * FROM table_name WHERE RowNos IS NOT NULL ORDER BY RowNos ASC;

Merging groups of interval data - SQL Server

I have two sets of interval data I.E.
Start End Type1 Type2
0 2 L NULL
2 5 L NULL
5 7 L NULL
7 10 L NULL
2 3 NULL S
3 5 NULL S
5 8 NULL S
11 12 NULL S
What I'd like to do is merge these sets into one. This seems possible by utilising an islands and gaps solution but due to the non-continuous nature of the intervals I'm not sure how to go about applying it... The output I'm expecting would be:
Start End Type1 Type2
0 2 L NULL
2 3 L S
3 5 L S
5 7 L S
7 8 L S
8 10 L NULL
11 12 NULL S
Anyone out there done something like this before??? Thanks!
Create script below:
CREATE TABLE Table1
([Start] int, [End] int, [Type1] varchar(4), [Type2] varchar(4))
;
INSERT INTO Table1
([Start], [End], [Type1], [Type2])
VALUES
(0, 2, 'L', NULL),
(2, 3, NULL, 'S'),
(2, 5, 'L', NULL),
(3, 5, NULL, 'S'),
(5, 7, 'L', NULL),
(5, 8, NULL, 'S'),
(7, 10, 'L', NULL),
(11, 12, NULL, 'S')
;
I assume that Start is inclusive, End is exclusive and given intervals do not overlap.
CTE_Number is a table of numbers. Here it is generated on the fly. I have it as a permanent table in my database.
CTE_T1 and CTE_T2 expand each interval into the corresponding number of rows using a table of numbers. For example, interval [2,5) generates rows with Values
2
3
4
This is done twice: for Type1 and Type2.
Results for Type1 and Type2 are FULL JOINed together on Value.
Finally, a gaps-and-islands pass groups/collapses intervals back.
Run the query step-by-step, CTE-by-CTE and examine intermediate results to understand how it works.
Sample data
I added few rows to illustrate a case when there is a gap between values.
DECLARE #Table1 TABLE
([Start] int, [End] int, [Type1] varchar(4), [Type2] varchar(4))
;
INSERT INTO #Table1 ([Start], [End], [Type1], [Type2]) VALUES
( 0, 2, 'L', NULL),
( 2, 3, NULL, 'S'),
( 2, 5, 'L', NULL),
( 3, 5, NULL, 'S'),
( 5, 7, 'L', NULL),
( 5, 8, NULL, 'S'),
( 7, 10, 'L', NULL),
(11, 12, NULL, 'S'),
(15, 20, 'L', NULL),
(15, 20, NULL, 'S');
Query
WITH
e1(n) 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
) -- 10
,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b) -- 10*10
,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
,CTE_Numbers
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY n) AS Number
FROM e3
)
,CTE_T1
AS
(
SELECT
T1.[Start] + CA.Number - 1 AS Value
,T1.Type1
FROM
#Table1 AS T1
CROSS APPLY
(
SELECT TOP(T1.[End] - T1.[Start]) CTE_Numbers.Number
FROM CTE_Numbers
ORDER BY CTE_Numbers.Number
) AS CA
WHERE
T1.Type1 IS NOT NULL
)
,CTE_T2
AS
(
SELECT
T2.[Start] + CA.Number - 1 AS Value
,T2.Type2
FROM
#Table1 AS T2
CROSS APPLY
(
SELECT TOP(T2.[End] - T2.[Start]) CTE_Numbers.Number
FROM CTE_Numbers
ORDER BY CTE_Numbers.Number
) AS CA
WHERE
T2.Type2 IS NOT NULL
)
,CTE_Values
AS
(
SELECT
ISNULL(CTE_T1.Value, CTE_T2.Value) AS Value
,CTE_T1.Type1
,CTE_T2.Type2
,ROW_NUMBER() OVER (ORDER BY ISNULL(CTE_T1.Value, CTE_T2.Value)) AS rn
FROM
CTE_T1
FULL JOIN CTE_T2 ON CTE_T2.Value = CTE_T1.Value
)
,CTE_Groups
AS
(
SELECT
Value
,Type1
,Type2
,rn
,ROW_NUMBER() OVER
(PARTITION BY rn - Value, Type1, Type2 ORDER BY Value) AS rn2
FROM CTE_Values
)
SELECT
MIN(Value) AS [Start]
,MAX(Value) + 1 AS [End]
,Type1
,Type2
FROM CTE_Groups
GROUP BY rn-rn2, Type1, Type2
ORDER BY [Start];
Result
+-------+-----+-------+-------+
| Start | End | Type1 | Type2 |
+-------+-----+-------+-------+
| 0 | 2 | L | NULL |
| 2 | 8 | L | S |
| 8 | 10 | L | NULL |
| 11 | 12 | NULL | S |
| 15 | 20 | L | S |
+-------+-----+-------+-------+
A step-by-step way is:
-- Finding all break points
;WITH breaks AS (
SELECT Start
FROM yourTable
UNION
SELECT [End]
FROM yourTable
) -- Finding Possible Ends
, ends AS (
SELECT Start
, (SELECT Min([End]) FROM yourTable WHERE yourTable.Start = breaks.Start) End1
, (SELECT Max([End]) FROM yourTable WHERE yourTable.Start < breaks.Start) End2
FROM breaks
) -- Finding periods
, periods AS (
SELECT Start,
CASE
WHEN End1 > End2 And End2 > Start THEN End2
WHEN End1 IS NULL THEN End2
ELSE End1
END [End]
FROM Ends
WHERE NOT(End1 IS NULL AND Start = End2)
) -- Generating results
SELECT p.Start, p.[End], Max(Type1) Type1, Max(Type2) Type2
FROM periods p, yourTable t
WHERE p.start >= t.Start AND p.[End] <= t.[End]
GROUP BY p.Start, p.[End];
In above query some situations may not fit at analyzing all of them, you can improve it as you want ;).
First getting all the numbers of start and end via a Union.
Then joining those numbers on both the 'L' and 'S' records.
Uses a table variable for the test.
DECLARE #Table1 TABLE (Start int, [End] int, Type1 varchar(4), Type2 varchar(4));
INSERT INTO #Table1 (Start, [End], Type1, Type2)
VALUES (0, 2, 'L', NULL),(2, 3, NULL, 'S'),(2, 5, 'L', NULL),(3, 5, NULL, 'S'),
(5, 7, 'L', NULL),(5, 8, NULL, 'S'),(7, 10, 'L', NULL),(11, 12, NULL, 'S');
select
n.Num as Start,
(case when s.[End] is null or l.[End] <= s.[End] then l.[End] else s.[End] end) as [End],
l.Type1,
s.Type2
from
(select Start as Num from #Table1 union select [End] from #Table1) n
left join #Table1 l on (n.Num >= l.Start and n.Num < l.[End] and l.Type1 = 'L')
left join #Table1 s on (n.Num >= s.Start and n.Num < s.[End] and s.Type2 = 'S')
where (l.Start is not null or s.Start is not null)
order by Start, [End];
Output:
Start End Type1 Type2
0 2 L NULL
2 3 L S
3 5 L S
5 7 L S
7 8 L S
8 10 L NULL
11 12 NULL S

SQL - Select query to get purchase orders that have two or more products from different product lines

I'm wondering what I can do to rework/improve this query (it takes too long to run).
Some background info:
Order ID => 'id'
Purchase ID => 'pid'
"Table" is a table of purchase lines, the id is the id of an order, and orders can have multiple lines. For example, there can be three lines with id 1, having various purchase ids, so:
id | pid
1 | 3
1 | 3
1 | 46
The purpose of the query is to find orders that have purchases from multiple product lines, such as 1, 21, 31, 41 and 2, 22, 32, 42.
Query below is what I came up with, but it runs very slowly from all the sub-queries, is it possible to optimize this query or get the same results with a different, faster query?
SELECT a.id
FROM Table AS a
GROUP BY a.id
HAVING (
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('1', '21', '31', '41') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('2', '22', '32', '42') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('3', '23', '33', '43') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('4', '24', '34', '44') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('5', '25', '35', '45') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('6', '26', '36', '46') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('7', '27', '37', '47') AND id = a.id ) THEN 1 ELSE 0 END AS INT) +
CAST( CASE WHEN EXISTS (SELECT NULL FROM Table WHERE pid IN ('8', '28', '38', '48') AND id = a.id ) THEN 1 ELSE 0 END AS INT)
) > 1
EDIT:
Final working query (is 97% faster than previous):
SELECT y.Id
FROM (SELECT x.Id,
x.productLine
FROM ( SELECT a.id,
CASE
WHEN a.pid IN ('1', '21', '31', '41') THEN 1
WHEN a.pid IN ('2', '22', '32', '42') THEN 2
WHEN a.pid IN ('3', '23', '33', '43') THEN 3
WHEN a.pid IN ('4', '24', '34', '44') THEN 4
WHEN a.pid IN ('5', '25', '35', '45') THEN 5
WHEN a.pid IN ('6', '26', '36', '46') THEN 6
WHEN a.pid IN ('7', '27', '37', '47') THEN 7
WHEN a.pid IN ('8', '28', '38', '48') THEN 8
ELSE 9
END AS productLine
FROM Table AS a
WHERE a.pid IN ('1', '21', '31', '41','2', '22', '32', '42','3', '23', '33', '43','4', '24', '34', '44','5', '25', '35', '45','6', '26', '36', '46','7', '27', '37', '47','8', '28', '38', '48')
) AS x
GROUP BY x.Id, x.productLine
) AS y
GROUP BY y.Id
HAVING COUNT(*) > 1
As I understand Your problem this query should meet your request:
SELECT x.Id
FROM ( SELECT a.Id ,
CAST(a.pid AS INT) % 10 AS pid
FROM [Table] AS a
GROUP BY a.Id ,
CAST(a.pid AS INT) % 10
) x
GROUP BY x.Id
HAVING COUNT(*) > 1
After taking into account new assumptions query should look like:
SELECT y.Id
(SELECT x.Id,
x.pid
FROM ( SELECT a.id,
CASE WHEN a.pid IN ('1', '21', '31', '41') THEN 1
WHEN a.pid IN ('2', '22', '32', '42') THEN 2
WHEN a.pid IN ('3', '23', '33', '43') THEN 3
WHEN a.pid IN ('4', '24', '34', '44') THEN 4
WHEN a.pid IN ('5', '25', '35', '45') THEN 5
WHEN a.pid IN ('6', '26', '36', '46') THEN 6
WHEN a.pid IN ('7', '27', '37', '47') THEN 7
WHEN a.pid IN ('8', '28', '38', '48') THEN 8
ELSE 9 END AS productLine
FROM Table AS a
) x
GROUP BY x.Id, x.pid) y
GROUP BY y.Id
HAVING COUNT(*) > 1
Althoug I really like RafaƂ's solution as it gives numbers to the groups, I thought of another, little simplier solution, but was unable to test it earlier.
SELECT distinct(id) FROM store a
WHERE 4 = (
SELECT COUNT(DISTINCT(pid))
FROM store
where (id = a.id AND (
pid in (1, 21, 31, 41) OR
pid in (2, 22, 32, 42)
)
)
)