Pages - Menu

When Pivot and CTE meet together by Dynamic SQL

Scope

Today have to do something with pivot in dynamic SQL, I tweaked the scenario a bit so I am not giving out any client information.

Objectives

To create a sproc that retrieve a list of entries (candidates) with their basic info, answers (if any) for their corresponding questions in a result set.

Database Table Structure

  • Candidates
  • CandidateCategories
  • CandidateInCategories
  • Core Questions
  • Core Answers
    • CandidateID - FK
    • CoreQuestionID - FK
  • Optional Questions
    • CandidateCategoryID - FK
  • Optional Answers
    • CandidateID – FK
    • OptionalQuestionID - FK

Technical Requirements

  • Performance is not an issue, it will run less than 5 times a year
  • Need to pull out basic info of the candidate and their answer to questions, 1 candidate per row.
  • All Core and all Optional Questions will become the column header of the result set.
  • Depends on which categories that candidates in, they will have zero to all optional answers.
  • An extra column to be generated to indicate the Candidate Category, just before the columns of the set of questions.
  • Categories are to be sorted in order base on a column name CandidateCategory.CategoryOrder
  • Questions are to be sorted in order base on a column name QuestionNumber in CoreQuestion and OptionalQuestion
  • This column is ‘yes’ / ‘no’ depends if the candidate had answered any optional questions in that category.
  • Questions are unlimited and dynamically entered by clients.

Technical Overview

We know this much so far:- 
  • The result set is some kind of join between Candidate (for the basic info) and questions / answers.
  • Obviously pulling out questions by using select * from then join will not get the questions span to the column names, this is some kind of transpose with pivot, but it is only partial. Basic info do not require transpose.

Solution Overview

CTE for information

Let’s walk through the code steps by steps. I know I need to get information from multiple tables, this information will be used exactly twice. Once to generate the questions in column header and once to do the partial transpose.

-- Column definition, questions in this case
DECLARE @cols AS NVARCHAR(MAX)
DECLARE @query AS NVARCHAR(MAX)
Declare @WithQuery as NVARCHAR(MAX)
;


-- Question (column names) not to exceed 128, this is SQL Standard

Set @WithQuery = '
with cte
as
(
    SELECT  e.ItemID as CandidateID,
            cq.CandidateCategoryID,
            left(cast(ac.CandidateCategoryID as varchar(10)) + '' - '' + ac.Name, 128) as Question,
            Case
                when eca.AnswerID is null then ''No''
                else ''Yes''
            End  as Answer,
            ac.CategoryOrder as CategoryOrder,
            -1 as QuestionNumber
    FROM    Candidate e
    CROSS JOIN OptionalQuestion cq
    LEFT JOIN OptionalAnswer eca ON eca.QuestionID = cq.QuestionID AND eca.CandidateID = e.CandidateID
    JOIN CandidateCategory ac ON (ac.CandidateCategoryID = cq.CandidateCategoryID)

    UNION

    -- Core Questions

    SELECT  e.ItemID as CandidateID,
            -1,
            left(''Core Q'' + cast(cq.QuestionNumber as varchar(10)) + '' - '' + cq.Question, 128) as Question, 
            eca.Answer,
            -1 as CategoryOrder,
            cq.QuestionNumber
    FROM    Candidate e

    CROSS JOIN CoreQuestion cq
    LEFT JOIN CandidateCoreAnswers eca ON eca.QuestionID = cq.ItemID AND eca.CandidateID = e.ItemID
    WHERE    (@Filter1 IS NULL OR e.Filter1 = @Filter1)
    AND        (@Filter2 IS NULL OR e.Filter2 = @Filter2)
    AND        (@Filter3 IS NULL OR e.Filter3 = @Filter3)

    UNION

    -- Optional Questions

    SELECT  e.ItemID as CandidateID,
            cq.CandidateCategoryID,
            left(cast(ac.CandidateCategoryID as varchar(10)) + '' - Optional Q'' + cast(cq.QuestionNumber as varchar(10)) + '' - '' + cq.Question, 128) as Question, 
            eca.Answer,
            ac.CategoryOrder as CategoryOrder,
            cq.QuestionNumber
    FROM    Candidate e
    CROSS JOIN OptionalQuestion cq
    JOIN CandidateCategory ac ON (ac.ItemID = cq.CandidateCategoryID)
    LEFT JOIN OptionalAnswer eca ON eca.QuestionID = cq.QuestionID AND eca.CandidateID = e.CandidateID

 WHERE    (@Filter1 IS NULL OR e.Filter1 = @Filter1)
    AND        (@Filter2 IS NULL OR e.Filter2 = @Filter2)
    AND        (@Filter3 IS NULL OR e.Filter3 = @Filter3)
)
'

  • All the required information is now stored in CTE
  • Interesting point to note is column names must be less than or equal to 128 characters even though it is generated dynamically and not stored in sysname.
  • Line 22, –1 as question number will display the category before the question
  • Line 36, similarly, –1 as category order will display core questions before optional.


