Home > database >  Transpose data in SQL Server Select
Transpose data in SQL Server Select

Time:10-01

I am wondering if there is a better way to write this query. It achieves the target result but my colleague would prefer it be written without the subselects into temp tables t1-t3. The main "challenge" here is transposing the data from dbo.ReviewsData into a single row along with the rest of the data joined from dbo.Prodcucts and dbo.Reviews.

CREATE TABLE dbo.Products (
idProduct int identity,
product_title varchar(100)
PRIMARY KEY (idProduct)
);
INSERT INTO dbo.Products VALUES
(1001, 'poptart'),
(1002, 'coat hanger'),
(1003, 'sunglasses');

CREATE TABLE dbo.Reviews (
Rev_IDReview int identity,
Rev_IDProduct int
PRIMARY KEY (Rev_IDReview)
FOREIGN KEY (Rev_IDProduct) REFERENCES dbo.Products(idProduct)
);
INSERT INTO dbo.Reviews VALUES
(456, 1001),
(457, 1002),
(458, 1003);

CREATE TABLE dbo.ReviewFields (
RF_IDField int identity,
RF_FieldName varchar(32),
PRIMARY KEY (RF_IDField)
);
INSERT INTO dbo.ReviewFields VALUES
(1, 'Customer Name'),
(2, 'Review Title'),
(3, 'Review Message');

CREATE TABLE dbo.ReviewData (
RD_idData int identity,
RD_IDReview int,
RD_IDField int,
RD_FieldContent varchar(100)
PRIMARY KEY (RD_idData)
FOREIGN KEY (RD_IDReview) REFERENCES dbo.Reviews(Rev_IDReview)
);
INSERT INTO dbo.ReviewData VALUES
(79, 456, 1, 'Daniel'),
(80, 456, 2, 'Love this item!'),
(81, 456, 3, 'Works well...blah blah'),
(82, 457, 1, 'Joe!'),
(84, 457, 2, 'Pure Trash'),
(85, 457, 3, 'It was literally a used banana peel'),
(86, 458, 1, 'Karen'),
(87, 458, 2, 'Could be better'),
(88, 458, 3, 'I can always find something wrong');        

SELECT P.product_title as "item", t1.ReviewedBy, t2.ReviewTitle, t3.ReviewContent
        FROM dbo.Reviews R
        
        INNER JOIN dbo.Products P
        ON P.idProduct = R.Rev_IDProduct
        
        INNER JOIN (
           SELECT D.RD_FieldContent AS "ReviewedBy", D.RD_IDReview
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 1
        ) t1
        ON t1.RD_IDReview = R.Rev_IDReview
        
        INNER JOIN (
           SELECT D.RD_FieldContent AS "ReviewTitle", D.RD_IDReview
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 2
        ) t2
        ON t2.RD_IDReview = R.Rev_IDReview
        
        INNER JOIN (
           SELECT D.RD_FieldContent AS "ReviewContent", D.RD_IDReview
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 3
        ) t3
        ON t3.RD_IDReview = R.Rev_IDReview

EDIT: I have updated this post with the create statements for the tables as opposed to an image of the data (shame on me) and a more specific description of what exactly needed to be improved. Thanks to all for the comments and patience.

CodePudding user response:

As others have said in comments, there is nothing objectively wrong with the query. However, you could argue that it's verbose and hard to read.

One way to shorten it is to replace INNER JOIN with CROSS APPLY:

    INNER JOIN (
       SELECT D.RD_FieldContent AS 'ReviewedBy', D.RD_IDReview
       FROM dbo.ReviewsData D
       WHERE D.RD_IDField = 1
    ) t1
    ON t1.RD_IDReview = R.Rev_IDReview

APPLY lets you refer to values from the outer query, like in a subquery:

    CROSS APPLY (
       SELECT D.RD_FieldContent AS 'ReviewedBy'
       FROM dbo.ReviewsData D
       WHERE D.RD_IDField = 1 AND D.RD_IDReview = R.Rev_IDReview
    ) t1

I think of APPLY like a subquery that brings in new columns. It's like a cross between a subquery and a join. Benefits:

  • The query can be shorter, because you don't have to repeat the ID column twice.
  • You don't have to expose columns that you don't need.

Disadvantages:

  • If the query in the APPLY references outer values, then you can't extract it and run it all by itself without modifications.
  • APPLY is specific to Sql Server and it's not that widely-used.

Another thing to consider is using subqueries instead of joins for values that you only need in one place. Benefits:

  • The queries can be made shorter, because you don't have to repeat the ID column twice, and you don't have to give the output columns unique aliases.

  • You only have to look in one place to see the whole subquery.

  • Subqueries can only return 1 row, so you can't accidentally create extra rows, if only 1 row is desired.

    SELECT
        P.product_title as 'item',
    
        (SELECT D.RD_FieldContent
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 1 AND
                 D.RD_IDReview = R.Rev_IDReview) as ReviewedBy,
    
        (SELECT D.RD_FieldContent
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 2 AND
                 D.RD_IDReview = R.Rev_IDReview) as ReviewTitle,   
    
        (SELECT D.RD_FieldContent
           FROM dbo.ReviewsData D
           WHERE D.RD_IDField = 3 AND
                 D.RD_IDReview = R.Rev_IDReview) as ReviewContent
    
    FROM dbo.Reviews R
     INNER JOIN dbo.Products P ON P.idProduct = R.Rev_IDProduct
    

Edit:

It just occurred to me that you have made the joins themselves unnecessarily verbose (@Dale K actually already pointed this out in the comments):

    INNER JOIN (
       SELECT D.RD_FieldContent AS 'ReviewedBy', D.RD_IDReview
       FROM dbo.ReviewsData D
       WHERE D.RD_IDField = 1
    ) t1
    ON t1.RD_IDReview = R.Rev_IDReview

Shorter:

    SELECT RevBy.RD_FieldContent AS 'ReviewedBy'
    ...
    INNER JOIN dbo.ReviewsData RevBy
                 ON RevBy.RD_IDReview = R.Rev_IDReview AND
                    RevBy.RD_IDField = 1

CodePudding user response:

The originally submitted query is undoubtedly and unnecessarily verbose. Having digested various feedback from the community it has been revised to the following, working splendidly. In retrospect I feel very silly for having done this with subselects originally. I am clearly intermediate at best when it comes to SQL - I had not realized an "AND" clause could be included in the "ON" clause in a "JOIN" statement. Not sure why I would have made such a poor assumption.

SELECT 
P.product_title as 'item',
D1.RD_FieldContent as 'ReviewedBy',
D2.RD_FieldContent as 'ReviewTitle',
D3.RD_FieldContent as 'ReviewContent'
FROM dbo.Reviews R
INNER JOIN dbo.Products P
ON P.idProduct = R.Rev_IDProduct
INNER JOIN dbo.ReviewsData D1
ON D1.RD_IDReview = R.Rev_IDReview AND D1.RD_IDField = 1
INNER JOIN dbo.ReviewsData D2
ON D2.RD_IDReview = R.Rev_IDReview AND D2.RD_IDField = 2
INNER JOIN dbo.ReviewsData D3
ON D3.RD_IDReview = R.Rev_IDReview AND D3.RD_IDField = 3
  • Related