Order by Case When ColumnName as Variable - sql

I've seen plenty of examples where people are using CASE WHEN in the ORDER BY clause of a Select statement. Typically, they're comparing the value of a variable to a string of the column name.
This is fine but what about when you have an extremely wide table?
Can you not just say something like
ORDER BY
CASE WHEN #SortDesc = 1 THEN #SortField END DESC,
CASE WHEN #SortDesc = 0 THEN #SortField END ASC
Or do you really really have to have a CASE WHEN for every column in the result set? Edit: Note that this is being converted from a SQL string to plain old SQL so dynamically building and executing it as a string isn't an option.

You can build up a dynamic sql string for something like this:
DECLARE #SortDesc varchar(max)
SELECT #SortDesc = [query to get your Sort column name].
DECLARE #sql varchar(max) = 'SELECT * FROM TABLE ORDER BY ' + #SortDesc
exec(#sql)

Or do you really really have to have a CASE WHEN for every column in
the result set?
ORDER BY
CASE WHEN #SortDesc = 1 THEN #SortField END DESC,
CASE WHEN #SortDesc = 0 THEN #SortField END ASC
The point is that your shown code won't order. #SortField is replaced on all records by the same value, and you have no special order at all.
What you can do is address a #orderBy variable witch controls what realy ocurrs like this:
ORDER BY
CASE WHEN #orderBy = 'NameDesc' THEN name END DESC,
CASE WHEN #orderBy = 'NameAsc' THEN name END,
CASE WHEN #orderBy = 'someDateDesc' THEN someDate END DESC,
CASE WHEN #orderBy = 'someDateAsc' THEN someDate END,
CASE WHEN #orderBy = 'IdDesc' THEN Id END DESC,
CASE WHEN #orderBy = 'IdAsc' THEN Id END
Here you are ordering by six criterias, but five of them are null for all records, so your efecive order is by the selected field. And yes, you will need to write all variants that you want to use.

Related

SQL concatenation with ORDER BY clause

I want to concatenate a variable name into my SQL query for order by such as the following..
I basically don't wan to use dynamic sql and exec and sp_executesql.
DECLARE #Order VARCHAR(1000)
SET #Order = 'CO_ID DESC'
print #Order
select * From contact where co_username= 'sandeepshm' order by #Order+#Order
select * From contact where co_username= 'sandeepshm' order by CO_ID DESC
you can use Case statement but not variable
something like this:
declare #sortByCO_id bit
declare #sortbyCo_Username bit
select
*
from contact
order by
case when #sortByCO_id = 1 then CO_Id else NULL end DESC
,case when sortbyCo_Username = 1 then Co_Username else NULL end

Custom sorting and pagination over large tables

I'm used to get benefits from ROW_NUMBER function in MS SQL Server scripts since 2005 version. But I noticed there is big performance disadvantage querying big tables using this function.
Imagine table with four columns (a real table from external database has more columns, but I used only those to avoid complexity of example):
DECLARE TABLE StockItems (
Id int PRIMARY KEY IDENTITY(1,1),
StockNumber nvarchar(max),
Name nvarchar(max),
[Description] nvarchar(max))
I've written procedure for querying this table filled up by 200 000+ rows with following parameters:
#SortExpression - name of column by which I want to sort
#SortDirection - bit information (0=ascending, 1=descending)
#startRowIndex - zero based index at which I want retrieve rows
#maximumRows - number of rows to be retrieved
Query:
SELECT sortedItems.Id
,si.StockNumber
,si.Name
,si.Description
FROM (SELECT s.Id
,CASE WHEN #SortDirection=1 THEN
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN
ROW_NUMBER() OVER (ORDER by s.Name DESC)
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN
ROW_NUMBER() OVER (ORDER by s.StockNumber DESC)
ELSE ROW_NUMBER() OVER (ORDER by s.StockNumber DESC)
END
ELSE
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN
ROW_NUMBER() OVER (ORDER by s.Name ASC)
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN
ROW_NUMBER() OVER (ORDER by s.StockNumber ASC)
ELSE ROW_NUMBER() OVER (ORDER by s.StockNumber ASC)
END
END AS RowNo
FROM stockItems s
) as sortedItems
INNER JOIN StockItems si ON sortedItems.Id=si.Id
ORDER BY sortedItems.RowNo
In situation when number of rows is growing rapidly, ROW_NUMBER became ineffective, because must sort all rows.
Please can you help me to avoid this performance disadvantage and speed up the query?
Check the execution path. ROW_NUMBER() does not have big impact as long as you have the correct index. The problem with your query isn't in the ROW_NUMBER().
Use dynamic instead, it will eliminate the 2 SEGMENTATION caused by the ROW_NUMBER().
I tested this on a >4mil records table and it returns in split second:
DECLARE #SortExpression VARCHAR(32) SET #SortExpression = 'StockNumber'
DECLARE #SortDirection BIT SET #SortDirection = 1
DECLARE #startRowIndex BIGINT SET #startRowIndex = 1000
DECLARE #maximumRows BIGINT SET #maximumRows = 5000
DECLARE #vsSQL AS NVARCHAR(MAX)
SET #vsSQL = ''
SET #vsSQL = #vsSQL + 'SELECT sortedItems.Id, sortedItems.StockNumber, sortedItems.Name, sortedItems.Description FROM ( '
SET #vsSQL = #vsSQL + 'SELECT s.Id, s.StockNumber, s.Name, s.Description, '
SET #vsSQL = #vsSQL + 'ROW_NUMBER() OVER (ORDER BY ' + #SortExpression + ' ' + CASE #SortDirection WHEN 1 THEN 'DESC' ELSE 'ASC' END + ') AS RowNo '
SET #vsSQL = #vsSQL + 'FROM StockItems s '
SET #vsSQL = #vsSQL + ') AS sortedItems '
SET #vsSQL = #vsSQL + 'WHERE RowNo BETWEEN ' + CONVERT(VARCHAR,#startRowIndex) + ' AND ' + CONVERT(VARCHAR,#startRowIndex+#maximumRows) + ' '
SET #vsSQL = #vsSQL + 'ORDER BY sortedItems.RowNo'
PRINT #vsSQL
EXEC sp_executesql #vsSQL
You can move the case expression to an order by clause:
order by (case when #SortDirection=1 and CHARINDEX('Name',#SortExpression)=1 then s.name end) desc,
(case when #SortDirection=1 and CHARINDEX('StockNumber',#SortExpression)=1 then s.StockNumber end) desc,
(case when #SortDirection=1 and (CHARINDEX('StockNumber',#SortExpression)<>1 and CHARINDEX('Name',#SortExpression)<>1) then va.match end) desc,
(case when #SortDirection<>1 and CHARINDEX('Name',#SortExpression)=1 then s.name end) asc,
(case when #SortDirection<>1 and CHARINDEX('StockNumber',#SortExpression)=1 then s.StockNmber end) asc,
(case when #SortDirection<>1 and (CHARINDEX('StockNumber',#SortExpression)<>1 and CHARINDEX('Name',#SortExpression)<>1) then va.match end) asc
I notice the expression has va.match, so it doesn't really match any tables in your query. So, I'm just putting in the order by expression.
And, yes, as the table gets bigger, this is going to take more time. I don't know that the order by will be more efficient than the row_number(), but it is possible.
If you need to order the rows, then you have to do a sort, one way or another (perhaps you could use an index instead). If you don't care about the order, you could take your chances with:
row_number() over (order by (select NULL))
In SQL Server, I have found that this assigns a sequential number without a separate sort. However, this is not guaranteed (I haven't found any documentation to support this use). And, the result is not necessarily stable from one run to the next.
I've found solution how to avoid performance penalty using ROW_NUMBER() function over large result sets. A goal I didn't write in my question was to avoid declaring query as nvarchar variable and executing it, because it can cause open door for SQL injection.
So, solution is to query data as much as possible in required sort order, then query result set and switch ordering and get data only for current page. Finally I can take result ordered in opposite order and order them again.
I defined new variable #innerCount to query most inner result set and order it as query client specify in #sortExpression and #sortDirection variables
SET #innerCount = #startRowIndex + #maximumRows
Select OppositeQuery.Id
,s.StockNumber
,s.Name
,s.Description
FROM (SELECT TOP (#maximumRows) InnerItems.Id
FROM
(SELECT TOP (#innerCount) sti.Id
FROM stockItems sti
ORDER BY
CASE WHEN #SortDirection=1 THEN
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN sti.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN sti.StockNumber
ELSE sti.StockNumber
END
END DESC
CASE WHEN ISNULL(#SortDirection,0)=0 THEN
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN sti.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN sti.StockNumber
ELSE sti.StockNumber
END
END ASC
) as InnerQuery
INNER JOIN StockItems si on InnerQuery.Id=si.Id
ORDER BY
CASE WHEN #SortDirection=1 then
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN si.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN si.StockNumber
ELSE si.StockNumber
END
END ASC
CASE WHEN ISNULL(#SortDirection,0)=0 then
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN si.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN si.StockNumber
ELSE si.StockNumber
END
END ASC
) AS OppositeQuery
INNER JOIN StockItems s on OppositeQuery.Id=s.Id
ORDER BY
CASE WHEN #SortDirection=1 THEN
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN s.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN s.StockNumber
ELSE s.StockNumber
END
END DESC
CASE WHEN ISNULL(#SortDirection,0)=0 THEN
CASE
WHEN CHARINDEX('Name',#SortExpression)=1 THEN s.Name
WHEN CHARINDEX('StockNumber',#SortExpression)=1 THEN s.StockNumber
ELSE s.StockNumber
END
END ASC
Disadvantage of this approach is that I have to sort data three times, but in case of multiple inner joins to StockItems table subqueries are much faster than using ROW_NUMBER() function.
Thank to all contributors for help.

T-SQL Conditional Order By

I am trying to write a stored procedure that returns a list of object with the sort order and sort direction selected by the user and passed in as sql parameters.
Lets say I have a table of products with the following columns: product_id(int), name(varchar), value(int), created_date(datetime)
and parameters #sortDir and #sortOrder
I want to do something like
select *
from Product
if (#sortOrder = 'name' and #sortDir = 'asc')
then order by name asc
if (#sortOrder = 'created_date' and #sortDir = 'asc')
then order by created_date asc
if (#sortOrder = 'name' and #sortDir = 'desc')
then order by name desc
if (#sortOrder = 'created_date' and #sortDir = 'desc')
then order by created_date desc
I tried do it with case statements but was having problems since the data types were different. Anyone got any suggestions?
CASE is an expression that returns a value. It is not for control-of-flow, like IF. And you can't use IF within a query.
Unfortunately, there are some limitations with CASE expressions that make it cumbersome to do what you want. For example, all of the branches in a CASE expression must return the same type, or be implicitly convertible to the same type. I wouldn't try that with strings and dates. You also can't use CASE to specify sort direction.
SELECT column_list_please
FROM dbo.Product -- dbo prefix please
ORDER BY
CASE WHEN #sortDir = 'asc' AND #sortOrder = 'name' THEN name END,
CASE WHEN #sortDir = 'asc' AND #sortOrder = 'created_date' THEN created_date END,
CASE WHEN #sortDir = 'desc' AND #sortOrder = 'name' THEN name END DESC,
CASE WHEN #sortDir = 'desc' AND #sortOrder = 'created_date' THEN created_date END DESC;
An arguably easier solution (especially if this gets more complex) is to use dynamic SQL. To thwart SQL injection you can test the values:
IF #sortDir NOT IN ('asc', 'desc')
OR #sortOrder NOT IN ('name', 'created_date')
BEGIN
RAISERROR('Invalid params', 11, 1);
RETURN;
END
DECLARE #sql NVARCHAR(MAX) = N'SELECT column_list_please
FROM dbo.Product ORDER BY ' + #sortOrder + ' ' + #sortDir;
EXEC sp_executesql #sql;
Another plus for dynamic SQL, in spite of all the fear-mongering that is spread about it: you can get the best plan for each sort variation, instead of one single plan that will optimize to whatever sort variation you happened to use first. It also performed best universally in a recent performance comparison I ran:
http://sqlperformance.com/conditional-order-by
You need a case statement, although I would use multiple case statements:
order by (case when #sortOrder = 'name' and #sortDir = 'asc' then name end) asc,
(case when #sortOrder = 'name' and #sortDir = 'desc' then name end) desc,
(case when #sortOrder = 'created_date' and #sortDir = 'asc' then created_date end) asc,
(case when #sortOrder = 'created_date' and #sortDir = 'desc' then created_date end) desc
Having four different clauses eliminates the problem of converting between types.
There are multiple ways of doing this. One way would be:
SELECT *
FROM
(
SELECT
ROW_NUMBER() OVER ( ORDER BY
CASE WHEN #sortOrder = 'name' and #sortDir = 'asc' then name
END ASC,
CASE WHEN #sortOrder = 'name' and #sortDir = 'desc' THEN name
END DESC,
CASE WHEN i(#sortOrder = 'created_date' and #sortDir = 'asc' THEN created_date
END ASC,
CASE WHEN i(#sortOrder = 'created_date' and #sortDir = 'desc' THEN created_date
END ASC) RowNum
*
)
order by
RowNum
You can also do it using dynamic sql.
declare #str varchar(max)
set #str = 'select * from Product order by ' + #sortOrder + ' ' + #sortDir
exec(#str)

Dynamic order by without using dynamic sql?

I have the following stored procedure which can be sorted ascending and descending on TemplateName,CreatedOn and UploadedBy. The following SP when runs doesnot sort records.if i replace 2,3,4 by columnname, i got an error message "Conversion failed when converting the nvarchar value 'Test Template' to data type int.".Please suggest how to achieve sorting.
CREATE PROCEDURE [dbo].[usp_SEL_GetRenderingTemplate]
(
#facilityID INT,
#sortOrder VARCHAR(5),
#sortExpression VARCHAR(100),
#errorCode INT OUTPUT
)
AS
BEGIN
SET NOCOUNT ON ;
BEGIN TRY
SET #sortOrder = CASE #sortOrder
WHEN 'Ascending' THEN 'ASC'
WHEN 'Descending' THEN 'DESC'
ELSE 'ASC'
END
SELECT TemplateID,
TemplateName,
CreatedOn,
( [user].LastName + ' ' + [user].FirstName ) AS UploadedBy
FROM Templates
INNER JOIN [user] ON [user].UserID = Templates.CreatedBy
WHERE facilityid = #facilityID
ORDER BY CASE WHEN #sortExpression = 'TemplateName'
AND #sortOrder = 'ASC' THEN 2
WHEN #sortExpression = 'CreatedOn'
AND #sortOrder = 'ASC' THEN 3
WHEN #sortExpression = 'UploadedBy'
AND #sortOrder = 'ASC' THEN 4
END ASC,
CASE WHEN #sortExpression = 'TemplateName'
AND #sortOrder = 'DESC' THEN 2
WHEN #sortExpression = 'CreatedOn'
AND #sortOrder = 'DESC' THEN 3
WHEN #sortExpression = 'UploadedBy'
AND #sortOrder = 'DESC' THEN 4
END DESC
SET #errorCode = 0
END TRY
BEGIN CATCH
SET #errorCode = -1
DECLARE #errorMsg AS VARCHAR(MAX)
DECLARE #utcDate AS DATETIME
SET #errorMsg = CAST(ERROR_MESSAGE() AS VARCHAR(MAX))
SET #utcDate = CAST(GETUTCDATE() AS DATETIME)
EXEC usp_INS_LogException 'usp_SEL_GetFacilityWorkTypeList',
#errorMsg, #utcDate
END CATCH
END
The dynamic ordering has to be of the same datatype. Here is an example of something that I use to case order three different datatypes - integer, date (ascending and descending), and string. This may not work for your situation, but at least you can see some techniques for casting to a common datatype.
...
ORDER BY
Case Parent.RankTypeID
When 0 Then dbo.Documents.Rank
When 1 Then Convert(int, dbo.Documents.DateStart, 112)
When 2 Then (1 - Convert(int, dbo.Documents.DateStart, 112))
When 3 Then Cast(dbo.Documents.Title as sql_variant)
End
Note:
112 is a date format of YYYYMMDD - useful for ordering.
We perform a similar sort of dynamic ordering in one of our products.
The only real different to your code is firstly, we don't use the live join.
We create a temporary table, so we can perform paging, and then apply the order.
We also use ints for ordering, cuts down on the overhead of string comparison.
It does make your SQL a bit longer, but this approach is definitely faster for the Query Optimiser. Also, and more importantly, I don't think you can mix types in Switch blocks, for ordering, so to follow your original code, you'd have to CONVERT all your data to the same time, which defeats the object :(
So you'd have
DECLARE #temp TABLE(ID int identity(1,1), TemplateID int, TemplateName nvarchar(100), CreatedOn datetime, UploadedBy nvarchar(100))
INSERT INTO #temp(TemplateID, TemplateName, CreatedOn, UploadedBy)
SELECT TemplateID,
TemplateName,
CreatedOn,
( [user].LastName + ' ' + [user].FirstName ) AS UploadedBy
FROM Templates
INNER JOIN [user] ON [user].UserID = Templates.CreatedBy
WHERE facilityid = #facilityID
Then:
IF #SortOrder = 1 --'ASC'
BEGIN
IF #sort = 2
Select *
From #Temp
Order by TemplateName ASC
ELSE IF #sort = 3
Select *
From #Temp
Order By CreatedBy ASC
-- and so on...
END
ELSE -- descending
BEGIN
-- Ad. Inf.
END
Delete
From #Temp
WHERE ID < #pageStart or ID > #pageStart + #pageSize
The following SP when runs doesnot
sort records.if i replace 2,3,4 by
columnname,
You shouldn't replace 2, 3, 4 in the ORDER BY clause by the column name. When you call the procedure just pass the column you want to sort by as the 3rd parameter.
EXEC [dbo].[usp_SEL_GetRenderingTemplate] 1,'Ascending','CreatedOn',#vErrorCode
The CASE will then evaluate the query to something like
...ORDER BY 3 DESC
which sorts the query by the 3rd column in the SELECT clause (i.e. CreatedOn)
The problem is that in CASE clauses the return values must have the same data type.
You can work around this problem by using multiple CASE statements http://www.extremeexperts.com/sql/articles/CASEinORDER.aspx
Or casting everything to the same data type.

Can I store SQL Server sort order in a variable?

I have the following SQL within a stored procedure. Is there a way to remove the IF statement and pass the 'ASC'/'DESC' option as a variable?
I know I could do the query a number of different ways, or return a table and sort it externally etc. I would just like to know if I can avoid duplicating the CASE statement.
IF #sortOrder = 'Desc'
BEGIN
SELECT * FROM #t_results
ORDER BY
CASE WHEN #OrderBy = 'surname' THEN surname END DESC,
CASE WHEN #OrderBy = 'forename' THEN forename END DESC,
CASE WHEN #OrderBy = 'fullName' THEN fullName END DESC,
CASE WHEN #OrderBy = 'userId' THEN userId END DESC,
CASE WHEN #OrderBy = 'MobileNumber' THEN MSISDN END DESC,
CASE WHEN #OrderBy = 'DeviceStatus' THEN DeviceStatus END DESC,
CASE WHEN #OrderBy = 'LastPosition' THEN LastPosition END DESC,
CASE WHEN #OrderBy = 'LastAlert' THEN LastAlert END DESC,
CASE WHEN #OrderBy = 'LastCommunication' THEN LastCommunication END DESC,
CASE WHEN #OrderBy = 'LastPreAlert' THEN LastPreAlert END DESC
END
ELSE
BEGIN
SELECT * FROM #t_results
ORDER BY
CASE WHEN #OrderBy = 'surname' THEN surname END DESC,
CASE WHEN #OrderBy = 'forename' THEN forename END DESC,
CASE WHEN #OrderBy = 'fullName' THEN fullName END DESC,
CASE WHEN #OrderBy = 'userId' THEN userId END DESC,
CASE WHEN #OrderBy = 'MobileNumber' THEN MSISDN END DESC,
CASE WHEN #OrderBy = 'DeviceStatus' THEN DeviceStatus END DESC,
CASE WHEN #OrderBy = 'LastPosition' THEN LastPosition END DESC,
CASE WHEN #OrderBy = 'LastAlert' THEN LastAlert END DESC,
CASE WHEN #OrderBy = 'LastCommunication' THEN LastCommunication END DESC,
CASE WHEN #OrderBy = 'LastPreAlert' THEN LastPreAlert END DESC
END
END
Pass in #OrderBy int, where positive is ASC, negative is DESC, and the actual number is the column to sort by:
SELECT
dt.yourColumn1
,dt.yourColumn2
,dt.yourColumn3
,CASE
WHEN #OrderBy>0 THEN dt.SortBy
ELSE NULL
END AS SortByAsc
,CASE
WHEN #OrderBy<0 THEN dt.SortBy
ELSE NULL
END AS SortByDesc
FROM (SELECT
yourColumn1
,yourColumn2
,yourColumn3
,CASE
WHEN ABS(#OrderBy) = 1 THEN surname
WHEN ABS(#OrderBy) = 2 THEN forename
WHEN ABS(#OrderBy) = 3 THEN fullName
WHEN ABS(#OrderBy) = 4 THEN CONVERT(varchar(10),userId)
WHEN ABS(#OrderBy) = 5 THEN CONVERT(varchar(10),MobileNumber
WHEN ABS(#OrderBy) = 6 THEN DeviceStatus
WHEN ABS(#OrderBy) = 7 THEN LastPosition
WHEN ABS(#OrderBy) = 8 THEN CONVERT(varchar(23),LastAlert,121)
WHEN ABS(#OrderBy) = 9 THEN CONVERT(varchar(23),LastCommunication,121)
WHEN ABS(#OrderBy) =10 THEN CONVERT(varchar(23),LastPreAlert,121)
ELSE NULL
END AS SortBy
FROM YourTablesHere
WHERE X=Y
) dt
ORDER BY SortByAsc ASC, SortByDesc DESC
Just make sure you build a string that sort properly. Notice I used 'YYYY-MM-DD hh:mm:ss.mmm' for the dates and put the numbers into strings. We usually put multiple columns together, so if you sort by surname, forename is used too, etc. Watch out, if you do combine multiple columns you'll need to pad with zeros or spaces.
If you don't want the SortByAsc and SortByDesc columns to be in the result set, wrap the entire thing in a derived table.
You can do it without dynamic SQL...
SELECT
*
FROM
My_Table
WHERE
Whatever = #something
ORDER BY
CASE #sort_order
WHEN 'ASC' THEN
CASE #order_by
WHEN 'surname' THEN surname
WHEN 'forename' THEN forename
WHEN 'fullname' THEN fullname
ELSE surname
END
ELSE '1'
END ASC,
CASE #sort_order
WHEN 'DESC' THEN
CASE #order_by
WHEN 'surname' THEN surname
WHEN 'forename' THEN forename
WHEN 'fullname' THEN fullname
ELSE surname
END
ELSE '1'
END DESC
Yes, but you have to use dynamic queries.
Take a look here.
How about this:
CASE WHEN #order_by = #order_by THEN #order_by END
Then there is no reason to have multiple cases.