Paging in SQL with no break on primary key - sql

I am new to Stackoverflow. Please forgive me if I have made some mistake.
I have following data in SQL.
Row No. Customer No. Customer Name
------------------------------------------------------------
1 1234 ABCD
2 1234 ABCD
3 1234 ABCD
4 6789 WXYZ
5 6789 WXYZ
6 3456 OPQR
7 4567 JKLM
I need to page the above data with following constraint
Page size : 4 records
If customer no. is splitting between two pages then new customer no. should go on the next page.
output desired:
Paging with 4 records each page
1st Page
Row No. Customer No. Customer Name
------------------------------------------------------------
1 1234 ABCD
2 1234 ABCD
3 1234 ABCD
2nd Page
Row No. Customer No. Customer Name
------------------------------------------------------------
4 6789 WXYZ
5 6789 WXYZ
6 3456 OPQR
7 4567 JKLM
Please help.

You can read page size +1 records using any limiting, then if the last record and the record before the last has the same Customer Name just filter out the customer using HAVING.
Could be tricky and depends on DB you use and could be easily done on presentation layer

Declare #PageNumber INT = 1, #PageSize INT = 4
;with cte1(RowNo,CustomerNo,CustomerName)
AS
(
Select 1,1234,'ABCD' union all
Select 2,1234,'ABCD' union all
Select 3,1234,'ABCD' union all
Select 4,6789,'WXYZ' union all
Select 5,6789,'WXYZ' union all
Select 6,3456,'OPQR' union all
Select 7,4567,'JKLM'
)
select * from cte1 order by RowNo
OFFSET #PageSize * (#PageNumber - 1) ROWS
FETCH NEXT #PageSize ROWS ONLY

This can be done and not to complicated using recursive CTE.
For sample data, I have used query from other answer
WITH cte1(RowNo,CustomerNo,CustomerName)
AS
(
SELECT 1,1234,'ABCD' UNION ALL
SELECT 2,1234,'ABCD' UNION ALL
SELECT 3,1234,'ABCD' UNION ALL
SELECT 4,6789,'WXYZ' UNION ALL
SELECT 5,6789,'WXYZ' UNION ALL
SELECT 6,3456,'OPQR' UNION ALL
SELECT 7,4567,'JKLM'
)
SELECT * INTO #table FROM cte1
I'l add one more row with reapaing user to split this to 3rd page
INSERT #table (RowNo, CustomerNo, CustomerName) VALUES (8,3456,'OPQR')
And here is the solution. I've put some comments to explain parts
WITH CTE_Source AS
(
--I use this to add RN column simply beacuse I can't trust that rowNo column will have no gaps
SELECT *
, ROW_NUMBER() OVER (ORDER BY RowNo) RN
FROM #table t
)
, CTE_R AS
(
--First part is select of first row
SELECT s.RowNo
, s.CustomerNo
, s.CustomerName
, s.RN
, 1 AS Grp --this is a current group of rows
, 1 AS Cnt --counter of how many rows group have
FROM CTE_Source s WHERE RN = 1
UNION ALL
--subsequent select is for next row
SELECT
s.RowNo
, s.CustomerNo
, s.CustomerName
, s.RN
-- increase group when different customer
, CASE WHEN s.CustomerNo = r.CustomerNo THEN r.Grp ELSE r.Grp+Cnt END
-- increase counter when same customer
, CASE WHEN s.CustomerNo = r.CustomerNo THEN r.Cnt + 1 ELSE 1 END
FROM CTE_R r
INNER JOIN CTE_Source s ON s.RN = r.Rn + 1
)
, CTE_Paging AS
(
SELECT *
, CEILING((r.Grp + r.Cnt) / 4.) AS Page -- replace 4 with your page size
FROM CTE_R r
)
SELECT * FROM CTE_Paging --Just add WHERE Page = if you want specific page
OPTION (MAXRECURSION 0) -- for unlimited recursion if you have more than 100 rows

Related

Snowflake: Repeating rows based on column value

