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.
Related
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.
I have very weird behavior. If my query have,
,ROW_NUMBER() OVER (ORDER BY CDF.Id) AS [ROW_Number]
Then it will take 1 to 2 seconds. If I have,
,ROW_NUMBER() OVER (ORDER BY CASE '' WHEN '' THEN CDF.Id END) AS [ROW_Number]
Then it will take 1 to 2 seconds. But If I have a variable with empty value,
DECLARE #SortExpression varchar(50)=''
,ROW_NUMBER() OVER (ORDER BY CASE #SortExpression WHEN '' THEN CDF.Id END) AS [ROW_Number]
Then it will take 12 to 16 seconds. In my real query I have some CASE statements CASE WHEN statements in ORDER BY clause. Here is my real query,
,ROW_NUMBER() OVER (
ORDER BY
CASE WHEN #SortExpression = 'MerchantName' THEN M.Name END ASC,
CASE WHEN #SortExpression = '-MerchantName' THEN M.Name END DESC,
CASE WHEN #SortExpression = 'Id' THEN CD.Id END ASC,
CASE WHEN #SortExpression = '-Id' THEN CD.Id END DESC,
CASE WHEN #SortExpression = 'MerchantProductId' THEN CD.MerchantProductId END ASC,
CASE WHEN #SortExpression = '-MerchantProductId' THEN CD.MerchantProductId END DESC,
CASE WHEN #SortExpression = 'Sku' THEN CD.Sku END ASC,
CASE WHEN #SortExpression = '-Sku' THEN CD.Sku END DESC,
CASE WHEN #SortExpression = 'ModelNumber' THEN CD.ModelNumber END ASC,
CASE WHEN #SortExpression = '-ModelNumber' THEN CD.ModelNumber END DESC,
CASE WHEN #SortExpression = 'Offer' THEN CD.Offer END ASC,
CASE WHEN #SortExpression = '-Offer' THEN CD.Offer END DESC,
CASE WHEN #SortExpression = 'Price' THEN CD.Price END ASC,
CASE WHEN #SortExpression = '-Price' THEN CD.Price END DESC,
CASE WHEN #SortExpression = 'NewPrice' THEN CD.NewPrice END ASC,
CASE WHEN #SortExpression = '-NewPrice' THEN CD.NewPrice END DESC,
CASE WHEN #SortExpression = 'InventoryControlType' THEN CD.InventoryControlType END ASC,
CASE WHEN #SortExpression = '-InventoryControlType' THEN CD.InventoryControlType END DESC,
CASE WHEN #SortExpression = 'Inventory' THEN CD.Inventory END ASC,
CASE WHEN #SortExpression = '-Inventory' THEN CD.Inventory END DESC,
CASE WHEN #SortExpression = 'Featured' THEN CD.Featured END ASC,
CASE WHEN #SortExpression = '-Featured' THEN CD.Featured END DESC,
CASE WHEN #SortExpression = 'Visible' THEN CD.Visible END ASC,
CASE WHEN #SortExpression = '-Visible' THEN CD.Visible END DESC,
CASE WHEN #SortExpression = 'Field1' THEN CD.Field1 END ASC,
CASE WHEN #SortExpression = '-Field1' THEN CD.Field1 END DESC,
CASE WHEN #SortExpression = 'Field2' THEN CD.Field2 END ASC,
CASE WHEN #SortExpression = '-Field2' THEN CD.Field2 END DESC,
CASE WHEN #SortExpression = 'Field3' THEN CD.Field3 END ASC,
CASE WHEN #SortExpression = '-Field3' THEN CD.Field3 END DESC,
CASE WHEN #SortExpression = 'Field4' THEN CD.Field4 END ASC,
CASE WHEN #SortExpression = '-Field4' THEN CD.Field4 END DESC,
CASE WHEN #SortExpression = 'OutletCode' THEN CD.OutletCode END ASC,
CASE WHEN #SortExpression = '-OutletCode' THEN CD.OutletCode END DESC,
CASE WHEN #SortExpression = 'Stock' THEN CD.Stock END ASC,
CASE WHEN #SortExpression = '-Stock' THEN CD.Stock END DESC,
CASE WHEN #SortExpression = 'Order' THEN CD.[Order] END ASC,
CASE WHEN #SortExpression = '-Order' THEN CD.[Order] END DESC,
CASE WHEN #SortExpression = 'ErrorDescription' THEN CD.[ErrorDescription] END ASC,
CASE WHEN #SortExpression = '-ErrorDescription' THEN CD.[ErrorDescription] END DESC,
CASE WHEN #SortExpression = 'CreationDateUtc' THEN CD.[CreationDateUtc] END ASC,
CASE WHEN #SortExpression = '-CreationDateUtc' THEN CD.[CreationDateUtc] END DESC,
CDF.Id, CD.[Order]
) AS [ROW_Number]
Just to address the first part, CASE '' WHEN '' THEN CDF.Id END will be optimised away at compile to just CDF.Id, so your first two queries are equivalent.
At compile time the optimiser knows you want to sort by CDF.Id so can generate a plan that utilizes an index on this.
Short Answer
Just add the OPTION (RECOMPILE) query hint, but this will only help if the column you are sorting on is indexed.
The full answer
The problem with your latter example is that the optimiser will create a plan based on an unknown value for #SortExpression, therefore cannot plan to use an appropriate index, since the sort column is unknown.
I created a simple test DDL:
IF OBJECT_ID(N'dbo.T', 'U') IS NOT NULL DROP TABLE dbo.T;
CREATE TABLE dbo.T (A INT, B INT, C INT);
INSERT dbo.T (A, B, C)
SELECT TOP 100000
A = ABS(CHECKSUM(NEWID())) % 1000,
B = ABS(CHECKSUM(NEWID())) % 1000,
C = ABS(CHECKSUM(NEWID())) % 1000
FROM sys.all_objects AS a
CROSS JOIN sys.all_objects AS b;
CREATE INDEX IX_T_A ON dbo.T (A);
CREATE INDEX IX_T_B ON dbo.T (B);
CREATE INDEX IX_T_C ON dbo.T (C);
As a control, I ran:
SELECT TOP 100 A, B, C,
D = ROW_NUMBER() OVER (ORDER BY A)
FROM dbo.T
ORDER BY D;
This gives a plan as such:
The key here is that the optimiser knows that rather than sort the whole table, it only needs the first 100 rows from the index IX_T_A, which is very cheap compared to sorting the table, since the index is already sorted.
This is our optimal plan for a sort on an indexed column. So the aim of the game is to get to this plan while using a variable to define the sort. Just to explain further, I have used TOP because it is representative of what (I assume) you are trying to achieve which is to filter for a certain set of records for paging:
SELECT *
FROM ( SELECT A, B, C,
D = ROW_NUMBER() OVER (ORDER BY A)
FROM dbo.T
) T
WHERE D BETWEEN 150 AND 250;
This gives exactly the same plan, it just means internally the index seek starts further into the index. For the rest of the tests I will continue with TOP as it is shorter.
As explained above, if I run this with the variable, the query plan cannot use the index scan on IX_T_A since it does not know for sure that A will be the sort column, so it just uses a plain old table scan, and has to sort the entire table, rather than being able to just sequentially read from a nonclustered index:
DECLARE #Sort VARCHAR(10) = 'A';
SELECT TOP 100 A, B, C,
D = ROW_NUMBER() OVER (ORDER BY
CASE WHEN #Sort = 'A' THEN A END ASC,
CASE WHEN #Sort = '-A' THEN A END DESC,
CASE WHEN #Sort = 'B' THEN B END ASC,
CASE WHEN #Sort = '-B' THEN B END DESC,
CASE WHEN #Sort = 'C' THEN C END ASC,
CASE WHEN #Sort = '-C' THEN C END DESC)
FROM dbo.T
ORDER BY D;
Query Plan:
The only way around this that I can see is to force recompilation at run-time, so that the redundant sorts can be optimised away, and the correct index used:
DECLARE #Sort VARCHAR(10) = 'A';
SELECT TOP 100 A, B, C,
D = ROW_NUMBER() OVER (ORDER BY
CASE WHEN #Sort = 'A' THEN A END ASC,
CASE WHEN #Sort = '-A' THEN A END DESC,
CASE WHEN #Sort = 'B' THEN B END ASC,
CASE WHEN #Sort = '-B' THEN B END DESC,
CASE WHEN #Sort = 'C' THEN C END ASC,
CASE WHEN #Sort = '-C' THEN C END DESC)
FROM dbo.T
ORDER BY D
OPTION (RECOMPILE);
Query Plan:
As you can see, this has reverted to the plan when the sort column was hard coded. You will have an additional cost at compile time, but this should be less than the additional 10s+ you are seeing in run time.
If you sort on a column without an index then it does not matter whether you recompile or not, it will use the same plan.
The only option that I could think of would require indexes on all the columns. I'm not sure if this is really feasible, but you had all such indexes, then the following might perform well:
(case when #SortExpression = 'MerchantName'
then row_number() over (order by MerchantName)
when #SortExpression = '-MerchantName'
then row_number() over (order by MerchantName desc)
. . .
end)
SQL Server is smart enough to use indexes for row_number(), when possible. And, I'm pretty sure index usage is at the root of the performance difference. It should be smart enough to use indexes even when row_number() is an expression in a case statement.
I will strongly recommend to use dynamic query here. For example:
declare #mainStatement varchar(1000) = 'select * from sometable'
declare #orderingStatement varchar(1000) = ' order by '
if #SortExpression = 'MerchantName'
set #orderingStatement = 'M.Name ASC'
else if #SortExpression = '-MerchantName'
set #orderingStatement = 'M.Name DESC'
else if
......
set #mainStatement = #mainStatement + #orderingStatement
exec(#mainStatement)
This way you will end up with query such us:
select * from sometable order by M.Name ASC
or
select * from sometable order by M.Name DESC
You will do your best and optimize query as much as possible. The rest is the job of DBA. He will add some missing indexes on table`s columns and voilĂ .
I'd like to add a column to a stored procedure that simply counts up from 1, irrespective of how the data is sorted or which page I'm fetching (using offset.)
The stored procedure itself looks like this:
SELECT
e.EntityId, e.HierarchyId, e.ParentNode, e.NgageId, e.NgageParentId,
e.Type, e.IsDeleted, e.LastIndexedDate, e.CurrentVersion, e.Level,
v.EntityVersionID, v.VerisonNotes, v.Title, v.[Description], v.FormID,
v.IsPublic, v.CreatedDate, v.LastModifiedDate, v.ExpirationDate,
d.DocumentID, d.OwnerId AS OwnerId,
o.ADUsername AS OwnerAccount, o.Name AS Author,
d.[Status], d.DocumentType, d.CheckoutUserId AS CheckoutUserId,
c.ADUsername AS CheckoutUserAccount, c.Name AS CheckoutUserName,
d.CheckoutDate,
dbo.GetCommaSeperatedTags(e.EntityID) AS Tags
--INTO #documentsTemp
FROM
dbo.Entities AS e
INNER JOIN
dbo.EntityVersions AS v ON v.EntityID = e.EntityID AND e.CurrentVersion = v.VersionNumber
INNER JOIN
dbo.Documents AS d ON d.EntityVersionID = v.EntityVersionID
INNER JOIN
dbo.Users AS o ON o.UserID = d.OwnerId
LEFT JOIN
dbo.Users AS c ON c.UserID = d.CheckoutUserId
WHERE
[Type] = 5
AND NgageID = #ngageId
AND IsDeleted = 0
AND dbo.CheckPermissions(e.EntityId, #userId, 'LIST') = 1
ORDER BY
CASE WHEN #orderBy = 'Title' THEN v.Title END,
CASE WHEN #orderBy = 'Title' AND #desc = 1 THEN v.Title END DESC,
CASE WHEN #orderBy = 'Author' THEN o.Name END,
CASE WHEN #orderBy = 'Author' AND #desc = 1 THEN o.Name END DESC,
CASE WHEN #orderBy = 'DateModified' THEN v.LastModifiedDate END,
CASE WHEN #orderBy = 'DateModified' AND #desc = 1 THEN v.LastModifiedDate END DESC
OFFSET ((#pageNumber - 1) * #pageSize) ROWS
FETCH NEXT #pageSize ROWS ONLY;
What is the best way to do this?
Thanks,
Joe
I see three options (two of them are already mentioned by Gordon and the third one is not recommended):
do this at the application level
repeat the ORDER BY clause and do some arithmetic
rely on undocumented behaviour, with potentially incorrect results
Here are options 2 and 3 in AdventureWorks (to shorten the queries):
DECLARE #pageSize int=5, #pagenumber int=2, #OrderBy varchar(20)='Name'
SELECT (ROW_NUMBER() OVER (
ORDER BY
CASE WHEN #OrderBy='Name' THEN Name END,
CASE WHEN #OrderBy='ProductNumber' THEN ProductNumber END
)-1)%#pageSize+1 as N, *
FROM Production.Product
ORDER BY
CASE WHEN #OrderBy='Name' THEN Name END,
CASE WHEN #OrderBy='ProductNumber' THEN ProductNumber END
OFFSET ((#pageNumber - 1) * #pageSize) ROWS
FETCH NEXT #pageSize ROWS ONLY
SELECT ROW_NUMBER() OVER (ORDER BY #pageSize) AS RN, *
FROM (
SELECT * FROM Production.Product
ORDER BY
CASE WHEN #OrderBy='Name' THEN Name END,
CASE WHEN #OrderBy='ProductNumber' THEN ProductNumber END
OFFSET ((#pageNumber - 1) * #pageSize) ROWS
FETCH NEXT #pageSize ROWS ONLY
) X
Razvan
Using Microsoft SQL server manager 2008.
Making a stored procedure that will "eventually" select the top 10 on the Pareto list. But I also would like to run this again to find the bottom 10.
Now, instead of replicating the query all over again, I'm trying to see if there's a way to pass a parameter into the query that will change the order by from asc to desc.
Is there any way to do this that will save me from replicating code?
CREATE PROCEDURE [dbo].[TopVRM]
#orderby varchar(255)
AS
SELECT Peroid1.Pareto FROM dbo.Peroid1
GROUP by Pareto ORDER by Pareto #orderby
Only by being slightly silly:
CREATE PROCEDURE [dbo].[TopVRM]
#orderby varchar(255)
AS
SELECT Peroid1.Pareto FROM dbo.Peroid1
GROUP by Pareto
ORDER by CASE WHEN #orderby='ASC' THEN Pareto END,
CASE WHEN #orderby='DESC' THEN Pareto END DESC
You don't strictly need to put the second sort condition in a CASE expression at all(*), and if Pareto is numeric, you may decide to just do CASE WHEN #orderby='ASC' THEN 1 ELSE -1 END * Pareto
(*) The second sort condition only has an effect when the first sort condition considers two rows to be equal. This is either when both rows have the same Pareto value (so the reverse sort would also consider them equal), of because the first CASE expression is returning NULLs (so #orderby isn't 'ASC', so we want to perform the DESC sort.
You might also want to consider retrieving both result sets in one go, rather than doing two calls:
CREATE PROCEDURE [dbo].[TopVRM]
#orderby varchar(255)
AS
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (ORDER BY Pareto) as rn1,
ROW_NUMBER() OVER (ORDER BY Pareto DESC) as rn2
FROM (
SELECT Peroid1.Pareto
FROM dbo.Peroid1
GROUP by Pareto
) t
) t2
WHERE rn1 between 1 and 10 or rn2 between 1 and 10
ORDER BY rn1
This will give you the top 10 and the bottom 10, in order from top to bottom. But if there are less than 20 results in total, you won't get duplicates, unlike your current plan.
try:
CREATE PROCEDURE [dbo].[TopVRM]
(#orderby varchar(255)
AS
IF #orderby='asc'
SELECT Peroid1.Pareto FROM dbo.Peroid1
GROUP by Pareto ORDER by Pareto asc
ELSE
SELECT Peroid1.Pareto FROM dbo.Peroid1
GROUP by Pareto ORDER by Pareto desc
I know it's pretty old, but just wanted to share our solution here, hoping to help someone :)
After some performance tests for several candidate solutions (some of them posted in this thread), we realized you must be really careful with your implementation: your SP performance could be hugely impacted, specially when you combine it with pagination problem.
The best solution we found was to save raw results, ie. just applying filters, in a temporal table (#RawResult in the example), adding afterwards the ORDER BY and OFFSET clauses for pagination. Maybe it's not the prettiest solution (as you are force to copy & paste a clause twice for each column you want to sort), but we were unable to find other better in terms of performance.
Here it goes:
CREATE PROCEDURE [dbo].[MySP]
-- Here goes your procedure arguments to filter results
#Page INT = 1, -- Resulting page for pagination, starting in 1
#Limit INT = 100, -- Result page size
#OrderBy NVARCHAR(MAX) = NULL, -- OrderBy column
#OrderByAsc BIT = 1 -- OrderBy direction (ASC/DESC)
AS
-- Here goes your SP logic (if any)
SELECT
* -- Here goes your resulting columns
INTO
#RawResult
FROM
...
-- Here goes your query data source([FROM], [WHERE], [GROUP BY], etc)
-- NO [ORDER BY] / [TOP] / [FETCH HERE]!!!!
--From here, ORDER BY columns must be copy&pasted twice: ASC and DESC orders for each colum
IF (#OrderByAsc = 1 AND #OrderBy = 'Column1')
SELECT * FROM #RawResult ORDER BY Column1 ASC OFFSET #Limit * (#Page - 1) ROWS FETCH NEXT #Limit ROWS ONLY
ELSE
IF (#OrderByAsc = 0 AND #OrderBy = 'Column1')
SELECT * FROM #RawResult ORDER BY Column1 DESC OFFSET #Limit * (#Page - 1) ROWS FETCH NEXT #Limit ROWS ONLY
ELSE
IF (#OrderByAsc = 1 AND #OrderBy = 'Column2')
SELECT * FROM #RawResult ORDER BY Column2 ASC OFFSET #Limit * (#Page - 1) ROWS FETCH NEXT #Limit ROWS ONLY
ELSE
IF (#OrderByAsc = 0 AND #OrderBy = 'Column2')
SELECT * FROM #RawResult ORDER BY Column2 DESC OFFSET #Limit * (#Page - 1) ROWS FETCH NEXT #Limit ROWS ONLY
ELSE
...
ELSE --Default order, first column ASC
SELECT * FROM #RawResult ORDER BY 1 ASC OFFSET #Limit * (#Page - 1) ROWS FETCH NEXT #Limit ROWS ONLY
This gives you more options
CREATE PROCEDURE [dbo].[TopVRM] #orderby varchar(255) = 'Pareto asc'
DECLARE #SendIt NVARCHAR(MAX)
AS
BEGIN
SET #SendIt = 'SELECT Peroid1.Pareto FROM dbo.Peroid1
GROUP by Pareto ORDER by '+ #orderby
EXEC sp_executesql #SendIt
END
GO
EXEC dbo.TopVRM 'Pareto DESC'
GO
I found this nice example of doing paging in SQL Server, however, I need to do some dynamic ordering. That is, the user passes in an integer, which then gets used to do the ordering, like this:
ORDER BY
CASE WHEN #orderBy = 1 THEN DateDiff(ss, getdate(), received_date) --oldest
WHEN #orderBy = 2 THEN DateDiff(ss, received_date, getdate()) --newest
WHEN #orderBy = 3 THEN message_id --messageid
WHEN #orderBy = 4 THEN LEFT(person_reference, LEN(person_reference)-1) --personid
END
Is it possible to do paging, with this form of dynamic ordering?
What you do instead is move the ORDER BY code into the ROW_NUMBER window function.
Like this example
SELECT * -- your columns
FROM
(
SELECT *, ROWNUM = ROW_NUMBER() OVER (
ORDER BY
CASE WHEN #orderBy = 1 THEN DateDiff(ss, getdate(), received_date) --oldest
WHEN #orderBy = 2 THEN DateDiff(ss, received_date, getdate()) --newest
WHEN #orderBy = 3 THEN message_id --messageid
WHEN #orderBy = 4 THEN LEFT(person_reference, LEN(person_reference)-1) --personid
END
)
FROM TBL
) R
where ROWNUM between ((#pageNumber-1)*#PageSize +1) and (#pageNumber*#PageSize)
The main problem with the complex ORDER BY and the windowing function is that you end up fully materializing the rownum against all rows before returning just one page.