T-SQL Conditional Order By - sql

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)

Related

SQL Case Order By specific order and Direction

I have a table that I need sorted on fields By #SortBy and #SortDirection,
for ID,PriorityID,stateType (column type is int) result is OK, but for Title (nvarchar) Query Result is:
Conversion failed when converting the nvarchar value 'Title Column value' to data type int.
Query:
CASE
WHEN #SortDirection = 'ASC' THEN
CASE #SortBy
WHEN 'ID' THEN ID --int
WHEN 'Title' THEN Title --nvarchar
WHEN 'PriorityID' THEN [Priority] --int
WHEN 'stateType' THEN [state] --int
end
END ASC
,case WHEN #SortDirection = 'DESC' THEN
CASE #SortBy
WHEN 'ID' THEN ID
WHEN 'Title' THEN Title
WHEN 'Priority' THEN [Priority]
WHEN 'state' THEN [state]
END
END DESC
The types are different, and that is a problem for the case expressions. The simplest method is a different case for each possibility:
ORDER BY (CASE WHEN #SortDirection = 'ASC' AND #SortBy = 'ID' THEN ID END) ASC,
(CASE WHEN #SortDirection = 'ASC' AND #SortBy = 'Title' THEN Title END) ASC,
. . .

The text, ntext, and image data types cannot be compared RowNumber

i know that this problem has been resolved many times but after tens forum i cannot find my mistake...
I'm on 2008 SQL Server
Here is my query:
Declare #order varchar(MAX) = 'DESC'
Declare #sort varchar(MAX) = 'Description'
SELECT TOP 10 * from ( SELECT *, ROW_NUMBER() OVER
(ORDER BY
(CASE WHEN #sort = 'Name' and #order = 'ASC' THEN NAME
WHEN #sort = 'URL' and #order = 'ASC' THEN URL
WHEN #sort = 'Description' and #order = 'ASC' THEN Description
END) ASC,
(CASE WHEN #sort = 'Name' and #order = 'DESC' THEN NAME
WHEN #sort = 'URL' and #order = 'DESC' THEN URL
WHEN #sort = 'Description' and #order = 'DESC' THEN Description
END) DESC)
AS RowNum FROM Application) AS MyDerivedTable WHERE MyDerivedTable.RowNum BETWEEN 0 AND 10
And the error:
"The text, ntext, and image data types cannot be compared or sorted,
except when using IS NULL or LIKE operator"
I also tried to replace varchar by nvarchar but it doesnt worked.
Thanks for help
A case expression returns a single type. I would recommend that you rewrite the order by using separate expressions:
ORDER BY (CASE WHEN #sort = 'Name' and #order = 'ASC' THEN NAME END) ASC,
(CASE WHEN #sort = 'URL' and #order = 'ASC' THEN URL END) ASC,
(CASE WHEN #sort = 'Description' and #order = 'ASC' THEN Description END) ASC,
(CASE WHEN #sort = 'Name' and #order = 'DESC' THEN NAME END) DESC,
(CASE WHEN #sort = 'URL' and #order = 'DESC' THEN URL END) DESC,
(CASE WHEN #sort = 'Description' and #order = 'DESC' THEN Description END) DESC
I'm not sure if this will fix your particular problem, which seems due to arcane types being used for certain columns. However, this fixes most of the problems that arise from using a single case expression with mixed types.
Presumably, you are using text columns. This type has been deprecated, so you should not use it. I would strongly recommend that you change the type to varchar(max) or nvarchar(max).
SOLVED ! I Had a 'URL' as type 'text' in my Table definition, I replaced it by nvarchar. Thank you

Optimizing this 40+ second select query on MSSQL 2012?

I'm no DBA and I'm out of ideas on optimizing this query. It's taking roughly 40+ seconds to run. Any glaring newbie mistakes where I could optimize?
USE [deskcal2014]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROC [dbo].[proc_AdminRegisteredCards]
(
#Take AS INT,
#Skip AS INT,
#FilterColumn AS NVARCHAR(max),
#FilterOrder AS NVARCHAR(max))
AS
BEGIN
SELECT TOP(#Take)
ISNULL( ROW_NUMBER() OVER(ORDER BY ta.CreatedOn, ta.ItemId), -1000) AS AdminRegisteredCardsId,
ta.ItemId,
ta.CardNumber,
ta.FirstName,
ta.LastName,
ta.Birthday,
ta.PostalCode,
ta.[Description],
ta.CardActivated,
ta.ContactInfo,
ta.PhoneNumber,
ta.ReceiveCalendarReminders,
ta.ReceiveGeneralMails,
ta.ReceivePrefStoreMails,
ta.CardStatus,
ta.SamoaCardId,
ta.CalendarUserId,
ta.LiveOpsRegistrantId,
ta.UseType,
ta.CreatedOn,
ta.ModifiedBy,
ta.ModifiedOn from (
SELECT CalendarUser.CalendarUserId as ItemId,
SamoaCard.CardNumber,
SamoaCard.FirstName,
SamoaCard.LastName,
CalendarUser.Birthday,
CalendarUser.PostalCode,
RegisterSourceType.[Description],
CalendarUserCard.CardActivated,
CalendarUser.EmailAddress as ContactInfo,
CalendarUser.PhoneNumber,
CalendarUser.ReceiveCalendarReminders,
CalendarUser.ReceiveGeneralMails,
CalendarUser.ReceivePrefStoreMails,
CASE WHEN CalendarUserCard.CardDeactivated IS NOT NULL THEN 'Deactivated' ELSE 'Activated' END AS CardStatus,
SamoaCard.SamoaCardId,
CalendarUser.CalendarUserId,
null as LiveOpsRegistrantId,
SamoaCard.CreatedOn,
'C' as UseType,
CalendarUser.ModifiedBy,
CalendarUser.ModifiedOn
FROM (
(dbo.CalendarUser CalendarUser
INNER JOIN dbo.RegisterSourceType RegisterSourceType ON (CalendarUser.RegisterType = RegisterSourceType.RegisterType))
INNER JOIN dbo.CalendarUserCard CalendarUserCard ON (CalendarUserCard.CalendarUserId = CalendarUser.CalendarUserId)
)
INNER JOIN dbo.SamoaCard SamoaCard ON (CalendarUserCard.SamoaCardId = SamoaCard.SamoaCardId)
ORDER BY
case when #FilterColumn = 'FirstName' and #FilterOrder = 'ASC'
then CalendarUser.Firstname end asc,
case when #FilterColumn = 'FirstName' and #FilterOrder = 'DESC'
then CalendarUser.Firstname end desc,
case when #FilterColumn = 'LastName' and #FilterOrder = 'ASC'
then CalendarUser.Lastname end asc,
case when #FilterColumn = 'LastName' and #FilterOrder = 'DESC'
then CalendarUser.Lastname end desc,
case when #FilterColumn = 'CardNumber' and #FilterOrder = 'ASC'
then CalendarUser.CardNumber end asc,
case when #FilterColumn = 'CardNumber' and #FilterOrder = 'DESC'
then CalendarUser.CardNumber end desc,
case when #FilterColumn = 'Birthday' and #FilterOrder = 'ASC'
then CalendarUser.Birthday end asc,
case when #FilterColumn = 'Birthday' and #FilterOrder = 'DESC'
then CalendarUser.Birthday end desc,
case when #FilterColumn = 'Description' and #FilterOrder = 'ASC'
then RegisterSourceType.[Description] end asc,
case when #FilterColumn = 'Description' and #FilterOrder = 'DESC'
then RegisterSourceType.[Description] end desc,
case when #FilterColumn = 'ContactInfo' and #FilterOrder = 'ASC'
then CalendarUser.EmailAddress end asc,
case when #FilterColumn = 'ContactInfo' and #FilterOrder = 'DESC'
then CalendarUser.EmailAddress end desc,
case when #FilterColumn = 'CardActivated' and #FilterOrder = 'ASC'
then CalendarUserCard.CardActivated end asc,
case when #FilterColumn = 'CardActivated' and #FilterOrder = 'DESC'
then CalendarUserCard.CardActivated end desc,
case when #FilterColumn = 'PostalCode' and #FilterOrder = 'ASC'
then CalendarUser.PostalCode end asc,
case when #FilterColumn = 'PostalCode' and #FilterOrder = 'DESC'
then CalendarUser.PostalCode end desc
OFFSET #Skip ROWS -- skip N rows
FETCH NEXT #Take ROWS ONLY
union all
SELECT TOP(10)
LiveOpsRegistrant.LiveOpsRegistrantId as ItemId,
LiveOpsRegistrant.CardNumber,
'Registered' as FirstName,
'Card' as LastName,
LiveOpsRegistrant.Birthday,
null as PostalCode,
'LiveOps' as Description,
LiveOpsRegistrant.CreatedOn as CardActivated,
LiveOpsRegistrant.PhoneNumber as ContactInfo,
LiveOpsRegistrant.PhoneNumber,
CONVERT(bit,0) as ReceiveCalendarReminders,
CONVERT(bit,0) as ReceiveGeneralMails,
CONVERT(bit,0) as ReceivePrefStoreMails,
'Activated' AS CardStatus,
SamoaCard.SamoaCardId,
null as CalendarUserId,
LiveOpsRegistrant.LiveOpsRegistrantId,
SamoaCard.CreatedOn,
'L' as UseType,
SamoaCard.ModifiedBy,
SamoaCard.ModifiedOn
FROM dbo.LiveOpsRegistrant LiveOpsRegistrant
INNER JOIN dbo.SamoaCard SamoaCard ON (LiveOpsRegistrant.CardNumber = SamoaCard.CardNumber)) ta
END
GO
Echoing some of the comments already given: Having a bunch of logic in an ORDER BY clause usually doesn't work well. ROW_NUMBER() can be nasty when used in a query with many joins and other complexities as in your case.
Temp tables are probably your first best option here. Reading your code, I think the first one is for CalendarUser.CalendarUserId, and you'll want to populate it with a bunch of nested if ... else if statements:
if #FilterColumn = 'FirstName' and #FilterOrder = 'ASC'
begin
insert into #CalendarUser
select top(#Take) CalendarUserId
order by Firstname asc
offset #Skip rows
fetch next #Take rows only
end
else
begin
if .....
Populate a second temp table #DataOut with all the fields you want to output, using an inner join on #CalendarUser to filter the result set. Exclude the field you're calculating with ROW_NUMBER(). Leave the UNION ALL out of this query, append the data from that into #DataOut table as a separate step.
The final output query will be
select
ISNULL( ROW_NUMBER() OVER(ORDER BY CreatedOn, ItemId), -1000)
AS AdminRegisteredCardsId,
#DataOut.*
from #DataOut
Not pleasant to write, feels brute force, but I'm fairly sure you'll see a dramatic performance improvement.
I've been trying to optimize a similar query and came to the conclusion that ordering and filtering across joins has a big impact on performance - my advice would be to denormalize everything that you sort by or filter on using an indexed view and seeing what impact this has on the performance.

Passing dynamic order by in stored procedure

I am creating below stored procedure.
declare #PageNum as Int
declare #PerPageResult as Int
declare #StartDate as varchar(25)
declare #EndDate as varchar(25)
declare #SortType as Varchar(50)
declare #SortDirection as Varchar(4)
set #PageNum=1
set #PerPageResult=20
set #StartDate='2008-02-08'
set #EndDate='2015-02-08'
set #SortType='RegDate'
set #SortDirection='Desc'
declare #Temp Table(RowNum int, RegDate Date, Registered int, Female int, Male int, [Join] int, Rebill int, TotalPointsEarned int, Expire int)
declare #sort varchar(50)
Insert into #Temp
Select ROW_NUMBER() over (order by #SortType+' '+#SortDirection) As RowNum, * From (
SELECT
CAST(m.registrationdate AS Date) as RegDate,
count(m.id) Registered,
count(CASE WHEN m.gender='F' then 'F' end) As Female,
count(CASE WHEN m.gender='M' then 'M' end) As Male
count(CASE WHEN p.paymenttransactiontype='N' then 'N' end) As [Join],
count(CASE WHEN p.paymenttransactiontype='R' then 'R' end) As Rebill,
count(m.tokensearned) As TotalPointsEarned,
count(CASE WHEN p.paymenttransactiontype='E' then 'E' end) As Expire
from member m
join payment p on m.id=p.id_member
join user_role u on u.member_id=m.id
where u.role_id <> 3
and CAST(m.registrationdate AS Date) > #StartDate and CAST(m.registrationdate AS Date) < #EndDate
GROUP BY CAST(m.registrationdate AS Date)
) as aa
Select * from #Temp Where RowNum>((#PageNum-1)*#PerPageResult) and RowNum<=#PerPageResult * #PageNum
Order by #SortType+' '+#SortDirection
In above when i pass the Order by clause dynamically, its not sorting the data properly but when i write column name explicitly, it works fine. Might be its taking #SortType+' '+#SortDirection as varchar rather than Date
I tried writing Order by case when (#Sort='RegDate' and #SortDirection='Desc') Then RegDate End Desc, but it didn't work
How can i pass order by dynamically here.
Edit:
#Andomar: I tried your provided solution and added one more field for Date type. And it didn't work too.
below is what i did.
create table t1 (id int, name varchar(50), dt date);
insert t1 values
(1, 'Chihiro Ogino','2009-02-08'),
(2, 'Spirit of the Kohaku River','2008-02-08'),
(3, 'Yubaba','2012-02-08');
declare #sortColumn varchar(50) = 'dt'
declare #sortOrder varchar(50) = 'ASC'
select *
from t1
order by
case
when #sortOrder <> 'ASC' then 0
when #sortColumn = 'id' then id
end ASC
, case
when #sortOrder <> 'ASC' then ''
when #sortColumn = 'name' then name
end ASC
, case
when #sortOrder <> 'ASC' then ''
when #sortColumn = 'dt' then name
end ASC
, case
when #sortOrder <> 'DESC' then 0
when #sortColumn = 'id' then id
end DESC
, case
when #sortOrder <> 'DESC' then ''
when #sortColumn = 'name' then name
end DESC
, case
when #sortOrder <> 'DESC' then ''
when #sortColumn = 'dt' then name
end DESC
You can use a complicated order by clause. That requires one case for each sort direction and each data type. With this example dataset:
create table t1 (id int, name varchar(50), created date);
insert t1 values
(1, 'Chihiro Ogino', '2012-01-01'),
(2, 'Spirit of the Kohaku River', '2012-01-03'),
(3, 'Yubaba', '2012-01-02');
You could use an order by clause like:
declare #sortColumn varchar(50) = 'created'
declare #sortOrder varchar(50) = 'DESC'
select *
from t1
order by
case
when #sortOrder <> 'ASC' then 0
when #sortColumn = 'id' then id
end ASC
, case
when #sortOrder <> 'ASC' then ''
when #sortColumn = 'name' then name
end ASC
, case
when #sortOrder <> 'ASC' then cast(null as date)
when #sortColumn = 'created' then created
end ASC
, case
when #sortOrder <> 'DESC' then 0
when #sortColumn = 'id' then id
end DESC
, case
when #sortOrder <> 'DESC' then ''
when #sortColumn = 'name' then name
end DESC
, case
when #sortOrder <> 'DESC' then cast(null as date)
when #sortColumn = 'created' then created
end DESC
Working example at SQL Fiddle.
Another option is to create the query dynamically, and run it with exec. For example:
declare #sql nvarchar(max)
set #sql = 'select * from YourTable order by ' + #sortColumn + ' ' + #sortDir
exec (#sql)
#Andomar's answer help solve a similar issue. I needed to sort on any number of 23 different columns, in any order. I ended up with the following:
create table sorting(ID int, columnName varchar(50), sort varchar(10), position int)
insert into sorting
values(1,'column1','DESC',1),
(1,'column2','ASC',2),
...
(1,'columnN','DESC',N)
Adding parameter #sort to the SP to identify the entries in sorting:
ORDER BY ISNULL(STUFF((SELECT ', ' + a.columnName + ' ' + a.sort
FROM sorting a
WHERE a.ID = #sort
ORDER BY a.position ASC
FOR XML PATH('')), 1, 2, ''),NULL)
There are two basic approaches to building dynamically orderable stored procedures:
Pass in the ORDER BY clause as a parameter to the stored procedure. In the stored procedure, build up the SQL statement in a string and then execute this statement using EXEC or sp_ExecuteSql.
-- This Method is used when your Column names are dynamic
-- We need to create a dynamic query and Execute it as shown below.
CREATE PROCEDURE getEmployees ( #OrderByClause varchar(100) ) AS
-- Create a variable #SQLStatement
DECLARE #SQLStatement varchar(255)
-- Enter the dynamic SQL statement into the
-- variable #SQLStatement
SELECT #SQLStatement = 'SELECT EmployeeID, FirstName, LastName, SSN, Salary
FROM Employees ORDER BY '+ #OrderByClause+''
-- Execute the SQL statement
EXEC(#SQLStatement)
Pass in the column to sort by and then use a CASE statement in the ORDER BY clause to order the results according to the input parameter value.
--This method is used when you column name is not dynamic
SELECT EmployeeID, FirstName, LastName, SSN, Salary
FROM Employees
ORDER BY
CASE WHEN #ColumnName='LastName' THEN LastName
WHEN #ColumnName='Salary' THEN CONVERT(varchar(50), Salary)
WHEN #ColumnName='SSN' THEN SSN
END
something like this should work :
ORDER BY
CASE WHEN #SortDirection = 'ASC' THEN #SortType END ASC,
CASE WHEN #SortDirection = 'DESC' THEN #SortType END DESC

ORDER BY with a CASE statement for column with alias

I need a stored procedure which will allow me to return sorted results based on two input parameters: #sortColumnName and #sortDirection. I wrote the following stored procedure, but when I run it, I am getting this error: "Invalid column name 'LastPayCheckDate'."
SELECT Name, SUM(Pay), MAX(PayCheckDate) as LastPayCheckDate
FROM Employee
GROUP BY Name
ORDER BY
CASE WHEN #sortColumnName = 'LastPayCheckDate' AND #sortDirection = 'ASC'
THEN [LastPayCheckDate] END ASC,
CASE WHEN #sortColumnName = 'LastPayCheckDate' AND #sortDirection = 'DESC'
THEN [LastPayCheckDate] END DESC
What is going on? I suppose that t-sql runs the case statement before the select... Am I right? How can I work around this issue?
Thanks for the help!
Try this
ORDER BY
CASE WHEN #sortColumnName = 'LastPayCheckDate' AND #sortDirection = 'ASC'
THEN MAX(PayCheckDate) END ASC,
CASE WHEN #sortColumnName = 'LastPayCheckDate' AND #sortDirection = 'DESC'
THEN MAX(PayCheckDate) END DESC
Example
create table Test (id int, somevalue int)
insert Test values(1,1)
insert Test values(2,1)
insert Test values(3,2)
insert Test values(3,2)
insert Test values(4,2)
run this in 1 shot
declare #sortDirection char(4)
select #sortDirection = 'DESC'
select somevalue, COUNT(*)
from Test
group by somevalue
order by case when #sortDirection = 'ASC'
then COUNT(*) end asc,
case when #sortDirection = 'DESC'
then COUNT(*) end desc
select #sortDirection = 'ASC'
select somevalue, COUNT(*)
from Test
group by somevalue
order by case when #sortDirection = 'ASC'
then COUNT(*) end asc,
case when #sortDirection = 'DESC'
then COUNT(*) end desc
You need to either use the function again or use a subquery if you want to be able to refer to the column alias.
Also, I think that you need to make sure that all of your columns in the case statement get converted to the same data type.