9

I have two tables which I want to combine. The first table is with clients and the other with products. Currently I have 22 products, but I want to have a flexible DB design so instead of having 22 columns in the product DB, I have 1 row for each product for each client so if I add or remove 1 product overall, I don't have to change the DB structure.

I want to have a select statement where I select all products for each client and the output should be in a single row with a column for each product.

I have seen some other questions which are similar, but there the aim is to have all the rows concatenated in 1 column- which I don't want.

Assuming 2 clients and 3 products.

Table client:

ClientId | ClientName
---------------------
 1       | Name1
 2       | Name2

Table products

ProductId | ClientId | Product
-------------------------------------
 1        |   1      |  SomeproductA
 2        |   1      |  SomeproductB
 3        |   1      |  SomeproductA
 4        |   2      |  SomeproductC
 5        |   2      |  SomeproductD
 6        |   2      |  SomeproductA

The output should be something like:

Table output:

 ClientId | ClientName | Product1     | Product 2    | Product 3
 -------------------------------------------------------------------
     1    | Name1      | SomeproductA | SomeproductB | SomeproductA
     2    | Name2      | SomeproductC | SomeproductD | SomeproductA

The perfect solution would also be flexible in the sense that the select statement should count the number of distinct products for each client (they will always be the same for all clients), such that if I add or remove 1 product for all clients, I should not change the select statement.

O. Jones
  • 103,626
  • 17
  • 118
  • 172
user2218856
  • 117
  • 1
  • 2
  • 6
  • 5
    You're looking to PIVOT. There's multiple question on Stack Overflow which already deal with this but they are RDBMS dependent. So, it would be extremely helpful if you could tag your question with the correct database (Oracle, MySQL etc). Please always do this. – Ben Dec 19 '13 at 08:47
  • the data you have provided for **products** are proper? – Bhavesh Kachhadiya Dec 19 '13 at 08:49
  • 1
    Even if you use the PIVOT technique, you will generally need to specify the maximum number of columns in the result set. Adding an additional product could invalidate the results for a client that has more products associated that you considered when creating the query. – Pekka Dec 19 '13 at 08:51
  • I have added the MySQL tag now. – user2218856 Dec 19 '13 at 08:56
  • Its Microsoft SQL Server Management Studio which im using. – user2218856 Dec 19 '13 at 09:01
  • Unlike forum sites, we don't use "Thanks", or "Any help appreciated", or signatures on [so]. See "[Should 'Hi', 'thanks,' taglines, and salutations be removed from posts?](http://meta.stackexchange.com/questions/2950/should-hi-thanks-taglines-and-salutations-be-removed-from-posts). – John Saunders Dec 19 '13 at 09:20

2 Answers2

8

MYSQL Edition

Here is the query. The joined query generates RowNumber (1,2,3,...) for each product inside each client group using User Defined Variables MySQL feature. The outer query forms a PIVOT table using GROUP BY and CASE with Row Numbers from the inner table. If you need to variable products column count then consider creating this query dynamic adding MAX(CASE WHEN p.RowNum=X THEN p.Product END) as ProductX to the select list.

select Clients.ClientName,
       MAX(CASE WHEN p.RowNum=1 THEN p.Product END) as Product1,
       MAX(CASE WHEN p.RowNum=2 THEN p.Product END) as Product2,
       MAX(CASE WHEN p.RowNum=3 THEN p.Product END) as Product3,
       MAX(CASE WHEN p.RowNum=4 THEN p.Product END) as Product4


FROM Clients
JOIN
(
  SELECT Products.*,
       if(@ClientId<>ClientId,@rn:=0,@rn),
       @ClientId:=ClientId,
       @rn:=@rn+1 as RowNum

  FROM Products, (Select @rn:=0,@ClientId:=0) as t
  ORDER BY ClientId,ProductID
 ) as P 
   ON Clients.ClientId=p.ClientId

GROUP BY Clients.ClientId

SQLFiddle demo

SQL Server Edition:

select Clients.ClientId,
       MAX(Clients.ClientName),
       MAX(CASE WHEN p.RowNum=1 THEN p.Product END) as Product1,
       MAX(CASE WHEN p.RowNum=2 THEN p.Product END) as Product2,
       MAX(CASE WHEN p.RowNum=3 THEN p.Product END) as Product3,
       MAX(CASE WHEN p.RowNum=4 THEN p.Product END) as Product4


FROM Clients
JOIN
(
  SELECT Products.*,
       ROW_NUMBER() OVER (PARTITION BY ClientID ORDER BY ProductID) 
         as RowNum

  FROM Products
 ) as P 
   ON Clients.ClientId=p.ClientId
GROUP BY Clients.ClientId

SQLFiddle demo

