
When a MySQL table contains more rows than we want to display at one time, we can divide the records into pages and show one page inside a Tkinter Treeview.
MySQL LIMIT controls how many records are returned, while OFFSET controls where that page starts.
SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT 5 OFFSET 0
The next page changes the offset:
LIMIT 5 OFFSET 5
and the following page uses:
LIMIT 5 OFFSET 10
Assume the student table contains 23 rows and we want to display five rows per page.
| Page | LIMIT | OFFSET | Rows |
|---|---|---|---|
| 1 | 5 | 0 | 1 to 5 |
| 2 | 5 | 5 | 6 to 10 |
| 3 | 5 | 10 | 11 to 15 |
| 4 | 5 | 15 | 16 to 20 |
| 5 | 5 | 20 | 21 to 23 |
The page size stays fixed:
PAGE_SIZE=5
Only the offset changes.
We need the total number of rows to calculate the number of pages and decide when the Next button should be disabled.
count_query=text('SELECT COUNT(*) FROM student')
with engine.connect() as conn:
total_records=conn.execute(count_query).scalar_one()
If:
total_records=23
PAGE_SIZE=5
the total number of pages is:
total_pages=(total_records+PAGE_SIZE-1)//PAGE_SIZE
Result:
5
query=text('''SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT :limit OFFSET :offset''')
Pass the pagination values separately:
result=conn.execute(
query,
{'limit':PAGE_SIZE,'offset':current_offset}
)
Then collect the rows:
rows=result.mappings().all()
A pagination query should have a predictable row order.
Use:
ORDER BY id
before LIMIT and OFFSET.
ORDER BY, SQL does not guarantee that rows will always be returned in the same order. Stable ordering is important when dividing results into pages.The original program created another Treeview every time my_display() was called. Instead, create it once:
tree=ttk.Treeview(
root,
columns=('id','name','class','mark','gender'),
show='headings',
selectmode='browse',
height=PAGE_SIZE
)
The buttons are also created once.
Only the Treeview items and button states change when a new page is requested.
def load_page(requested_offset):
global current_offset,total_records
count_query=text('SELECT COUNT(*) FROM student')
data_query=text('''SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT :limit OFFSET :offset''')
with engine.connect() as conn:
total_records=conn.execute(count_query).scalar_one()
if total_records:
last_offset=((total_records-1)//PAGE_SIZE)*PAGE_SIZE
current_offset=max(0,min(requested_offset,last_offset))
else:
current_offset=0
rows=conn.execute(
data_query,
{'limit':PAGE_SIZE,'offset':current_offset}
).mappings().all()
The offset is clamped so it cannot go below zero or beyond the final page.
for item in tree.get_children():
tree.delete(item)
Then insert the new records:
for row in rows:
tree.insert(
'',
tk.END,
iid=str(row['id']),
values=(row['id'],row['name'],row['class'],row['mark'],row['gender'])
)
Because the previous page is cleared first, the same MySQL primary key can safely continue to be used as the Treeview iid.
def previous_page():
load_page(current_offset-PAGE_SIZE)
def next_page():
load_page(current_offset+PAGE_SIZE)
def first_page():
load_page(0)
def last_page():
if total_records:
offset=((total_records-1)//PAGE_SIZE)*PAGE_SIZE
load_page(offset)
prev_btn.config(state=tk.DISABLED if current_offset==0 else tk.NORMAL)
next_btn.config(
state=tk.DISABLED
if current_offset+PAGE_SIZE>=total_records
else tk.NORMAL
)
Instead of displaying the raw SQL query to the application user, show useful pagination information.
For example:
Page 3 of 5 | Records 11-15 of 23
Calculate this using:
page_number=current_offset//PAGE_SIZE+1
total_pages=(total_records+PAGE_SIZE-1)//PAGE_SIZE
first_record=current_offset+1
last_record=min(current_offset+PAGE_SIZE,total_records)
import tkinter as tk
from tkinter import ttk, messagebox
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
engine=create_engine('mysql+mysqldb://id:pw@localhost/my_db')
PAGE_SIZE=5
current_offset=0
total_records=0
root=tk.Tk()
root.geometry('620x370')
root.title('MySQL Treeview Pagination - plus2net')
page_var=tk.StringVar()
status_var=tk.StringVar()
tree=ttk.Treeview(root,columns=('id','name','class','mark','gender'),show='headings',selectmode='browse',height=PAGE_SIZE)
tree.grid(row=0,column=0,columnspan=4,padx=20,pady=20)
tree.column('id',width=50,anchor='center')
tree.column('name',width=130,anchor='w')
tree.column('class',width=90,anchor='center')
tree.column('mark',width=70,anchor='center')
tree.column('gender',width=90,anchor='center')
tree.heading('id',text='ID')
tree.heading('name',text='Name')
tree.heading('class',text='Class')
tree.heading('mark',text='Mark')
tree.heading('gender',text='Gender')
def load_page(requested_offset):
global current_offset,total_records
count_query=text('SELECT COUNT(*) FROM student')
data_query=text('''SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT :limit OFFSET :offset''')
try:
with engine.connect() as conn:
total_records=conn.execute(count_query).scalar_one()
if total_records>0:
last_offset=((total_records-1)//PAGE_SIZE)*PAGE_SIZE
current_offset=max(0,min(requested_offset,last_offset))
else:
current_offset=0
rows=conn.execute(data_query,{'limit':PAGE_SIZE,'offset':current_offset}).mappings().all()
except SQLAlchemyError as e:
print(e)
messagebox.showerror('Database Error','Unable to load records.')
return
for item in tree.get_children():
tree.delete(item)
for row in rows:
tree.insert('',tk.END,iid=str(row['id']),values=(row['id'],row['name'],row['class'],row['mark'],row['gender']))
update_navigation()
def update_navigation():
if total_records==0:
page_var.set('No records found')
first_btn.config(state=tk.DISABLED)
prev_btn.config(state=tk.DISABLED)
next_btn.config(state=tk.DISABLED)
last_btn.config(state=tk.DISABLED)
return
total_pages=(total_records+PAGE_SIZE-1)//PAGE_SIZE
page_number=current_offset//PAGE_SIZE+1
first_record=current_offset+1
last_record=min(current_offset+PAGE_SIZE,total_records)
page_var.set(f'Page {page_number} of {total_pages} | Records {first_record}-{last_record} of {total_records}')
first_btn.config(state=tk.DISABLED if current_offset==0 else tk.NORMAL)
prev_btn.config(state=tk.DISABLED if current_offset==0 else tk.NORMAL)
next_btn.config(state=tk.DISABLED if current_offset+PAGE_SIZE>=total_records else tk.NORMAL)
last_btn.config(state=tk.DISABLED if current_offset+PAGE_SIZE>=total_records else tk.NORMAL)
def first_page():
load_page(0)
def previous_page():
load_page(current_offset-PAGE_SIZE)
def next_page():
load_page(current_offset+PAGE_SIZE)
def last_page():
if total_records>0:
last_offset=((total_records-1)//PAGE_SIZE)*PAGE_SIZE
load_page(last_offset)
first_btn=tk.Button(root,text='<< First',command=first_page)
first_btn.grid(row=1,column=0,padx=4)
prev_btn=tk.Button(root,text='< Previous',command=previous_page)
prev_btn.grid(row=1,column=1,padx=4)
next_btn=tk.Button(root,text='Next >',command=next_page)
next_btn.grid(row=1,column=2,padx=4)
last_btn=tk.Button(root,text='Last >>',command=last_page)
last_btn.grid(row=1,column=3,padx=4)
tk.Label(root,textvariable=page_var).grid(row=2,column=0,columnspan=4,pady=12)
load_page(0)
root.mainloop()
The revised load_page() runs:
SELECT COUNT(*) FROM student
every time a page is loaded.
This means the page count and button states are refreshed if another operation adds or deletes rows.
The requested offset is also checked against the current last page:
last_offset=((total_records-1)//PAGE_SIZE)*PAGE_SIZE
current_offset=max(0,min(requested_offset,last_offset))
For example, if deleting the only record on the final page reduces the number of pages, reloading automatically moves the view back to the new final valid page.
LIMIT/OFFSET pagination is simple and works well for learning projects and moderate datasets.
However, a very deep page can require the database to pass over many earlier rows before returning the requested records.
For example:
LIMIT 20 OFFSET 500000
can be less efficient than early pages.
For very large datasets, applications can use indexed filters or keyset pagination based on a value such as the last displayed primary key.
For this beginner Treeview project, LIMIT/OFFSET remains the clearest method because it provides familiar Previous, Next and page-number navigation.
Create Treeview once. Clear and replace its rows when the page changes.
Create the buttons once and update their state.
Pagination requires a predictable record order.
ORDER BY id
next is a Python built-in function. Prefer names such as:
next_offset
or avoid storing it entirely by calculating:
current_offset+PAGE_SIZE
height=5
controls approximately five visible GUI rows.
LIMIT 5
controls how many rows MySQL returns.
Clamp the requested page to zero or greater.
Calculate the last valid offset from the current record count.
Create the SQLAlchemy Engine once and use a connection context when queries are executed.
Use explicit columns:
SELECT id,name,class,mark,gender
when the GUI is designed for those five values.
The database should return only the current page. Do not retrieve thousands of rows and then slice them only in the Tkinter program.
It is the number of database records requested for one page. In this example, PAGE_SIZE=5.
OFFSET tells MySQL how many ordered rows to skip before returning the current page.
ORDER BY gives the query a predictable row sequence so successive pages follow the same ordering rule.
No. Create one Treeview and replace its items when the current page changes.
The total record count and page size determine the final valid offset and total number of pages.
Refreshing the total record count allows the program to recalculate the last valid page and navigation-button states.
It is simple and useful for many applications, but very deep offsets can become inefficient. Larger systems may use indexed or keyset pagination.
COUNT(*) to calculate the total number of records.PAGE_SIZE controls records per page.OFFSET controls the starting position.ORDER BY for predictable pagination.iid.height and SQL LIMIT serve different purposes.Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.