I'm using MySQL to try to manipulate some data for cancer research and machine learning. It seems an ideal problem for the PIVOT statement but I can't quite get it to work and would welcome any help. If there's a better tool, like maybe R, I'm all ears too.
Let's say I have three tables, patients, samples, and mutations:
patients table has unique rows, each with a unique patient_id.
samples table has unique rows, each with a unique sample_id, but also a patient_id that can be found in the patients table. There may be multiple rows in the samples table with the same patient_id.
mutations table has NON-unique rows. Each row in the mutations table contains just two columns: gene and sample_id.
I need to create a new table, call it summary, with patient_id in the first column, sample_id, followed by a column for every distinct gene in the mutations table.
Each row of the new summary table should contain
the patient_id from the patients table,
the sample_id from the samples table,
a the number 1 in each following gene column for each gene in the mutations table that has a sample_id for the specific patient or a number 0 if not.
New summary table looks sort of like this:
patient_id sample_id gene A gene B gene C gene D etc
12345678 54321 1 0 0 0
23456789 65432 0 1 1 0
34567890 76543 0 0 1 0
34567890 87654 0 1 0 1
etc
The new summary table must have an entry, either a 0 or a 1, for every distinct gene found in the mutations table even if there are no entries in the mutations table that have a sample_id belonging to the patient for a specific row.
Remember, there may be multiple samples belonging to the same patient, so the summary table could contain multiple rows for a given patient - each row for a different sample.
Here's my current non-working SQL:
SELECT cs.patient_id, g.*
FROM samples cs
INNER JOIN (
SELECT *
FROM
(WITH cp AS
(SELECT * FROM
(SELECT gene FROM mutations GROUP BY gene) c
CROSS JOIN (SELECT sample_id FROM samples GROUP BY sample_id) m)
SELECT cp.gene, cp.sample_id, IFNULL(m.id,0) id
FROM cp
LEFT JOIN (SELECT gene, sample_id, 1 id FROM mutations) m on m.gene=cp.gene and m.sample_id=cp.sample_id)
PIVOT ( MAX(id) for gene in ('BAP1','PDGFRA','KRAS','CDKN1B','IDH1','ARID1A','DOT1L','NOTCH4','ABL1',
'PBRM1','MLL3','TET2','SPEN','CCND2','DDR2','RICTOR','SMAD4','GLI1','RASA1',
'MAP2K1','CSF3R','HIST1H3D','DNMT3B','CEBPA','GATA2','ARID1B','BRCA2','EPHA7',
'CTNNB1','EPHA5','EP300','RAF1','NF1','EGFR','NBN','INHA','CARD11','ANKRD11',
'ERBB3','TERT','DNMT1','ATM','RIT1','PDCD1','SMARCA4','FOXP1','DICER1','TGFBR2',
'PTPRS','FANCC','APC','NCOA3','NTRK1','PTPRD','NSD1','GRIN2A','SMARCB1','PTCH1',
'KEAP1','KDR','IRS2','PIK3R3','SUFU','STAG2','MAP3K13','SOX9','SETD2','FAT1',
'ZFHX3','NRAS','MAP3K1','ERBB4','JAK3','NF2','PGR','KDM6A','RPTOR','TP53','CIC',
'MSH2','MAP2K4','AXIN2','PTEN','XPO1','ERCC4','AXL','RNF43','DNMT3A','ERG','NOTCH2',
'RFWD2','IGF1R','GATA1','SMAD3','TMPRSS2','MLL','BRAF','TET1','BCOR','YAP1','HLA-A',
'PLCG2','CBL','IRS1','PIK3CA','POLE','LATS2','MST1','H3F3B','IRF4','AR','B2M','NCOR1',
'FUBP1','NOTCH3','ATR','RPS6KB2','TSC2','PIK3CG','MDM2','ROS1','TCF3','TSC1','FGFR2',
'FBXW7','FOXA1','MEN1','CDKN2Ap16INK4A','EPHA3','PMS1','PAK1','E2F3','PIK3CD','PLK2',
'MPL','RHEB','RBM10','ASXL2','MSH6','RAD21','BRIP1','PTPRT','GNA11','CDKN1A','RAD50',
'BRD4','STK11','ARID2','RUNX1','MTOR','JAK1','TBX3','MALT1','RYBP','MLL2','PIK3CB',
'SMO','AXIN1','MAPK3','VHL','JUN','KDM5A','ARID5B','AMER1','PPM1D','ASXL1','MLH1',
'CASP8','BARD1','DAXX','CDH1','PALB2','AKT3','RECQL4','IGF2','MED12','FLT3','HIST3H3',
'MST1R','EIF4A2','CREBBP','STAT5B','PHOX2B','BRCA1','ERBB2','MITF','RB1','CD79A',
'TMEM127','MAPK1','CDKN2A','CDKN2Ap14ARF','CSF1R','FLT4','CENPA','RPS6KA4','SRC',
'ERCC3','NEGR1','RET','ACVR1','SYK','ICOSLG','FYN','SOX17','ETV6','NTRK3','HIST1H1C',
'IDH2','CHEK1','GNAS','PPP6C','EZH2','MYCL1','SDHA','MDC1','ARAF','RAC1','KDM5C','PARP1',
'NKX2-1','CXCR4','SMAD2','IL7R','TGFBR1','U2AF1','SF3B1','FGFR4','ERRFI1','SMARCD1','FGFR1',
'EPHB1','PDPK1','FLCN','RAD54L','MGA','PPP2R1A'))
) g on g.sample_id = cs.sample_id;
Sample data text files
patients - https://drive.google.com/open?id=1NhRkHvvydmZ5ilHJ4TwKE_AslNFvCOcS
samples - https://drive.google.com/open?id=1Txdaa7JKOVMS3TZ8g9tkQUzZPkNc2m24
mutations - https://drive.google.com/open?id=1-HXEszbpcrkPX7MomJnkcAsVCKuzl-rJ
You seem to be over complicating this query when it should be a lot simpler. Here's an example on how to get the first 3 columns, you would just need to copy paste and replace for the rest.
SELECT s.patient_id,
s.sample_id,
MAX( CASE WHEN m.gene = 'BAP1' THEN 1 ELSE 0 END) AS BAP1,
MAX( CASE WHEN m.gene = 'PDGFRA' THEN 1 ELSE 0 END) AS PDGFRA,
MAX( CASE WHEN m.gene = 'KRAS' THEN 1 ELSE 0 END) AS KRAS
FROM samples s
LEFT JOIN mutations m ON s.sample_id = m.sample_id
GROUP BY s.patient_id,
s.sample_id;
If you want to create this query dynamically, you can do it to prevent writing a large amount of code.
DECLARE #Columns NVARCHAR(MAX),
#SQL NVARCHAR(MAX);
SELECT #Columns = ( SELECT CHAR(10) + CHAR(9) + ',MAX( CASE WHEN m.gene = ' + QUOTENAME( gene, '''') + ' THEN 1 ELSE 0 END) AS ' + QUOTENAME(gene)
FROM mutations
GROUP BY gene
FOR XML PATH(''), TYPE).value('./text()[1]', 'nvarchar(max)')
SET #SQL = N'SELECT s.patient_id ' + NCHAR(10)
+ N' ,s.sample_id '
+ #Columns + NCHAR(10)
+ N'FROM samples s ' + NCHAR(10)
+ N'LEFT JOIN mutations m ON s.sample_id = m.sample_id ' + NCHAR(10)
+ N'GROUP BY s.patient_id, ' + NCHAR(10)
+ N' s.sample_id;' + NCHAR(10)
PRINT sp_executesql --For debugging purposes
EXECUTE sp_executesql #SQL --, #ParametersDefinition, #Param1, #Param2, ..., #ParamN
(SQL Server 2012 - Web Edition)
I have a parent/child (one to many) relationship in a query like so:
SELECT a.a, a.b, b.c
FROM tablea INNER JOIN
tableb ON b.pk = a.fk
I have a huge pagination query that encompasses this using the standard (psuedo-code):
WITH C as (SELECT top(#perpage*#pagenum) rowID = row_number() OVER (somefield)),
SELECT c.* FROM C (query) WHERE DT_RowId > (#pagenum-1)*#perpage
The question I have is in this scenario is it possible to paginate off the parent table (a), instead of the entire query? Can I modify my pagination query (not the sql that pulls the query itself) so that when I ask for 10 rows, it gives me 10 rows from the parent, with 'x' number of children attached?
I know I'm not giving the bigger picture here, but the bigger picture is ugly. If need be, we can go there, but it's out there. Here's a small taste of where we're going with this:
IF UPPER(LEFT(#rSQL, 6)) = 'SELECT'
BEGIN
SET #rSQL = 'SELECT * FROM (' + #rSQL + ')' + ' as rTBL';
SET #rSQL = RIGHT(#rSQL, LEN(#rSQL)-7);
IF (LEN(LTRIM(#search)) > 0)
BEGIN
SET #rPaging =
'IF (#schemaonly=1) SET FMTONLY ON;
SELECT #ttlrows = COUNT(*) FROM (SELECT ' + #rSQL + #rWhere + ') AS TBL;
WITH C as (select top(#perpage*#pagenum) DT_RowId = ROW_NUMBER() OVER (' + #rOrder + '), ';
SET #rPaging = #rPaging + #rSQL + #rWhere + ')
SELECT C.*' + #rcols + ', (#perpage-1) * #pagenum as pagenum, #ttlrows as ct, CEILING(#ttlrows / CAST(#perpage AS FLOAT)) as pages
FROM C '+ #query + ' WHERE DT_RowId > (#pagenum-1) * #perpage ';
END
ELSE
BEGIN
SET #rPaging =
'IF (#schemaonly=1) SET FMTONLY ON;
SELECT #ttlrows = COUNT(*) FROM (' + #oSQL + ') AS SUBQUERY;
WITH C as (select top(#perpage*#pagenum) DT_RowId = ROW_NUMBER() OVER (' + #rOrder + '), ';
SET #rPaging = #rPaging + #rSQL + ')
SELECT C.*' + #rcols + ',(#perpage-1) * #pagenum as pagenum, #ttlrows as ct, CEILING(#ttlrows / CAST(#perpage AS FLOAT)) as pages
FROM C '+ #query + ' WHERE DT_RowId > (#pagenum-1) * #perpage ';
END
PRINT #rPaging;
EXECUTE SP_EXECUTESQL #rPaging, #parms, #ttlrows out, #schemaonly, #perpage, #pagenum, #fksiteID, #filter1, #filter2, #filter3, #filter4, #intfilter1, #intfilter2, #intfilter3, #intfilter4, #datefilter1, #datefilter2, #search;
SET FMTONLY OFF;
END
ELSE
BEGIN
SET #rSQL = LTRIM(REPLACE(UPPER(#rSQL), 'EXEC',''));
EXECUTE SP_EXECUTESQL #rSQL, #parms, #ttlrows out, #schemaonly, #perpage, #pagenum, #fksiteID, #filter1, #filter2, #filter3, #filter4, #intfilter1, #intfilter2, #intfilter3, #intfilter4, #datefilter1, #datefilter2;
END
You could do the pagination in a CTE that only gets the parent rows, and then join the child rows in a subsequent CTE or in the main query.
Due to the dynamic way you are using this, this might have to involve building your pagination query from the same building blocks you use to build #query. Without seeing the code that builds #query I can't be much more specific than that.
You could add
,DENSE_RANK() OVER (ORDER BY table_a.primary_key)
This would indirectly provide the same result as
,ROW_NUMBER() OVER(ORDER BY table_a.primary_key)
but the former would be on the final result set instead going back to table a for the latter code snippet.
But please be aware of the disadvantage: any additional ranking function will force an additional sort operation on the result set! This might significantly influence the query performance. If this is the case in your scenario, I'd recommend to follow Tab Allemans solution and use a cte.
I try to capture some statistic parameters for logging purpose. "SET parameters" are no option (i.e. set statistics time on).
So I tried to query some DMV:
select '3AAAAAAAAAAA';
--no GO-statement here
select
total_worker_time/execution_count AS [Avg CPU Time],
total_elapsed_time as [Elapsed Time],
total_rows as [Total rows],
st.text,
(select cast(text as varchar(4000)) from ::fn_get_sql((select sql_handle from sys.sysprocesses where spid = ##spid)))
from sys.dm_exec_query_stats AS qs
cross apply sys.dm_exec_sql_text(qs.sql_handle) AS st
--where ???
order by creation_time desc
The information captured here is almost what I need - but:
The query is only listed in the result of the DMV when it run in last executed GO-Block (not in the actual one). This is not what I need. I need something like ##error or ##rowcount what is available within the same GO-block and holds the elapsed time and CPU time. Any ideas how to query this information of the last statment?
If this can be solved: I would like to query the "last" statement execution within the session (##spid) without writing the statement twice.
Update on question:
This query is working "per session" and would list the requested values (although tirvial querys are missing). Top 1 would always bring back the values of the last Statement (not true if fired via exec #SQL what produces anonther session):
print 'hello';
select top 10 'my personal identifier: 1', * FROM sys.messages;
select top 20 'my personal identifier: 2', * FROM sys.messages;
print 'hello';
select 'hello';
select top 30 'my personal identifier: 3', * FROM sys.tables;
select top 1
total_worker_time/execution_count AS [Avg CPU Time],
total_elapsed_time as [Elapsed Time],
total_rows as [Total rows],
substring(st.text, (qs.statement_start_offset / 2) + 1, (case when qs.statement_end_offset = -1 then datalength(st.text) else qs.statement_end_offset end - qs.statement_start_offset ) / 2 + 5) as [executing statement]
from sys.dm_exec_query_stats AS qs
cross apply sys.dm_exec_sql_text(qs.sql_handle) AS st
where st.text = (select cast(text as varchar(4000)) from ::fn_get_sql((select sql_handle from sys.sysprocesses where spid = ##spid)))
order by qs.statement_start_offset desc;
The filter (where-clause) seems to be crude and not very robust. Is there any way to improve this?
I try to answer myself (Jeroen Mostert - Thank you very much for your help!) - the question is unanswered (see below):
The follwing function should give you CPU, execution time, I/O, number or rows of the last statement that was executed in the actual session, if the statement is complex enough to invoke a SQL plan generation. That is, after simple print commands the resultset would be enpty. Even so after the execution of stored procedures if they open a new session (i.e. after exec sp_executesql the resultset will be empty).
For the "average" SQL-Statement the following query should result in a rowset holding the information that you would otherwise get via set statistice time on and set statistice io on.
drop function if exists dbo.ufn_lastsql_resources ;
go
CREATE FUNCTION dbo.ufn_lastsql_resources (#session_id int)
RETURNS TABLE
AS
return
select
top 1
convert(char(10), getdate(), 121) + ' ' + substring(convert(char(40), getdate(), 121), 12,12) + ',' as [Time stamp],
cast(cast((last_worker_time / execution_count / 1000. ) as numeric(9,2)) as varchar(100)) + ',' as [Avg CPU Time in ms],
cast(cast((last_elapsed_time / 1000. ) as numeric(9,2)) as varchar(100)) + ',' as [Elapsed Time in ms],
cast(last_rows as varchar(100)) + ',' as [Total rows],
cast(substring(st.text, (statement_start_offset / 2) + 1, (case when statement_end_offset = -1 then datalength(st.text) else statement_end_offset end - statement_start_offset ) / 2 + 2) as varchar(4000)) + ','
as [executing statement],
last_physical_reads + last_logical_reads as [Reads],
last_logical_writes as [Writes],
--last_grant_kb,
--last_used_grant_kb,
--last_ideal_grant_kb,
--last_reserved_threads,
--last_used_threads
#session_id as spid
from
(
select qs.*
from sys.dm_exec_query_stats as qs
inner join sys.dm_exec_requests as eq
on qs.sql_handle = eq.sql_handle
and qs.plan_handle = eq.plan_handle
and eq.session_id = #session_id
) a
cross apply sys.dm_exec_sql_text(a.sql_handle) AS st
where
substring(st.text, (statement_start_offset / 2) + 1, (case when statement_end_offset = -1 then datalength(st.text) else statement_end_offset end - statement_start_offset ) / 2 + 2) not like '%ufn_lastsql_resources%'
order by
last_execution_time desc, statement_start_offset desc
go
Most probably there are more elegant ways to do so. Maybe it is possible to write something that will work properly even with statements that use an option (recompile) or on exec (#sql) Anyway: I seems to work on SQL Server 2016 and 2012. You need VIEW SERVER STATE permission on the Server. To invoke the function, try:
drop table if exists #t1
select top 10 'statement 1' a, * into #t1 from sys.messages
select 1, * from dbo.ufn_lastsql_resources(##spid) option (recompile)
drop table if exists #t2
select top 20 'statement 2' a, * into #t2 from sys.messages
--select 2, * from dbo.ufn_lastsql_resources(##spid)
select top 3 'statement 3' a, * from sys.messages
select 3, * from dbo.ufn_lastsql_resources(##spid) option (recompile)
The question remins unanswered, as the way is not working properly. It is not sure to catch the right statement out of the batch (top 1 within the session ordered by last_execution time and last in batch. This seems to be the wrong order. As the plans are reused this is the only way, I figured out to work.)
Please look at the below query..
select name as [Employee Name] from table name.
I want to generate [Employee Name] dynamically based on other column value.
Here is the sample table
s_dt dt01 dt02 dt03
2015-10-26
I want dt01 value to display as column name 26 and dt02 column value will be 26+1=27
I'm not sure if I understood you correctly. If I'am going into the wrong direction, please add comments to your question to make it more precise.
If you really want to create columns per sql you could try a variation of this script:
DECLARE #name NVARCHAR(MAX) = 'somename'
DECLARE #sql NVARCHAR(MAX) = 'ALTER TABLE aps.tbl_Fabrikkalender ADD '+#name+' nvarchar(10) NULL'
EXEC sys.sp_executesql #sql;
To retrieve the column name from another query insert the following between the above declares and fill the placeholders as needed:
SELECT #name = <some colum> FROM <some table> WHERE <some condition>
You would need to dynamically build the SQL as a string then execute it. Something like this...
DECLARE #s_dt INT
DECLARE #query NVARCHAR(MAX)
SET #s_dt = (SELECT DATEPART(dd, s_dt) FROM TableName WHERE 1 = 1)
SET #query = 'SELECT s_dt'
+ ', NULL as dt' + RIGHT('0' + CAST(#s_dt as VARCHAR), 2)
+ ', NULL as dt' + RIGHT('0' + CAST((#s_dt + 1) as VARCHAR), 2)
+ ', NULL as dt' + RIGHT('0' + CAST((#s_dt + 2) as VARCHAR), 2)
+ ', NULL as dt' + RIGHT('0' + CAST((#s_dt + 3) as VARCHAR), 2)
+ ' FROM TableName WHERE 1 = 1)
EXECUTE(#query)
You will need to replace WHERE 1 = 1 in two places above to select your data, also change TableName to the name of your table and it currently puts NULL as the dynamic column data, you probably want something else there.
To explain what it is doing:
SET #s_dt is selecting the date value from your table and returning only the day part as an INT.
SET #query is dynamically building your SELECT statement based on the day part (#s_dt).
Each line is taking #s_dt, adding 0, 1, 2, 3 etc, casting as VARCHAR, adding '0' to the left (so that it is at least 2 chars in length) then taking the right two chars (the '0' and RIGHT operation just ensure anything under 10 have a leading '0').
It is possible to do this using dynamic SQL, however I would also consider looking at the pivot operators to see if they can achieve what you are after a lot more efficiently.
https://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx
I was looking at different ways of writing a stored procedure to return a "page" of data. This was for use with the ASP ObjectDataSource, but it could be considered a more general problem.
The requirement is to return a subset of the data based on the usual paging parameters; startPageIndex and maximumRows, but also a sortBy parameter to allow the data to be sorted. Also there are some parameters passed in to filter the data on various conditions.
One common way to do this seems to be something like this:
[Method 1]
;WITH stuff AS (
SELECT
CASE
WHEN #SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name)
WHEN #SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC)
WHEN #SortBy = ...
ELSE ROW_NUMBER() OVER (ORDER BY whatever)
END AS Row,
.,
.,
.,
FROM Table1
INNER JOIN Table2 ...
LEFT JOIN Table3 ...
WHERE ... (lots of things to check)
)
SELECT *
FROM stuff
WHERE (Row > #startRowIndex)
AND (Row <= #startRowIndex + #maximumRows OR #maximumRows <= 0)
ORDER BY Row
One problem with this is that it doesn't give the total count and generally we need another stored procedure for that. This second stored procedure has to replicate the parameter list and the complex WHERE clause. Not nice.
One solution is to append an extra column to the final select list, (SELECT COUNT(*) FROM stuff) AS TotalRows. This gives us the total but repeats it for every row in the result set, which is not ideal.
[Method 2]
An interesting alternative is given here (https://web.archive.org/web/20211020111700/https://www.4guysfromrolla.com/articles/032206-1.aspx) using dynamic SQL. He reckons that the performance is better because the CASE statement in the first solution drags things down. Fair enough, and this solution makes it easy to get the totalRows and slap it into an output parameter. But I hate coding dynamic SQL. All that 'bit of SQL ' + STR(#parm1) +' bit more SQL' gubbins.
[Method 3]
The only way I can find to get what I want, without repeating code which would have to be synchronized, and keeping things reasonably readable is to go back to the "old way" of using a table variable:
DECLARE #stuff TABLE (Row INT, ...)
INSERT INTO #stuff
SELECT
CASE
WHEN #SortBy = 'Name' THEN ROW_NUMBER() OVER (ORDER BY Name)
WHEN #SortBy = 'Name DESC' THEN ROW_NUMBER() OVER (ORDER BY Name DESC)
WHEN #SortBy = ...
ELSE ROW_NUMBER() OVER (ORDER BY whatever)
END AS Row,
.,
.,
.,
FROM Table1
INNER JOIN Table2 ...
LEFT JOIN Table3 ...
WHERE ... (lots of things to check)
SELECT *
FROM stuff
WHERE (Row > #startRowIndex)
AND (Row <= #startRowIndex + #maximumRows OR #maximumRows <= 0)
ORDER BY Row
(Or a similar method using an IDENTITY column on the table variable).
Here I can just add a SELECT COUNT on the table variable to get the totalRows and put it into an output parameter.
I did some tests and with a fairly simple version of the query (no sortBy and no filter), method 1 seems to come up on top (almost twice as quick as the other 2). Then I decided to test probably I needed the complexity and I needed the SQL to be in stored procedures. With this I get method 1 taking nearly twice as long as the other 2 methods. Which seems strange.
Is there any good reason why I shouldn't spurn CTEs and stick with method 3?
UPDATE - 15 March 2012
I tried adapting Method 1 to dump the page from the CTE into a temporary table so that I could extract the TotalRows and then select just the relevant columns for the resultset. This seemed to add significantly to the time (more than I expected). I should add that I'm running this on a laptop with SQL Server Express 2008 (all that I have available) but still the comparison should be valid.
I looked again at the dynamic SQL method. It turns out I wasn't really doing it properly (just concatenating strings together). I set it up as in the documentation for sp_executesql (with a parameter description string and parameter list) and it's much more readable. Also this method runs fastest in my environment. Why that should be still baffles me, but I guess the answer is hinted at in Hogan's comment.
I would most likely split the #SortBy argument into two, #SortColumn and #SortDirection, and use them like this:
…
ROW_NUMBER() OVER (
ORDER BY CASE #SortColumn
WHEN 'Name' THEN Name
WHEN 'OtherName' THEN OtherName
…
END *
CASE #SortDirection
WHEN 'DESC' THEN -1
ELSE 1
END
) AS Row
…
And this is how the TotalRows column could be defined (in the main select):
…
COUNT(*) OVER () AS TotalRows
…
I would definitely want to do a combination of a temp table and NTILE for this sort of approach.
The temp table will allow you to do your complicated series of conditions just once. Because you're only storing the pieces you care about, it also means that when you start doing selects against it further in the procedure, it should have a smaller overall memory usage than if you ran the condition multiple times.
I like NTILE() for this better than ROW_NUMBER() because it's doing the work you're trying to accomplish for you, rather than having additional where conditions to worry about.
The example below is one based off a similar query I'm using as part of a research query; I have an ID I can use that I know will be unique in the results. Using an ID that was an identity column would also be appropriate here, though.
--DECLARES here would be stored procedure parameters
declare #pagesize int, #sortby varchar(25), #page int = 1;
--Create temp with all relevant columns; ID here could be an identity PK to help with paging query below
create table #temp (id int not null primary key clustered, status varchar(50), lastname varchar(100), startdate datetime);
--Insert into #temp based off of your complex conditions, but with no attempt at paging
insert into #temp
(id, status, lastname, startdate)
select id, status, lastname, startdate
from Table1 ...etc.
where ...complicated conditions
SET #pagesize = 50;
SET #page = 5;--OR CAST(#startRowIndex/#pagesize as int)+1
SET #sortby = 'name';
--Only use the id and count to use NTILE
;with paging(id, pagenum, totalrows) as
(
select id,
NTILE((SELECT COUNT(*) cnt FROM #temp)/#pagesize) OVER(ORDER BY CASE WHEN #sortby = 'NAME' THEN lastname ELSE convert(varchar(10), startdate, 112) END),
cnt
FROM #temp
cross apply (SELECT COUNT(*) cnt FROM #temp) total
)
--Use the id to join back to main select
SELECT *
FROM paging
JOIN #temp ON paging.id = #temp.id
WHERE paging.pagenum = #page
--Don't need the drop in the procedure, included here for rerunnability
drop table #temp;
I generally prefer temp tables over table variables in this scenario, largely so that there are definite statistics on the result set you have. (Search for temp table vs table variable and you'll find plenty of examples as to why)
Dynamic SQL would be most useful for handling the sorting method. Using my example, you could do the main query in dynamic SQL and only pull the sort method you want to pull into the OVER().
The example above also does the total in each row of the return set, which as you mentioned was not ideal. You could, instead, have a #totalrows output variable in your procedure and pull it as well as the result set. That would save you the CROSS APPLY that I'm doing above in the paging CTE.
I would create one procedure to stage, sort, and paginate (using NTILE()) a staging table; and a second procedure to retrieve by page. This way you don't have to run the entire main query for each page.
This example queries AdventureWorks.HumanResources.Employee:
--------------------------------------------------------------------------
create procedure dbo.EmployeesByMartialStatus
#MaritalStatus nchar(1)
, #sort varchar(20)
as
-- Init staging table
if exists(
select 1 from sys.objects o
inner join sys.schemas s on s.schema_id=o.schema_id
and s.name='Staging'
and o.name='EmployeesByMartialStatus'
where type='U'
)
drop table Staging.EmployeesByMartialStatus;
-- Populate staging table with sort value
with s as (
select *
, sr=ROW_NUMBER()over(order by case #sort
when 'NationalIDNumber' then NationalIDNumber
when 'ManagerID' then ManagerID
-- plus any other sort conditions
else EmployeeID end)
from AdventureWorks.HumanResources.Employee
where MaritalStatus=#MaritalStatus
)
select *
into #temp
from s;
-- And now pages
declare #RowCount int; select #rowCount=COUNT(*) from #temp;
declare #PageCount int=ceiling(#rowCount/20); --assuming 20 lines/page
select *
, Page=NTILE(#PageCount)over(order by sr)
into Staging.EmployeesByMartialStatus
from #temp;
go
--------------------------------------------------------------------------
-- procedure to retrieve selected pages
create procedure EmployeesByMartialStatus_GetPage
#page int
as
declare #MaxPage int;
select #MaxPage=MAX(Page) from Staging.EmployeesByMartialStatus;
set #page=case when #page not between 1 and #MaxPage then 1 else #page end;
select EmployeeID,NationalIDNumber,ContactID,LoginID,ManagerID
, Title,BirthDate,MaritalStatus,Gender,HireDate,SalariedFlag,VacationHours,SickLeaveHours
, CurrentFlag,rowguid,ModifiedDate
from Staging.EmployeesByMartialStatus
where Page=#page
GO
--------------------------------------------------------------------------
-- Usage
-- Load staging
exec dbo.EmployeesByMartialStatus 'M','NationalIDNumber';
-- Get pages 1 through n
exec dbo.EmployeesByMartialStatus_GetPage 1;
exec dbo.EmployeesByMartialStatus_GetPage 2;
-- ...etc (this would actually be a foreach loop, but that detail is omitted for brevity)
GO
I use this method of using EXEC():
-- SP parameters:
-- #query: Your query as an input parameter
-- #maximumRows: As number of rows per page
-- #startPageIndex: As number of page to filter
-- #sortBy: As a field name or field names with supporting DESC keyword
DECLARE #query nvarchar(max) = 'SELECT * FROM sys.Objects',
#maximumRows int = 8,
#startPageIndex int = 3,
#sortBy as nvarchar(100) = 'name Desc'
SET #query = ';WITH CTE AS (' + #query + ')' +
'SELECT *, (dt.pagingRowNo - 1) / ' + CAST(#maximumRows as nvarchar(10)) + ' + 1 As pagingPageNo' +
', pagingCountRow / ' + CAST(#maximumRows as nvarchar(10)) + ' As pagingCountPage ' +
', (dt.pagingRowNo - 1) % ' + CAST(#maximumRows as nvarchar(10)) + ' + 1 As pagingRowInPage ' +
'FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY ' + #sortBy + ') As pagingRowNo, COUNT(*) OVER () AS pagingCountRow ' +
'FROM CTE) dt ' +
'WHERE (dt.pagingRowNo - 1) / ' + CAST(#maximumRows as nvarchar(10)) + ' + 1 = ' + CAST(#startPageIndex as nvarchar(10))
EXEC(#query)
At result-set after query result columns:
Note:
I add some extra columns that you can remove them:
pagingRowNo : The row number
pagingCountRow : The total number of rows
pagingPageNo : The current page number
pagingCountPage : The total number of pages
pagingRowInPage : The row number that started with 1 in this page