How to repeat rows based on column value in snowflake using sql.
I tried a few methods but not working such as dual and connect by.
I have two columns: Id and Quantity.
For each ID, there are different values of Quantity.
So if you have a count, you can use a generator:
with ten_rows as (
select row_number() over (order by null) as rn
from table(generator(ROWCOUNT=>10))
), data(id, count) as (
select * from values
(1,2),
(2,4)
)
SELECT
d.*
,r.rn
from data as d
join ten_rows as r
on d.count >= r.rn
order by 1,3;
ID
COUNT
RN
1
2
1
1
2
2
2
4
1
2
4
2
2
4
3
2
4
4
Ok let's start by generating some data. We will create 10 rows, with a QTY. The QTY will be randomly chosen as 1 or 2.
Next we want to duplicate the rows with a QTY of 2 and leave the QTY =1 as they are.
Obviously you can change all parameters above to suit your needs - this solution works super fast and in my opinion way better than table generation.
Simply stack SPLIT_TO_TABLE(), REPEAT() with a LATERAL() join and voila.
WITH TEN_ROWS AS (SELECT ROW_NUMBER()OVER(ORDER BY NULL)SOME_ID,UNIFORM(1,2,RANDOM())QTY FROM TABLE(GENERATOR(ROWCOUNT=>10)))
SELECT
TEN_ROWS.*
FROM
TEN_ROWS,LATERAL SPLIT_TO_TABLE(REPEAT('hire me $10/hour',QTY-1),'hire me $10/hour')ALTERNATIVE_APPROACH;

Using cross join with multiple variable in cte

