CTE, ROW_NUMBER and ROWCOUNT - sql

I am trying to return a page of data and also row count of all data in one stored procedure which looks like following:
WITH Props AS
(
SELECT *,
ROW_NUMBER() OVER (ORDER BY PropertyID) AS RowNumber
FROM Property
WHERE PropertyType = #PropertyType AND ...
)
SELECT * FROM Props
WHERE RowNumber BETWEEN ((#PageNumber - 1) * #PageSize) + 1 AND (#PageNumber * #PageSize);
I am unable to return the row count (highest row number).
I know this has already been discussed (I've seen this:
Efficient way of getting ##rowcount from a query using row_number) but when I add COUNT(x) OVER(PARTITION BY 1) in the CTE, the performance degrades and the query above that normally takes no time takes forever to execute. I reckon it's because the count is calculated for each row? I seems that I can't reuse the CTE in another query. Table Props has 100k records, CTE returns 5k records.

In T-SQL it should be
;WITH Props AS
(
SELECT *,
ROW_NUMBER() OVER (ORDER BY PropertyID) AS RowNumber
FROM Property
WHERE PropertyType = #PropertyType AND ...
)
, Props2 AS
(
SELECT COUNT(*) CNT FROM Props
)
-- Now you can use even Props2.CNT
SELECT * FROM Props, Props2
WHERE RowNumber BETWEEN ((#PageNumber - 1) * #PageSize) + 1 AND (#PageNumber * #PageSize);
now you have CNT in every line... Or you wanted something different? You wanted a second resultset with only the count? Then do it!
-- This could be the second result-set of your query.
SELECT COUNT(*) CNT
FROM Property
WHERE PropertyType = #PropertyType AND ...
Note: reedited, the query 1 David was referencing now has been trashcanned, query 2 is now query 1.

You want the count for the whole resultset right?
does this work speedwise?
SELECT *,(select MAX(RowNumber) from Props) as MaxRow
FROM Props
WHERE RowNumber BETWEEN ((#PageNumber - 1) * #PageSize) + 1
AND (#PageNumber * #PageSize);

I had the same problem and wanted to share the code which would return page and total rows. The problem is fixed by a temporary table. Here is the body of the stored procedure:
DECLARE #personsPageTable TABLE(
RowNumber INT,
PersonId INT,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
BirthDate DATE,
TotalCount INT);
;WITH PersonPage AS
(
SELECT
ROW_NUMBER() OVER(ORDER BY persons.Id) RowNumber,
Id,
FirstName,
LastName,
BirthDate
FROM Persons
WHERE BirthDate >= #BirthDateFrom AND BirthDate <= #BirthDateTo
), TotalCount AS( SELECT COUNT(*) AS [Count] FROM PersonPage)
INSERT INTO #personsPageTable
SELECT *, (select * from TotalCount) TotalCount FROM PersonPage
ORDER BY PersonPage.RowNumber ASC
OFFSET ((#pageNumber - 1) * #pageSize) ROWS
FETCH NEXT #pageSize ROWS ONLY
SELECT TOP 1 TotalCount FROM #personsPageTable
SELECT
PersonId,
FirstName,
LastName,
BirthDate
FROM #personsPageTable
As you can see I put CTE result and total rows into the temporary table and select two queries. The first return total count and the second return page with data.

Related

SQL: How can I select top 1 row, based on different criteria on same column

Say I have a table which has a column named ItemCode, it has fixed format xxx-xxxx where x is [0-9], for example a possible value of ItemCode is 097-1234
Now I would like to select the largest ItemCode which starts with 987 AND the last ItemCode starts with 123, so I am trying to do something like (This is wrong)
SELECT TOP 1 ItemCode From Table
WHERE ItemCode like '987%' OR ItemCode like '123%'
ORDER BY 1 DESC
So how can I write a SQL which can select the last ItemCode of each criteria? Is there any general method which can extend to select top N rows on M such criterias?
(assuming there exists data fulfills the criteria, here 2 rows should be returned: largest ItemCode starts with 987 and largest ItemCode starts with 123)
Another option without UNION, you could use TOP 1 WITH TIES and ROW_NUMBER() OVER() like this
SELECT TOP 1 WITH TIES *
From YourTable
WHERE ItemCode like '987%' OR ItemCode like '123%'
ORDER BY ROW_NUMBER() OVER(PARTITION BY LEFT(ItemCode,3) ORDER BY Itemcode DESC)
You can use ROW_NUMBER() in a CTE for a more generalised form:
;With Ordered as (
select
*,
ROW_NUMBER() OVER (
PARTITION BY SUBSTRING(ItemCode,1,3)
ORDER BY ItemCode desc) as rn
from
Table
where
ItemCode like '987%' or
ItemCode like '123%'
)
select *
from Ordered
where rn = 1
As I alluded to in the comments, if possible I'd change the structure so that the ItemCode elements are separately stored, which would make for a simpler internal query form that could also more easily benefit from indexes. E.g. something like:
;With Ordered as (
select
*,
ROW_NUMBER() OVER (
PARTITION BY ItemCode_Prefix
ORDER BY ItemCode_Suffix desc) as rn
from
Table
where
ItemCode_Prefix in (987,123)
)
select *
from Ordered
where rn = 1
Thanks to #jarlh, I used UNION to achieve what I need.
If anyone has a more general method which may easier to be extended to more criteria, please post an answer and I will accept it. Cheers.
SELECT * FROM(
SELECT TOP 1 ItemCode FROM Table
WHERE ItemCode LIKE '987%'
ORDER BY 1 DESC
) AS A
UNION
SELECT * FROM(
SELECT TOP 1 ItemCode FROM Table
WHERE ItemCode LIKE '123%'
ORDER BY 1 DESC
) AS B
Complete solution based on ROW_NUMBER() function:
use tempdb;
go
-- Test data
create table #test_data
(ItemCode char(8) not null);
insert into #test_data
values
('097-1234'),
('097-1243'),
('097-7890'),
('012-1234'),
('912-1234'),
('123-1234'), -- second max for '987,123'
('123-1234'),
('123-0001'),
('123-0932'),
('987-1234'),
('987-5643'),
('987-7890'), -- first max for '987,123'
('000-7890');
go
-- Test data
-- Code
create proc dbo.top_n_from_m
#criterias varchar(max)
as
set nocount on;
declare #crs table
(id int not null identity (1, 1) primary key,
string char(3) not null);
insert into #crs (string)
select value
from string_split(#criterias, ',')
select t.ItemCode
from
(select t.ItemCode,
c.id,
row_id = row_number() over (partition by c.id order by t.ItemCode desc)
from #test_data as t
join #crs as c on t.ItemCode like c.string + '-%') as t
where t.row_id = 1
order by t.id
go
-- Code
-- Test
execute dbo.top_n_from_m #criterias = '987,123'
select ItemCode
from #test_data
order by ItemCode
-- Test
-- Clear
drop table #test_data;
drop proc dbo.top_n_from_m;
-- Clear

Inner select order by mssql

I need to do order by UserId in this inner select (so before this last WHERE clause is considered) but since I get error
The ORDER BY clause is invalid in views, inline functions...
I cant do it that way. Is there some other way I can order by UserId and DENSE_RANK() by JobId as is now?
Most of UserId values are NULL and when dense_ranked by JobId I need to sort those JobIds so that first are ones where UserId != NULL
SELECT *, #RecordCount as RecordCount
FROM
(
SELECT JobId, UserId, DENSE_RANK() OVER(ORDER BY JobId) AS Rnk
FROM #ListOfJobs
) t
WHERE Rnk between (#pn - 1) * #ps + 1 and #pn * #ps
What's about using CTE for obtaining the dense_rank and then sorting by UserId in the derived result set:
;WITH cte AS (
SELECT JobId, UserId, DENSE_RANK() OVER(ORDER BY JobId) AS Rnk
FROM #ListOfJobs
WHERE Rnk between (#pn - 1) * #ps + 1 and #pn * #ps
)
SELECT * FROM cte
ORDER BY UserId DESC, Rnk
How about using order by for both JobID and UserId.
SELECT TOP 100 PERCENT *, #RecordCount as RecordCount
FROM
(
SELECT JobId, UserId, DENSE_RANK() OVER(ORDER BY JobId) AS Rnk
FROM #ListOfJobs ORDER BY UserId
) t
WHERE Rnk between (#pn - 1) * #ps + 1 and #pn * #ps
If you simply want to first list all the entries with where UserId is specified and then those where it is not, you could just modify your DENSE_RANK expression like this:
DENSE_RANK() OVER (ORDER BY CASE WHEN UserId IS NULL THEN 1 ELSE 0 END, JobId)
That effectively sorts by two columns, of which the first one is a flag that indicates whether UserId is null or not and the second one is JobId. Rows where the flag is 0 will be assigned lower Rnk values than rows where the flag is 1.

Select records from n1 to n2

I have, for example, 10.000 records in my table. Every time I execute the query I get the page number n as a parameter. And I have to select records from n to n*100, which satisfy some complicated condition. I also use ORDER BY, so I can't keep the PrimaryKey of the last element and select top n records using the primary key. How can I achieve this?
Pass the value of From and To parameter in following query on basis of Page calculation.
WITH NumberedMyTable AS
(
SELECT
*,
ROW_NUMBER() OVER (ORDER BY Id) AS RowNumber
FROM
MyTable
)
SELECT
*
FROM
NumberedMyTable
WHERE
RowNumber BETWEEN #From AND #To
declare #n int
set #n=2
with my_query as(
select ROW_NUMBER() over (order by name) as ID, *
from sys.tables
)
select *
from my_query
where ID >=#n and ID<=(#n*10)

Equivalent of LIMIT and OFFSET for SQL Server?

In PostgreSQL there is the Limit and Offset keywords which will allow very easy pagination of result sets.
What is the equivalent syntax for SQL Server?
This feature is now made easy in SQL Server 2012.
This is working from SQL Server 2012 onwards.
Limit with offset to select 11 to 20 rows in SQL Server:
SELECT email FROM emailTable
WHERE user_id=3
ORDER BY Id
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;
ORDER BY: required
OFFSET: optional number of skipped rows
NEXT: required number of next rows
Reference: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql
The equivalent of LIMIT is SET ROWCOUNT, but if you want generic pagination it's better to write a query like this:
;WITH Results_CTE AS
(
SELECT
Col1, Col2, ...,
ROW_NUMBER() OVER (ORDER BY SortCol1, SortCol2, ...) AS RowNum
FROM Table
WHERE <whatever>
)
SELECT *
FROM Results_CTE
WHERE RowNum >= #Offset
AND RowNum < #Offset + #Limit
The advantage here is the parameterization of the offset and limit in case you decide to change your paging options (or allow the user to do so).
Note: the #Offset parameter should use one-based indexing for this rather than the normal zero-based indexing.
select top {LIMIT HERE} * from (
select *, ROW_NUMBER() over (order by {ORDER FIELD}) as r_n_n
from {YOUR TABLES} where {OTHER OPTIONAL FILTERS}
) xx where r_n_n >={OFFSET HERE}
A note:
This solution will only work in SQL Server 2005 or above, since this was when ROW_NUMBER() was implemented.
You can use ROW_NUMBER in a Common Table Expression to achieve this.
;WITH My_CTE AS
(
SELECT
col1,
col2,
ROW_NUMBER() OVER(ORDER BY col1) AS row_number
FROM
My_Table
WHERE
<<<whatever>>>
)
SELECT
col1,
col2
FROM
My_CTE
WHERE
row_number BETWEEN #start_row AND #end_row
Specifically for SQL-SERVER you can achieve that in many different ways.For given real example we took Customer table here.
Example 1: With "SET ROWCOUNT"
SET ROWCOUNT 10
SELECT CustomerID, CompanyName from Customers
ORDER BY CompanyName
To return all rows, set ROWCOUNT to 0
SET ROWCOUNT 0
SELECT CustomerID, CompanyName from Customers
ORDER BY CompanyName
Example 2: With "ROW_NUMBER and OVER"
With Cust AS
( SELECT CustomerID, CompanyName,
ROW_NUMBER() OVER (order by CompanyName) as RowNumber
FROM Customers )
select *
from Cust
Where RowNumber Between 0 and 10
Example 3 : With "OFFSET and FETCH", But with this "ORDER BY" is mandatory
SELECT CustomerID, CompanyName FROM Customers
ORDER BY CompanyName
OFFSET 0 ROWS
FETCH NEXT 10 ROWS ONLY
Hope this helps you.
-- #RowsPerPage can be a fixed number and #PageNumber number can be passed
DECLARE #RowsPerPage INT = 10, #PageNumber INT = 2
SELECT *
FROM MemberEmployeeData
ORDER BY EmployeeNumber
OFFSET #PageNumber*#RowsPerPage ROWS
FETCH NEXT 10 ROWS ONLY
For me the use of OFFSET and FETCH together was slow, so I used a combination of TOP and OFFSET like this (which was faster):
SELECT TOP 20 * FROM (SELECT columname1, columname2 FROM tablename
WHERE <conditions...> ORDER BY columname1 OFFSET 100 ROWS) aliasname
Note: If you use TOP and OFFSET together in the same query like:
SELECT TOP 20 columname1, columname2 FROM tablename
WHERE <conditions...> ORDER BY columname1 OFFSET 100 ROWS
Then you get an error, so for use TOP and OFFSET together you need to separate it with a sub-query.
And if you need to use SELECT DISTINCT then the query is like:
SELECT TOP 20 FROM (SELECT DISTINCT columname1, columname2
WHERE <conditions...> ORDER BY columname1 OFFSET 100 ROWS) aliasname
Note: The use of SELECT ROW_NUMBER with DISTINCT did not work for me.
Adding a slight variation on Aaronaught's solution, I typically parametrize page number (#PageNum) and page size (#PageSize). This way each page click event just sends in the requested page number along with a configurable page size:
begin
with My_CTE as
(
SELECT col1,
ROW_NUMBER() OVER(ORDER BY col1) AS row_number
FROM
My_Table
WHERE
<<<whatever>>>
)
select * from My_CTE
WHERE RowNum BETWEEN (#PageNum - 1) * (#PageSize + 1)
AND #PageNum * #PageSize
end
Another sample :
declare #limit int
declare #offset int
set #offset = 2;
set #limit = 20;
declare #count int
declare #idxini int
declare #idxfim int
select #idxfim = #offset * #limit
select #idxini = #idxfim - (#limit-1);
WITH paging AS
(
SELECT
ROW_NUMBER() OVER (order by object_id) AS rowid, *
FROM
sys.objects
)
select *
from
(select COUNT(1) as rowqtd from paging) qtd,
paging
where
rowid between #idxini and #idxfim
order by
rowid;
There is here someone telling about this feature in sql 2011, its sad they choose a little different keyword "OFFSET / FETCH" but its not standart then ok.
The closest I could make is
select * FROM( SELECT *, ROW_NUMBER() over (ORDER BY ID ) as ct from [db].[dbo].[table] ) sub where ct > fromNumber and ct <= toNumber
Which I guess similar to select * from [db].[dbo].[table] LIMIT 0, 10
Elaborating the Somnath-Muluk's answer just use:
SELECT *
FROM table_name_here
ORDER BY (SELECT NULL AS NOORDER)
OFFSET 9 ROWS
FETCH NEXT 25 ROWS ONLY
w/o adding any extra column.
Tested in SQL Server 2019, but I guess could work in older ones as well.
select top (#TakeCount) * --FETCH NEXT
from(
Select ROW_NUMBER() OVER (order by StartDate) AS rowid,*
From YourTable
)A
where Rowid>#SkipCount --OFFSET
#nombre_row :nombre ligne par page
#page:numero de la page
//--------------code sql---------------
declare #page int,#nombre_row int;
set #page='2';
set #nombre_row=5;
SELECT *
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY etudiant_ID ) AS RowNum, *
FROM etudiant
) AS RowConstrainedResult
WHERE RowNum >= ((#page-1)*#nombre_row)+1
AND RowNum < ((#page)*#nombre_row)+1
ORDER BY RowNum
Since nobody provided this code yet:
SELECT TOP #limit f1, f2, f3...
FROM t1
WHERE c1 = v1, c2 > v2...
AND
t1.id NOT IN
(SELECT TOP #offset id
FROM t1
WHERE c1 = v1, c2 > v2...
ORDER BY o1, o2...)
ORDER BY o1, o2...
Important points:
ORDER BY must be identical
#limit can be replaced with number of results to retrieve,
#offset is number of results to skip
Please compare performance with previous solutions as they may be more efficient
this solution duplicates where and order by clauses, and will provide incorrect results if they are out of sync
on the other hand order by is there explicitly if that's what's needed
I assume that, In C# Expression/LINQ statement of skip and take generating below SQL Command
DECLARE #p0 Int = 1
DECLARE #p1 Int = 3
SELECT [t1].[Id]
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[Id]
FROM [ShoppingCart] AS [t0]
) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN #p0 + 1 AND #p0 + #p1
ORDER BY [t1].[ROW_NUMBER]
In SQL server you would use TOP together with ROW_NUMBER()
Since, I test more times this script more useful by 1 million records each page 100 records with pagination work faster my PC execute this script 0 sec while compare with mysql have own limit and offset about 4.5 sec to get the result.
Someone may miss understanding Row_Number() always sort by specific field. In case we need to define only row in sequence should use:
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
SELECT TOP {LIMIT} * FROM (
SELECT TOP {LIMIT} + {OFFSET} ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ROW_NO,*
FROM {TABLE_NAME}
) XX WHERE ROW_NO > {OFFSET}
Explain:
{LIMIT}: Number of records for each page
{OFFSET}: Number of skip records

SQL question: how to select arbitrary numbers of records in each record group?

I have a table 'articles' : id / uid / last_update / content.
I want to construct a sql statement that selects an arbitrary number(say 3) of records of each user(identified by uid) that are most recently updated from the table. How can I do that?
In SQL Server and Oracle, you can use ROW_NUMBER() to label records per user. The following query tags rn=1 on the latest row for that user, rn=2 on the second latest, and so on:
select *
from (
select
row_number() over (partition by uid order by UpdateDt desc)
as rn
, *
from YourTable
) sub
where rn <= 3
The subquery is required because you can't use ROW_NUMBER() in a WHERE clause directly.
If you're using MySQL, this problem is much harder. Here's a link to a solution with user variables.
DECLARE #Top tinyint;
SELECT #Top = ABS(CHECKSUM(NEWID())) % 5 + 1;
;WITH MyCTE AS
(
SELECT
stuff, things,
ROW_NUMBER() OVER (PARTITION BY uid ORDER BY UpdatedDateTime DESC) AS Ranking
FROM
MyTable
)
SELECT
stuff, things
FROM
MyCTE
WHERE
Ranking <= #Top