0

This is my scenario:

CREATE TABLE [dbo].[tblSMSSendQueueMain](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SendMethod] [int] NOT NULL
 CONSTRAINT [PK_tblSMSSendQueueLog] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

CREATE TABLE [dbo].[tblSMSSendQueueMainSendStatus](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [QueueID] [int] NULL,
    [SendStatus] [int] NULL,
    [StatusDate] [datetime] NULL,
    [UserID] [int] NULL,
 CONSTRAINT [PK_tblSMSSendQueueMainSendStatus] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

and some indexes:

CREATE NONCLUSTERED INDEX [IX_tblSMSSendQueueMainSendStatus_SendStatus_Single] ON [dbo].[tblSMSSendQueueMainSendStatus]
(
    [SendStatus] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [IX_tblSMSSendQueueMain_SendMethod] ON [dbo].[tblSMSSendQueueMain]
(
    [SendMethod] ASC,
    [ID] DESC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

each table have about 13m rows QueueID column of tblSMSSendQueueMainSendStatus is foreign key of ID column in tblSMSSendQueueMain.

The server has an 8 cores Xeon CPU and 8GB RAM.

I use offset and fetch for my paging plan, its perfect and OK for offset under 100k but when the offset going up (more than 100k), the query response is slow, and takes about 5 or 6 seconds to run.

This is my query:

SELECT q.ID  
FROM tblSMSSendQueueMain q  
INNER JOIN tblSMSSendQueueMainSendStatus qs  
ON q.ID = qs.QueueID  
WHERE 1 = 1  
AND qs.SendStatus = 5  
AND [SendMethod] = 19  
ORDER BY q.ID desc OFFSET 10 * (1000000 - 1) ROWS  
FETCH NEXT 10 ROWS ONLY 

Does anyone have any idea where I am going wrong?

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • 2
    Check the execution plan. – Tim Biegeleisen Apr 12 '21 at 10:57
  • @TimBiegeleisen what do u mean bro? – Amir Hosein Apr 12 '21 at 10:58
  • 2
    https://learn.microsoft.com/en-us/sql/relational-databases/performance/execution-plans?view=sql-server-ver15 – Dale K Apr 12 '21 at 10:59
  • 2
    Paging using `OFFSET` and `FETCH` is an almost inherently inefficient operation because the server literally can't do much more than count off 100K rows before it gets to where you need to be -- there is no notion of "approximately correct", so it will have to do its very best to get you exactly the 10 rows you're asking for. As nobody would manually page through 100K results before getting to the right spot, you're better off seeking using actual values (be they dates or identity values), as those searches can be served by indexes. – Jeroen Mostert Apr 12 '21 at 11:01
  • Not 100K, in this case, @JeroenMostert , but 10 Million ;) (9,999,990 rows for the `OFFSET` and then the `10` for the `FETCH`) – Thom A Apr 12 '21 at 11:05
  • 2
    @Larnu: I mentioned 100K because the OP mentioned that things get slow from 100K onwards. And I mention *this* because I don't want people to think I can't count zeroes when I have to. I totally can, folks, when it matters. :P Whether it gets slow from offset 100K or 1M or 10M doesn't change the underlying issue. – Jeroen Mostert Apr 12 '21 at 11:09
  • 1
    Fair enough, @JeroenMostert . I wasn't trying to imply you couldn't read zeroes either. :) – Thom A Apr 12 '21 at 11:13
  • https://use-the-index-luke.com/sql/partial-results/fetch-next-page – jarlh Apr 12 '21 at 11:31
  • What @JeroenMostert is referring to is called [Keyset Pagination](https://use-the-index-luke.com/no-offset) and it's very very efficient on an indexed key. It also prevents missed rows due to inserts in lower pages. – Charlieface Apr 12 '21 at 12:13

1 Answers1

0

The reason this is so slow is that the only way for the server to get the correct starting row is by reading every single row before it.

You are much better off using Keyset Pagination. Instead of paging by starting row-number, pass in a parameter of the starting key.

For this to work, you must return a unique column or columns, and for this to be performant they should be indexed well.

Pass in @startingRow as the previous batch's highest ID, you can get this any way you like. E.g. I have used an ORDER BY so it will be the last row, or your client app will be able to retrieve it from a variable.

SELECT TOP (10)
    q.ID  
FROM tblSMSSendQueueMain q  
INNER JOIN tblSMSSendQueueMainSendStatus qs  
ON q.ID = qs.QueueID  
WHERE 1 = 1  
   AND qs.SendStatus = 5  
   AND q.[SendMethod] = 19
   AND qs.ID > @startingRow    -- drop this line for the first query
ORDER BY qs.ID;

I must say, your query is somewhat strange. If the foreign key is q.ID = qs.QueueID, then you will get multiple identical results if you are just querying q.ID. I suspect you actually only want q.ID, in which case that is your unique key:

SELECT TOP (10) DISTINCT
    q.ID  
FROM tblSMSSendQueueMain q  
INNER JOIN tblSMSSendQueueMainSendStatus qs  
ON q.ID = qs.QueueID  
WHERE 1 = 1  
   AND qs.SendStatus = 5  
   AND q.[SendMethod] = 19
   AND q.ID > @startingRow    -- drop this line for the first query
ORDER BY q.ID;

Alternatively, I would prefer an EXISTS/IN as it more clearly states the requirement:

SELECT TOP (10)
    q.ID  
FROM tblSMSSendQueueMain q  
WHERE 1 = 1  
   AND q.[SendMethod] = 19
   AND q.ID IN (
      SELECT qs.QueueID
      FROM tblSMSSendQueueMainSendStatus qs
      WHERE qs.SendStatus = 5
   )
   AND q.ID > @startingRow    -- drop this line for the first query
ORDER BY q.ID;
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • in your scenario how can we go to nth page with one action ? for example from page 1 to 100000 – Amir Hosein Apr 12 '21 at 18:21
  • You can't, you just have to make a rough estimate, but that is literally the only downside. See the presentation in the article I linked, also see https://blog.jooq.org/tag/keyset-pagination/ – Charlieface Apr 12 '21 at 18:35
  • i saw the linked article , but the project require an textbox for entering page number and push GO button, I wonder why there is no suitable solution in this regard, a little strange :) – Amir Hosein Apr 12 '21 at 18:54
  • 1
    Quite simple: because the index keys are not numbered by row. I suppose as a one off jump you could use row pagination just to get the starting key, but the idea is generally to page backwards and forwards using index keys only – Charlieface Apr 12 '21 at 20:13