I have some order numbers and want to check if any number has been skipped. I'll use left table method which is:
select * from
#CreatedCrossTable (which has all possibilities)
Left Join #MainTableWhichHaveRealSerialNo mt
where
mt is null
Order no structure is: "CodeType.Year.Month.SerialNo". For instance: "DP.21.07.001","DP.21.07.002".. or not DP, but "FB.21.07.001", "FB.21.07.002" etc.
I want to create a cross table schema for determine the skipped SerilNo values (CreatedCrossTable
above):
(Serial number is reset every month)
CodeType | Year | Month | SerialNo
DP 21 1 1
DP 21 1 2
DP 21 1 3
DP 21 1 4
...
(All SerialNos must increase max serial number of the original table's SerialNo (MainTableWhichHaveRealSerialNo) Also codeType,year and month values must match)
DP 21 2 1
DP 21 2 2
...
FB 21 1 1
FB 21 1 2
...
FB 21 1 1
FB 21 1 2
FB 21 1 3
...
Each Codes' and Month's serial number have a different Maximum Number
for creating CrossTable. I've written that code:
;WITH cteSerialNo AS
(
SELECT 1 AS ORDERNO
UNION ALL
SELECT (ORDERNO+1) AS ORDERNO FROM cteSerialNo WHERE ORDERNO < MAX_ORDER_NO
)
,cteMonthYear AS
(
SELECT CAST('2021.01.01' AS DATE) AS Dt
UNION ALL
SELECT DATEADD(MONTH , 1, Dt) AS Dt
FROM cteMonthYear
WHERE DATEADD (MONTH, 1, Dt) < GETDATE()
)
SELECT
*
FROM
(
SELECT
CODES.CODETYPE,
YEAR(Dts.Dt) AS 'YEAR',
MONTH(Dts.Dt) AS 'MONTH'
FROM
##KK_TBL_CODETYPES AS CODES
CROSS JOIN cteMonthYear AS Dts
) AS CROSSTABLE
CROSS JOIN cteSerialNo AS cSN
How can i enter (MAX_ORDER_NO) for each variable in this code?
Assuming that the max SerialNo value is based on the existing values in the SerialNo column, you would want to just find all possible combinations up to that SerialNo value and then remove those that have a match in the source data:
-- Define test data
declare #t table(CodeType varchar(2),[Year] int,[Month] int,SerialNo int);
insert into #t values
('DP',21,1,1)
,('DP',21,1,2)
,('DP',21,1,3)
--,('DP',21,1,4) -- Missing so should be in Output
,('DP',21,1,5)
,('DP',21,2,1)
,('DP',21,2,2)
,('FB',21,1,1)
,('FB',21,1,2)
,('FB',21,2,1)
,('FB',21,2,2)
--,('FB',21,2,3) -- Missing so should be in Output
,('FB',21,2,4)
;
with m as -- Get Max SerialNo for each grouping
(
select CodeType
,[Year]
,[Month]
,max(SerialNo) as MaxSerialNo
from #t
group by CodeType
,[Year]
,[Month]
)
,t as -- Create a table with 10 rows in
(
select t
from(values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) as t(t)
)
,n as -- Self join those 10 rows 5 times to generate a possible 10*10*10*10*10 = 100,000 incrementing numbers using row_number
(
select top(select max(MaxSerialNo) from m) row_number() over (order by (select null)) as n
from t,t t2,t t3,t t4,t t5
)
-- Join from the numbers table to the source data to generate all possible SerialNo values up to the Max
select m.CodeType
,m.[Year]
,m.[Month]
,n.n as SerialNo
from n
left join m
on n.n <= m.MaxSerialNo
except -- Then exclude any that are in the source data
select CodeType
,[Year]
,[Month]
,SerialNo
from #t
order by CodeType
,[Year]
,[Month]
,SerialNo
Output
CodeType
Year
Month
SerialNo
DP
21
1
4
FB
21
2
3

how to select multiple max values from column in sql server?

I have following two tables TBLSession and TBLStudentFeeRecord having following sample datasets
TBLSession:
SessionId SessionName SessionStartMonth SessionEndMonth
1 2018-2019 2018-03-24 2019-02-24
2 2019-2020 2019-01-30 2019-12-30
3 2020-2021 2020-01-30 2021-12-30
TBLStudentFeeRecord:
StudentId SessionId TutionFee BranchId ClassId SectionId
1001 1 1000 1 1 1
1001 2 2000 1 3 1
1001 3 1000 2 2 1
Now,what i am trying to achieve is to select two maximum sessions TutionFee of selected StudentId. I can use max(columnName) to get one maximum value from that particular column. Now how can i get two maximum sessions?
Following dataset is required after querying these table
ResultDataSet:
StudentId SessionId TutionFee SessionName
1001 2 2000 2019-2020
1001 3 1000 2020-2021
What will be the query to achieve above dataset?
You can use SQL Row_Number function with Partition By clause if you want to get the 2 session info per student
Otherwise, use Row_number() function without Partition By clause
You can find explanations for two alternatives in the following SQL CTE statements
;with cte as (
select *,
-- top 2 session per student
-- rn = ROW_NUMBER() over (partition by studentid order by sessionid desc)
-- top 2 sessions
rn = ROW_NUMBER() over (order by sessionid desc)
from TBLStudentFeeRecord
)
select
*
from cte
inner join TBLSession on TBLSession.sessionid = cte.sessionid
where rn <= 2
The output is as follows
You can modify the select list according to your requirements
Since you haven't posted your tries, so try something like this:
Pseudo query:
select top 2 from (select TutionFee from TBLStudentFeeRecord
where StudentId = 1001
order by TutionFee desc )
You could use a subselect Top 2 on the max TutionFee and SessionId
select a.* , t.TutionFee
from TBLSession a
inner JOIN (
select TOP 2 studentID, TutionFee, SessionId
from TBLStudentFeeRecord
where StudentId = 1001
order TutionFee desc, SessionID desc
) t on t.SessionId = a.SessionId
Try this:
SELECT StudentId ,SessionId,TutionFee,SessionName
FROM(
SELECT TSF.StudentId ,TSF.SessionId,TSF.TutionFee,TS.SessionName
,ROW_NUMBER() OVER(PARTITION BY TSF.TutionFee ORDER BY SessionId DESC)RN
FROM TBLStudentFeeRecord TSF
INNER JOIN TBLSession TS ON TS.SessionId=TSF.SessionId
)D
WHERE RN=1
this should work
select TBLStudentFeeRecord.StudentId, TBLStudentFeeRecord.SessionId, TBLStudentFeeRecord.TutionFee, TBLStudentFeeRecord.SessionName
from TBLStudentFeeRecord
inner join TBLSession on TBLSession.SessionId = TBLStudentFeeRecord.SessionId
where TBLStudentFeeRecord.StudentId = 1000
ORDER BY TBLStudentFeeRecord.TutionFee DESC
LIMIT 2
In general query languages are powerful if you face any difficult or any complex scenario just use stored procedure
Two max..........................................
CREATE PROCEDURE twoMaxFrom()
BEGIN
DECLARE max1 DOUBLE;
DECLARE max2 DOUBLE;
DECLARE emp_cursor CURSOR FOR SELECT
TutionFee
FROM TBLStudentFeeRecord;
-- 2. Declare NOT FOUND handler
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
-- 3. Open the cursor
OPEN emp_cursor;
L: LOOP
-- 4. Fetch next element
FETCH emp_cursor
INTO TutionFee;
-- Handler will set finished = 1 if cursor is empty
IF finished = 1
THEN
LEAVE L;
END IF;
SET max1 = 0;
SET max2 = 0;
IF max1 > max2
THEN
SET max1 = TutionFee;
SET max2 = max1;
END IF;
END LOOP;
-- 5. Close cursor when done
CLOSE emp_cursor;
SELECT max1;
SELECT max2;
END;
You can get it by using join and TOP
SELECT TOP 2 StudentId
,t1.SessionId
,TutionFee
,SessionName
FROM TBLSession AS t1
INNER JOIN TBLStudentFeeRecord AS t2 ON t1.SessionId = t2.SessionId
WHERE t2.StudentId = 1001
ORDER BY t1.SessionId DESC

Split a row into multiple rows based on a column value SQL

I have the following "Order Table" :
Item Quantity
pencil 2
pen 1
Notebook 4
I need the result like :
Item Quantity
pencil 1
pencil 1
pen 1
Notebook 1
Notebook 1
Notebook 1
Notebook 1
You didn't specify which RDBMS you are using, so how you generate the numbers will depend on that (maybe a recursive CTE for SQL Server, using DUAL for Oracle, etc.). I've only written code to handle the data that you've shown, but you'll obviously need to account for numbers larger than four in the final solution.
SELECT
MT.sr_no,
MT.item_name,
1 AS quantity
FROM
My_Table MT
INNER JOIN
(
SELECT 1 AS nbr UNION ALL SELECT 2 AS nbr UNION ALL
SELECT 3 AS nbr UNION ALL SELECT 4 AS nbr
) N ON N.nbr <= MT.quantity
You can use the recursive query using common table expression to generate number duplicate rows according to the quantity field as below
WITH cte (sno,item,quantity,rnum)
AS
(
SELECT sno,item,quantity, 1 as rnum
FROM [Order]
UNION ALL
SELECT cte.sno,cte.item,cte.quantity, rnum+1
FROM [Order] JOIN cte ON [Order].sno = cte.sno
AND cte.rnum < [Order].quantity
)
SELECT item,1 AS Quantity
FROM cte
ORDER BY sno

How to get in one query an item and another item having one of its value nearest of the former one?

Imagine I have the following table :
ID || Order
-----------
1 || 1
2 || 2
3 || 5
4 || 20
5 || 100
6 || 4000
(no specific rule applies to the order value).
I want to "move up" ou "move down" items by swapping order values.
Ex: a call to MoveItemUp(4) will results in this new table values :
ID || Order
-----------
1 || 1
2 || 2
3 || 20 <-- swapped order value
4 || 5 <-- swapped order value
5 || 100
6 || 4000
I want to do this in a single query, but I was not yet successful.
The following query works if items order are sequential, with no "hole" (steps of 1 :)
UPDATE dbo.ITEMS
set ORDER = case when c.ORDER = c2.ORDER then c.ORDER +1 else c.ORDER -1 end
from dbo.ITEMS c
inner join dbo.ITEMS c2 on c.ORDER = c2.ORDER or c.ORDER = c2.ORDER + 1
where c2.ID=4
However, I was not able to change this query to support hole. I'm trying to do :
UPDATE dbo.ITEMS
set case when c.ORDER = c2.ORDER then min(c2.ORDER ) else c2.ORDER end
FROM dbo.ITEMS c
inner join ITEMS c2 on c2.ORDER >= c.ORDER
where c2.ID=4
group by c.CAT_ID, c.ORDER
having c.ORDER = min(c2.ORDER ) or c.ORDER = c2.ORDER
However, this does not works as expected (the query updates all items having a greater order instead of the two orders to swap).
PS: I'm working with C# 2.0 on Sybase ASE 4.5, but I assume this question is not specific to this platform. If you have a MSSQL, MySql or Oracle equivalent, I'll put effort to convert it ;)
NOTE All below solutions assume that ItemOrder is unique
EDIT Adding a solution that is more like what OP was trying, and may be more portable to Sybase, this time on Microsoft SQL Server 2008. (See below for solutions using Oracle's analytic functions, that may be more efficient if available.)
First the select to get our row selection criteria correct:
declare #MoveUpId int
set #MoveUpId = 4
select current_row.Id
, current_row.ItemOrder
, prior_row.id as PriorRowId
, prior_row.ItemOrder as PriorItemOrder
, next_row.id as NextRowId
, next_row.ItemOrder as NextItemOrder
from #Items current_row
left outer join #Items prior_row
on prior_row.ItemOrder = (select max(ItemOrder)
from #Items
where ItemOrder < current_row.ItemOrder)
left outer join #Items next_row
on next_row.ItemOrder = (select min(ItemOrder)
from #Items
where ItemOrder > current_row.ItemOrder)
where #MoveUpId in (current_row.id, next_row.id)
Then the update based on the above:
update current_row
set ItemOrder = case
when current_row.Id = #MoveUpId then prior_row.ItemOrder
else next_row.ItemOrder end
from #Items current_row
left outer join #Items prior_row
on prior_row.ItemOrder = (select max(ItemOrder)
from #Items
where ItemOrder < current_row.ItemOrder)
left outer join #Items next_row
on next_row.ItemOrder = (select min(ItemOrder)
from #Items
where ItemOrder > current_row.ItemOrder)
where #MoveUpId in (current_row.id, next_row.id)
Id ItemOrder
1 1
2 2
3 20
4 5
5 100
6 4000
10 -1
20 -2
Set #MoveUpId to 20 and rerun above query results in:
Id ItemOrder
1 1
2 2
3 20
4 5
5 100
6 4000
10 -2
20 -1
but I assume this question is not specific to this platform. The question may not be specific, but the answer probably is. For example, using Oracle, first, a table and some test data:
create table Items (Id number(38) not null
, ItemOrder number);
insert into items values (1, 1);
insert into items values (2, 2);
insert into items values (3, 5);
insert into items values (4, 20);
insert into items values (5, 100);
insert into items values (6, 4000);
insert into items values (10, -1);
insert into items values (20, -2);
commit;
Next create a query that returns just the rows we want to update with their new values for Order. (Which I named ItemOrder, Order being a reserved word and all.) In Oracle this is simpliest using the analytic functions lag and lead:
select *
from (select Id
, ItemOrder
, lead(Id) over (order by Id) as LeadId
, lead(ItemOrder) over (order by Id) as LeadItemOrder
, lag(ItemOrder) over (order by Id) as LagItemOrder
from Items)
where 4 in (Id, LeadId)
order by Id;
ID ITEMORDER LEADID LEADITEMORDER LAGITEMORDER
---------- ---------- ---------- ------------- ------------
3 5 4 20 2
4 20 5 100 5
Convert that into an update statement. However the above query will not create an updateable view (in Oracle), so use merge instead:
merge into Items TRGT
using (select Id
, ItemOrder
, lead(Id) over (order by Id) as LeadId
, lead(ItemOrder) over (order by Id) as LeadItemOrder
, lag(ItemOrder) over (order by Id) as LagItemOrder
from Items) SRC
on (SRC.Id = TRGT.Id)
when matched then update
set ItemOrder = case TRGT.Id
when 4 then SRC.LagItemOrder
else SRC.LeadItemOrder end
where 4 in (SRC.Id, SRC.LeadId);
select * from Items order by Id;
ID ITEMORDER
---------- ----------
1 1
2 2
3 20
4 5
5 100
6 4000
10 -1
20 -2
Unfortunately, I do not believe lag and lead are widely implemented. Microsoft SQL Server, as far as I know, has yet to implement them. No experience with ASE, it they have them great.
Row_number() is more widely implemented. Row_number() can be used to get something that is gap free. (Row_number() is refered to as an analytic function on Oracle and a windowed function on SQL Server.) First the query:
with t as (select Id
, ItemOrder
, row_number() over (order by Id) as RN
from Items)
select current_row.id
, current_row.ItemOrder
, next_row.Id as NextId
, next_row.ItemOrder NextItemOrder
, prior_row.ItemOrder PriorItemOrder
from t current_row
left outer join t next_row on next_row.RN = current_row.RN + 1
left outer join t prior_row on prior_row.RN = current_row.RN - 1
where 4 in (current_row.id, next_row.id);
ID ITEMORDER NEXTID NEXTITEMORDER PRIORITEMORDER
---------- ---------- ---------- ------------- --------------
3 5 4 20 2
4 20 5 100 5
Doing the update, again with merge instead of update. (Oracle does allow the update ... from ... join ... syntax, one may be able to get away with update instead of merge on other platforms.)
merge into Items TRGT
using (with t as (select Id
, ItemOrder
, row_number() over (order by Id) as RN
from Items)
select current_row.id
, current_row.ItemOrder
, next_row.Id as NextId
, next_row.ItemOrder as NextItemOrder
, prior_row.ItemOrder as PriorItemOrder
from t current_row
left outer join t next_row on next_row.RN = current_row.RN + 1
left outer join t prior_row on prior_row.RN = current_row.RN - 1
where 4 in (current_row.id, next_row.id)) SRC
on (TRGT.Id = SRC.Id)
when matched then update
set ItemOrder = case
when TRGT.Id = 4 then SRC.PriorItemOrder
else SRC.NextItemOrder end;
select *
from Items
order by Id;
ID ITEMORDER
---------- ----------
1 1
2 2
3 20
4 5
5 100
6 4000
10 -1
20 -2
NOTE Note the solutions above will write null over OrderItems if matching against the Id for the first row.