valex
  • 23,966
  • 7
  • 43
  • 60
  • Looks great but returns the following error: Msg 156, Level 15, State 1, Line 12 Incorrect syntax near the keyword 'if'. Msg 137, Level 15, State 2, Line 12 Must declare the scalar variable "@ClientId". Msg 102, Level 15, State 1, Line 16 Incorrect syntax near ':'. – user2218856 Dec 19 '13 at 09:43
  • The incorrect syntax was solved by replacing ',' with ';' but I cant make the variable declaration work. – user2218856 Dec 19 '13 at 09:54
  • It seems that a tag `MySQL` has been changed to `sql-server` so the first query works only under MySQL. I've added SQL server query to do the same. – valex Dec 19 '13 at 10:16
  • Wow fantastic. It works perfectly but im not sure why. Is it correctly understood that first you make a new temporary table called "p" and then you select rows from that table and make them into new columns? – user2218856 Dec 19 '13 at 10:20
  • The inner query is needed to numerate rows for each Client. So `ROW_NUMBER()` function is used. Then you join this table `p` and make a PIVOT. You can pivot table in two ways [using PIVOT feature in MSSQL or using GROUP BY](http://stackoverflow.com/a/9118683/1554034) – valex Dec 19 '13 at 10:42
  • This is overcomplicated for SQL Server. There is actually a [PIVOT operator](http://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx). And, the question was _never_ tagged MySQL... – Ben Dec 20 '13 at 08:02
  • @Ben: First it was MySQL tagged (See "I have added the MySQL tag now" comment to the question by @user2218856). – valex Dec 20 '13 at 08:14
  • That's what was written... not what [actually happened](http://stackoverflow.com/posts/20676984/revisions) :-). – Ben Dec 20 '13 at 09:16
  • @valex This answer was quite helpful, but I am trying to do as you suggest and have a variable column count. Adding 'MAX(CASE WHEN p.RowNum=X THEN p.Product END) as ProductX' as you write causes an error Unknown column 'X'. Would you please write an example of how this can be dynamic? – Mike Oct 08 '14 at 19:24
  • @Mike If you have a variable column count then you need [a dynamic PIVOT](http://stackoverflow.com/questions/10404348/sql-server-dynamic-pivot-query) – valex Oct 09 '14 at 05:45
  • Thanks @valex really appreciate your help. Your solution helped me. – Kung Fu Panda Jun 01 '17 at 09:36
0

The answers seem to address both MySQL and SQL Server, so I am adding a further SQL Server Answer here and the logic might work in MySQL too.

Below is a dynamic SQL version in Transact SQL for MS SQL Server.

This enables you to get the same result without having to explicitly write out every column you need in the resultant table as the CASE WHEN solution. The CASE WHEN is nice and simple for a few columns, but I recently had a similar scenario that pivoted to around 200 columns.

For dynamic SQL you essentially compile the query that you want as a string using generated variables and then execute it.

-- variable tables to store data
DECLARE @Clients TABLE(ClientID int, 
                ClientName nvarchar(10))

DECLARE @Products TABLE(ProductID int, 
                    ClientID int, 
                    Product nvarchar(15))

-- populate the variable tables with sample data
INSERT INTO @Clients 
VALUES (1, 'Name1'),
    (2, 'Name2')

INSERT INTO @Products 
VALUES (1, 1, 'SomeproductA'),
    (2, 1, 'SomeproductB'),
    (3, 1, 'SomeproductA'),
    (4, 2, 'SomeproductC'),
    (5, 2, 'SomeproductD'),
    (6, 2, 'SomeproductA')

-- display the tables to check
SELECT * FROM @Clients
SELECT * FROM @Products

-- join the two tables and generate a column with rows which will become the new 
-- column names (Product_col) which gives a number to each product per client
SELECT c.ClientID, 
    c.ClientName, 
    p.ProductID, 
    p.Product,
    CONCAT('Product', ROW_NUMBER() 
        OVER(PARTITION BY c.ClientID ORDER BY p.Product ASC))  AS Product_col
INTO #Client_Products
FROM @Products p 
LEFT JOIN @Clients c ON c.ClientID = p.ClientID

-- view the joined data and future column headings
SELECT * FROM #Client_Products

-- setup for the pivot, declare the variables to contain the column names for pivoted 
-- rows and the query string
DECLARE @cols1 AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX);

-- column name list for products
SET @cols1 = STUFF((SELECT distinct ',' + QUOTENAME(Product_col) 
        FROM #Client_Products
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)') 
    ,1,1,'')

SELECT @cols1  -- view the future column names

-- generate query variable string

-- the top select is all the columns you want to actually see as the result
-- The inner query needs the columns you want to see in the result, and the columns 
-- you are pivoting with. The pivot needs to select the value you want to go into the 
-- new columns (MAX()) and the values that will become the column names (FOR x IN())
SET @query = 'SELECT ClientID, 
            ClientName,'
                + @cols1 +' 
            FROM
            (
                SELECT ClientID,
                    ClientName,
                    Product_col,
                    Product
                FROM #Client_Products
           ) x
         PIVOT 
        (
            MAX(Product)
            FOR Product_col IN (' + @cols1 + ')
        ) p'


EXECUTE(@query) -- execute the dynamic sql

DROP TABLE #Client_Products
Fiona Pye
  • 28
  • 4