A "self-join" may be used but ensuring that one of the product ids is greater then than the other so that we get get "pairs" of products per order. Then it is simple to count:
Demo
CREATE TABLE OrderDetail
([OrderID] int, [ProductID] int)
;
INSERT INTO OrderDetail
([OrderID], [ProductID])
VALUES
(0001, 1254), (0001, 1252), (0002, 0038), (0003, 1254), (0003, 1252), (0003, 1432), (0004, 0038), (0004, 1254), (0004, 1252)
;
Query 1:
select -- top(2)
od1.ProductID, od2.ProductID, count(*) count_of
from OrderDetail od1
inner join OrderDetail od2 on od1.OrderID = od2.OrderID and od2.ProductID > od1.ProductID
group by
od1.ProductID, od2.ProductID
order by
count_of DESC
Results:
| ProductID | ProductID | count_of |
|-----------|-----------|----------|
| 1252 | 1254 | 3 |
| 1252 | 1432 | 1 |
| 1254 | 1432 | 1 |
| 38 | 1252 | 1 |
| 38 | 1254 | 1 |
----
With respect to displaying the "top 2" or whatever. You are likely to get "equal top" results so I would suggest you need to use dense_rank() and you may even want to "unpivot" the result so you have a single column of productids with their associated rank. How often you perform this and/or store this I leave to you.
with ProductPairs as (
select
p1, p2, count_pair
, dense_rank() over(order by count_pair DESC) as ranked
from (
select
od1.ProductID p1, od2.ProductID p2, count(*) count_pair
from OrderDetail od1
inner join OrderDetail od2 on od1.OrderID = od2.OrderID and od2.ProductID > od1.ProductID
group by
od1.ProductID, od2.ProductID
) d
)
, RankedProducts as (
select p1 as ProductID, ranked, count_pair
from ProductPairs
union all
select p2 as ProductID, ranked, count_pair
from ProductPairs
)
select *
from RankedProducts
where ranked <= 2
order by ranked, ProductID