STUFF and QUOTENAME by XML PATH for column header

Set @query = @WithQuery + '
Select @cols = STUFF(
                    (SELECT '','' + QUOTENAME(c.question)
                     from (    select distinct CategoryOrder, QuestionNumber, question from cte ) c
                     Order by CategoryOrder, QuestionNumber
                     FOR XML PATH(''''), TYPE
                    ).value(''.'', ''NVARCHAR(MAX)'') 
        ,1,1,'''')
'

exec sp_executesql @query,
    N'@Filter1 int,
      @Filter2 int,
      @Filter3 int,
      @cols nvarchar(max) output',
      @Filter1 = @Filter1,
      @Filter2 = @Filter2,
      @Filter3 = @Filter3,
      @cols = @cols output

  • Line 1, reusing the CTE (should have called it @CTE but anyway)
  • The result is a massive long string that has all the questions


Dynamic SQL


set @query = @WithQuery  + '

SELECT    e.info1, e.info2, e.info3, e.infoN, r.info, derived.*
FROM    Candidate e
LEFT JOIN    OtherInfo r ON (e.OtherInfoID = r.OtherInfoID)
JOIN    (
    SELECT CandidateID, ' + @cols + ' from 
                (
                    select CandidateID
                        , isnull(answer, '''') as answer
                        , question
                    from cte
               ) x
                pivot
                (
                    max(answer)
                    for question in (' + @cols + ')
                ) p
) derived on derived.CandidateID = e.CandidateID
WHERE    (@Filter1 IS NULL OR e.Filter1 = @Filter1)
AND        (@Filter2 IS NULL OR e.Filter2 = @Filter2)
AND        (@Filter3 IS NULL OR e.Filter3 = @Filter3)

;
'

exec sp_executesql @query,
    N'@Filter1 int,
      @Filter2 int,
      @Filter3 int',
      @Filter1 = @Filter1,
      @Filter2 = @Filter2,
      @Filter3 = @Filter3

  • Line 3, derived is the questions and answers that will be transposed
  • Line 5, Candidate info can be drawn from Canddate table or join other tables for more information, just normal join stuff.
  • Line 6 – 18 is the tough part for doing the pivot (transpose), notice how line 7 and 17 use @cols dynamically appended to the @Query
  • Line 16 uses max for aggregation, noticed it became a limit that 1 candidate can only have 1 answer record per question.

Random values every row in SQL table

Ever wonder how to generate random integer value in every row of a table?
The below query will generate a random value between 0 to 0.9999.... but the value will be the same for every row.

Update table
set column = rand() 

NewId() is actually re-generated per row, so play around with it with checksum and mod will get you the desired random values.

The following will generate random values between 0 – 99 for every column, and the value would be different.



Update table
Set column = abs(checksum(NewId())) % 100

JQuery Dialog

Today came across a need to customize this jquery dialog. The aim is disable one of the button by condition.

http://docs.jquery.com/UI/Dialog

It turns out that that is an undocumented way to customize the ids for the buttons like below.
   1: $("#dialog").dialog({

   2:     height: "auto",

   3:     width: "auto",

   4:     buttons: [

   5:         {

   6:             id: "dialog-close",

   7:             text: "Cancel",

   8:             click: function() {

   9:                 // do something

  10:             }

  11:         },

  12:         {

  13:             id: "dialog-ok",

  14:             text: "Ok",

  15:             click: function() {

  16:                 // do something

  17:             }

  18:         }

  19:     ]

  20: });

  21:  



Once id can be set, event handling such as disable button or do other things became straight forward.

JQuery Star Rating Plugins Review

Scope

Today I needed a star scoring jquery plugin for one of my project. I had a quick search on the internet and chose raty at the end. There maybe better one than this out there, but I was not intending to search for the best one in the world, I simply needed one easy to use, easy to configure and works. I have put my reviews as follow:-

raty

http://wbotelhos.com/raty/

Pros
* Just needed a div node
* Everything worked out of the box
* Quite easily configurable

Cons
* If Jquery is disabled, all you get is a div and functionality lost

jRating

http://www.myjqueryplugins.com/jquery-plugin/jrating

Pros
* N/A, I couldn't get it to work with .Net

Cons
* Although I couldn't get it to work, but the jquery code must call some php to call the database, this is a contaminating between UI layer and DAL layer. Jquery should only be responsible for UI and return some kind of XML or JSON for other layers to take care of business logic or database access.

Jquery Star Rating


http://www.fyneworks.com/jquery/star-rating/

Pros
* It uses input radio button, therefore it is possible to fall back to non-javascript with radio buttons

Cons
* Not really a con but the reason I didn't try is too much copy and paste coding for input tags, and wasn't a requirement for me to support fall back non-javascript browser