Efficient pagination of a SQL table with 100M records
allyouneedisbackend.com
allyouneedisbackend.com
If you need to do operations like this on a database with this setup I'd suggest checking out the following config options:
1. max_heap_table_size
2. read_buffer_size
3. sort_buffer_size
4. join_buffer_size
5. thread_concurrency
6. tmp_table_size
I'd also suggest looking into TEMPORARY TABLEs with ENGINE=MEMORY.I have not included values in this because the values that make sense for you are likely not the values that make sense for me. Check out MySQL's documentation for what these values effect [0]. The defaults in even MySQL's huge config are very outdated for the horsepower that modern computers bring to the table. It's funny that MySQL's once massive 4GB of RAM config file is now the appropriate setup for my laptop.
[0] - https://dev.mysql.com/doc/refman/5.7/en/server-system-variab...
Use it, look at the suggestions, and use that as an excellent springboard for reading up on what and why, and see how it could apply to your database.
Write activity impacts database query caches. Many applications do not require realtime-accurate results from the database and generate a substantial number of the same paginated queries. For these cases, it is very important to consider higher-level caches within the system -- CDN, page-level caches, page block-level caches, etc. as caching your 99% traffic pattern will provide DB platform headroom to support your 1% traffic pattern.
Where the results you are paginating are based on any sort of matching (SQL WHERE), most read applications see a sizable benefit in integrating a search platform. Data selection for display is handled in the search layer, and underlying data retrieval for display happens either from the search layer or via the backing database using inexpensive lookups via primary key.
One of the key considerations not covered in the article is the need for result consistency when paginating, e.g. if the underlying data changes. It is the need for this consistency, not the desire for performance, that I see as the primary reason to include primary key identifiers or timestamp values in your pagination strategy.
And then we got that one customer that somehow backfilled old timestamped data and complained our pagination broke :(
I've also dealt with databases without column-level privileges. Where not handled by a column-level privilege system, it may still be possible to block this sort of UPDATE using a TRIGGER designed to fail.
Privilege grant (or drop of the trigger) could be used when the system is in an offline maintenance mode should you require the ability to correct that protected data, reinstituting the control when the maintenance is complete.
The criticality of the data and the level of automation in use would probably be the factors I would use to decide whether this overhead was warranted. Hopefully you had backups available.
It occurred when the clock on the hardware got messed up :(
If folks knew how to get the most out of their databases, 90% of them wouldn't even need the other systems and the complexity of the system as a whole would be reduced.
Even experts fall into this trap, actually:
https://sqlperformance.com/2015/01/t-sql-queries/pagination-...
I've invested several months of my life optimizing arbitrary sort and filter for MySQL results over tables varying from 100k rows to 100M (using time windows to scope things). As long as you can efficiently cut down the underlying data set into the region of 100k using some kind of window - usually recency based - and convince MySQL to filter by this before it sorts or does any other kind of filter - then limit / offset pagination doesn't hurt.
You can then use higher-level pagination on the recency window if it becomes necessary.
You can arrange for indexes on the most commonly used columns, so that e.g. initial default page load is fast, while leaving the user hanging for up to a second or two on the more unusual queries.
It turns out that when people have better filtering and sorting tools, they spent a lot less time digging for page 10,001.
Who else is to blame? Should every vendor turn the documentation into a textbook on relational databases and their intrinsic properties?
One can only hope.
I typed out the example queries below from memory, hopefully I haven't made any logic errors. Note:
a) Requires additional indexes to make it efficient.
b) Accounts for inserted and deleted rows.
c) You cannot use page numbers. You can only do first page, next and previous pages, and last page.
d) Example is for 20 items per page. The query pulls 21 (+1) items, so app can determine whether there is a next or previous page.
e) In a web app or api for example, your parameters get crazy, such as: /users?page=next&sort=name&sortId=20&sortVal=George
-- first page
SELECT name, id
FROM users
GROUP BY name, id
ORDER BY name, id
LIMIT 21;
-- next page (ex: last item on current
-- page has id 20 and name 'George')
SELECT name, id
FROM users
WHERE name >= 'George' AND id > 20
GROUP BY name, id
ORDER BY name, id
LIMIT 21;
-- prev page (ex: first item on current
-- page has id 21 and name 'Harry')
SELECT name, id
FROM users
WHERE name <= 'Harry' AND id < 21
GROUP BY name, id
-- need DESC, reverse order in app for display
ORDER BY name DESC, id DESC
LIMIT 21;
-- last page
SELECT name, id
FROM users
GROUP BY name, id
-- need DESC, reverse order in app for display
ORDER BY name DESC, id DESC
LIMIT 21;> c) You cannot use page numbers. You can only do first page, next and previous pages, and last page.
To get to page 9, you start on first page. Then "next page" 8 times. This is why you see this kind of UI fairly often. First (<<), previous (<), next (>), last (>>). No numbers.
Technically you can do relative page numbers (ie: current page + 8) by increasing the LIMIT of the query and skipping over results. It's really messy to handle and you can never use "real", absolute page numbers, because inserted and deleted records can change how many next or previous pages there will be.
Are you sure it will be faster than just using offset/limit on the server side?
The idea here is to sort by name, right? What happens when "Ingrid" should be on the "next page" but has ID 1?
"Five ways to paginate in Postgres, from the basic to the exotic"
https://www.citusdata.com/blog/2016/03/30/five-ways-to-pagin...
https://news.ycombinator.com/item?id=15446855 (Oct 2017, 42 comments)
I'm leaning with @jerf that the LIMIT/OFFSET approach can trap you on the 100M record scale, however in all honesty, most of us don't deal with databases at this size. As @alvil stated, his approach works fine for 1M records, which is way more then most personal and application databases will contain.
Maybe the solution is for the database vendor to actually implement and document a definitive solution for this. Maybe re-engineering the LIMIT/OFFSET approach under the hood to take advantage of this new way.
As for the article... I really have a personal problem when authors get lazy and take the easy way out by assuming everyone knows what they are talking about. Take this portion of the article
========================
Simplified algorithm:
We get PAGE_SIZE number of records from the table. Starting offset value is 0.
Use the max returned value for user_id in the batch as the offset for the next page.
Get the next batch from the records which have user_id value higher than current offset.
========================
The author took the time to write an entire article debating and demonstrating their solution to the whole pagination problem and they couldn't take 5 minutes to show the code behind these steps in their solution?
As I stated, it's just a personal thing.
UPDATE: Down the internet rabbit hole I go. Here is a very nice article similarly demonstrating and debating the LIMIT/OFFSET approach in MSSQL between their OFFSET/FETCH and CTE.
https://sqlperformance.com/2015/01/t-sql-queries/pagination-...
This is also, more broadly, something I've been thinking about recently. It feels (to me) like industry knowledge is getting lost somewhere, because you see people trying to reinvent the wheel in a lot of technical areas. One of them is data: there are proven approaches to most problems faced by the average organization, especially when it comes to designing and managing a data platform. Yet, you see people rolling their own approaches/designs/methodologies to basic things like ingestion, ETL and data modelling when the traditional approaches would suffice and would take half the time to implement and one third of the effort to maintain. It's great that people try to innovate, but what I see regularly is more like people trying to solve a solved problem without bothering to educate themselves on how it was solved so far.
Assuming you can’t toss history for business or compliance reasons, tables with 100M rows become commmon when an app has been in service for more than a few years. Even in SMBs.
How would you give a link to page 9 when you're on page 1?
As someone who often skips a few pages when navigating such interfaces, I say "thanks, but no thanks".
What? Why? It should just instantiate a cursor and return the first batch size worth of records.
I’m not sure why the dB pulls load the entire result set into ram to return results unless it requires a sort on a non indexed field.
Perhaps it's his client that is OOM-ing? Back in the day, I remember the PHP mysql client loading the entire result set into a local buffer unless you explicitly asked for the output to be streamed.
Edit: Found reference to PHP behavior I mentioned: http://php.net/manual/en/mysqlinfo.concepts.buffering.php
> You need to walk thru the table,
> extract each record,
> transform it inside your application’s code
> and insert to another place.
The writer said his database died in the middle of the query when he selected the 100,000,000 rows without first breaking them into pages.It looks like he's using MySQL. I've used PostgreSQL for over a decade, and I just don't see this happening, though I've tested only on a table with a few million rows. But anyway I doubt PostgreSQL loads the whole table into memory when you select it. It seems to load just parts at a time, as needed. And for what it's worth, an offset of a few million rows still took just a few seconds.
Actually what I would first try to is dump the table to a file, transform it if possible with Linux command-line tools like sed and awk, and then load the file into the new table.
Can anyone confirm if Postgres would die like MySQL did on too big a dataset --- selecting everything without paginating but fetching just one row at a time in the application?
Depends on the client library. A lot of clientside SQL interfaces load all rows into arrays of hashes or such into memory.
I've tested postgres with > 250m rows. It has problems. I've also tested to 1b rows, same issue.
Now throw in some sum, count and group by functions, more issues.
Add a join? Now you've major problems. Add several joins? Woah there!
I've been able to bring a 5 node citusDB cluster to it's knees. Which the only solution was to scale out massively to double digits servers. But you can't do that on a limited budget.
The caveat here, this was about 2 years ago now. I don't know if there have been any improvements since that time.
I don't have a postgres database now, I've since migrated over to memsql as the majority of my work is OLAP.
I’ve never been able to bring PGSQL to its knees.
This is called an "unbuffered query". The problem usually isn't with the number of rows you're trying to read; it's about what the database must do on its end before it can know which rows to send first. ORDER BY being one example wherein, unless you have perfect indexes including the sorted column(s), the database has to generate the entire resultset before even sending the first row. That means writing potentially millions of rows' worth of information to memory or - more commonly at large sizes - to a temporary table on disk.
Unbuffered queries are fairly rare in the "real world". While in theory it means you're parsing results faster and using less memory on the application side, it introduces difficulties like the fact you can't run a subsequent query until you've finished reading all rows from the original query.
When an application is reading this many rows with a single query, it's usually an indication that the app is poorly written. Of course there are exceptions, though typically reserved for maintenance scripts, reporting, data migrations, etc.
The only thing this approach does not support is jumping to an arbitrary page number.
SELECT *
FROM sales
WHERE (sale_date, sale_id) < (?, ?)
ORDER BY sale_date DESC, sale_id DESC
FETCH FIRST 10 ROWS ONLY
This is a postgresql example. See use-the-index-luke[0] for a generic approach.[0] http://use-the-index-luke.com/sql/partial-results/fetch-next...
1000 is tiny by DB standards but solutions like this one are often bolted without considerations.
I note this use case can be solved using keyset pagination instead of offset/limit, because the ordering of messages is stable: messages are never inserted in the middle of the list; they are appended at the end. We can attribute a number to each message, and use this message to filter and sort.
To do that aforedescribed accounting for us, and more.
If we have an pagination index over (topicId, deleted) on postTime asc.
/threads?id=${USER_NAME}&next=${NEXT_COMMENT_ID} $items = db("SELECT a,b,c FROM item
JOIN (SELECT id
FROM item
WHERE
<your-where-conditions>
ORDER BY id DESC
LIMIT <offset>, <limit>)
AS x ON
x.id = item.id");