Skip to content

Commit 8d35c53

Browse files
committed
Implement a server-side cursor to improve performance when fetching large volumes of data. pgadmin-org#5797
1 parent cab1bc3 commit 8d35c53

File tree

18 files changed

+380
-117
lines changed

18 files changed

+380
-117
lines changed
46.8 KB
Loading
208 KB
Loading

docs/en_US/query_tool.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,36 @@ To execute a macro, simply select the appropriate shortcut keys, or select it fr
558558
.. image:: images/query_output_data.png
559559
:alt: Query Tool Macros Execution
560560
:align: center
561+
562+
563+
Server Side Cursor
564+
******************
565+
566+
Server-side cursors allow partial retrieval of large datasets, making them particularly useful when working with
567+
very large result sets. However, they may offer lower performance in typical, everyday usage scenarios.
568+
569+
To enable server-side cursors:
570+
571+
* Go to Preferences > Query Tool > Options and set "Execute with server-side cursor?" to True.
572+
573+
.. image:: images/query_tool_server_cursor_preference.png
574+
:alt: Query Tool Manage Macros Clear row confirmation
575+
:align: center
576+
577+
* Alternatively, you can enable it on a per-session basis via the Query Tool’s Execute menu.
578+
579+
.. image:: images/query_tool_server_cursor_execute_menu.png
580+
:alt: Query Tool Manage Macros Clear row confirmation
581+
:align: center
582+
583+
584+
Limitations:
585+
586+
1. Transaction Requirement: Server-side cursors work only in transaction mode.
587+
If enabled pgAdmin will automatically ensure queries run within a transaction.
588+
589+
2. Limited Use Case: Use server-side cursors only when fetching large datasets.
590+
591+
3. Pagination Limitation: In the Result Grid, the First and Last page buttons will be disabled,
592+
as server-side cursors do not return a total row count. Consequently, the total number of rows
593+
will not be displayed after execution.

web/pgadmin/static/js/helpers/ObjectExplorerToolbar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function ObjectExplorerToolbar() {
7373
<Box display="flex" alignItems="center" gap="2px">
7474
<PgButtonGroup size="small">
7575
<ToolbarButton icon={<QueryToolIcon />} menuItem={menus['query_tool']} shortcut={browserPref?.sub_menu_query_tool} />
76-
<ToolbarButton icon={<ViewDataIcon />} menuItem={menus['view_all_rows_context'] ??
76+
<ToolbarButton icon={<ViewDataIcon />} menuItem={menus['view_all_rows_context'] ??
7777
{label :gettext('All Rows')}}
7878
shortcut={browserPref?.sub_menu_view_data} />
7979
<ToolbarButton icon={<RowFilterIcon />} menuItem={menus['view_filtered_rows_context'] ?? { label : gettext('Filtered Rows...')}} />

web/pgadmin/tools/sqleditor/__init__.py

Lines changed: 80 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ def get_exposed_url_endpoints(self):
146146
'sqleditor.get_new_connection_user',
147147
'sqleditor._check_server_connection_status',
148148
'sqleditor.get_new_connection_role',
149-
'sqleditor.connect_server'
149+
'sqleditor.connect_server',
150+
'sqleditor.server_cursor',
150151
]
151152

152153
def on_logout(self):
@@ -203,9 +204,14 @@ def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
203204
"""
204205

205206
if request.data:
206-
filter_sql = json.loads(request.data)
207+
_data = json.loads(request.data)
207208
else:
208-
filter_sql = request.args or request.form
209+
_data = request.args or request.form
210+
211+
filter_sql = _data['filter_sql'] if 'filter_sql' in _data else None
212+
server_cursor = _data['server_cursor'] if\
213+
'server_cursor' in _data and (_data['server_cursor'] == 'true' or
214+
_data['server_cursor'] is True) else False
209215

210216
# Create asynchronous connection using random connection id.
211217
conn_id = str(secrets.choice(range(1, 9999999)))
@@ -242,8 +248,9 @@ def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
242248
command_obj = ObjectRegistry.get_object(
243249
obj_type, conn_id=conn_id, sgid=sgid, sid=sid,
244250
did=did, obj_id=obj_id, cmd_type=cmd_type,
245-
sql_filter=filter_sql
251+
sql_filter=filter_sql, server_cursor=server_cursor
246252
)
253+
247254
except ObjectGone:
248255
raise
249256
except Exception as e:
@@ -354,6 +361,8 @@ def panel(trans_id):
354361
if 'database_name' in params:
355362
params['database_name'] = (
356363
underscore_escape(params['database_name']))
364+
params['server_cursor'] = params[
365+
'server_cursor'] if 'server_cursor' in params else False
357366

358367
return render_template(
359368
"sqleditor/index.html",
@@ -485,6 +494,8 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
485494
kwargs['auto_commit'] = pref.preference('auto_commit').get()
486495
if kwargs.get('auto_rollback', None) is None:
487496
kwargs['auto_rollback'] = pref.preference('auto_rollback').get()
497+
if kwargs.get('server_cursor', None) is None:
498+
kwargs['server_cursor'] = pref.preference('server_cursor').get()
488499

489500
try:
490501
conn = manager.connection(conn_id=conn_id,
@@ -544,6 +555,7 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
544555
# Set the value of auto commit and auto rollback specified in Preferences
545556
command_obj.set_auto_commit(kwargs['auto_commit'])
546557
command_obj.set_auto_rollback(kwargs['auto_rollback'])
558+
command_obj.set_server_cursor(kwargs['server_cursor'])
547559

548560
# Set the value of database name, that will be used later
549561
command_obj.dbname = dbname if dbname else None
@@ -909,8 +921,13 @@ def start_view_data(trans_id):
909921

910922
update_session_grid_transaction(trans_id, session_obj)
911923

924+
if trans_obj.server_cursor:
925+
conn.execute_void("BEGIN;")
926+
912927
# Execute sql asynchronously
913-
status, result = conn.execute_async(sql)
928+
status, result = conn.execute_async(
929+
sql,
930+
server_cursor=trans_obj.server_cursor)
914931
else:
915932
status = False
916933
result = error_msg
@@ -947,6 +964,7 @@ def start_query_tool(trans_id):
947964
)
948965

949966
connect = 'connect' in request.args and request.args['connect'] == '1'
967+
950968
is_error, errmsg = check_and_upgrade_to_qt(trans_id, connect)
951969
if is_error:
952970
return make_json_response(success=0, errormsg=errmsg,
@@ -1209,6 +1227,7 @@ def poll(trans_id):
12091227
'transaction_status': transaction_status,
12101228
'data_obj': data_obj,
12111229
'pagination': pagination,
1230+
'server_cursor': trans_obj.server_cursor,
12121231
}
12131232
)
12141233

@@ -1466,6 +1485,9 @@ def save(trans_id):
14661485
session_obj['columns_info'],
14671486
session_obj['client_primary_key'],
14681487
conn)
1488+
# trans_obj.set_thread_native_id(None)
1489+
# session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
1490+
# update_session_grid_transaction(trans_id, session_obj)
14691491
else:
14701492
status = False
14711493
res = error_msg
@@ -1825,6 +1847,11 @@ def check_and_upgrade_to_qt(trans_id, connect):
18251847

18261848
if 'gridData' in session and str(trans_id) in session['gridData']:
18271849
data = pickle.loads(session['gridData'][str(trans_id)]['command_obj'])
1850+
session['gridData'][str(trans_id)] = {
1851+
# -1 specify the highest protocol version available
1852+
'command_obj': pickle.dumps(data, -1)
1853+
}
1854+
18281855
if data.object_type in ['table', 'foreign_table', 'view', 'mview']:
18291856
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
18301857
data.sid)
@@ -1837,27 +1864,17 @@ def check_and_upgrade_to_qt(trans_id, connect):
18371864
'conn_id': data.conn_id
18381865
}
18391866
is_error, errmsg, _, _ = _init_sqleditor(
1840-
trans_id, connect, data.sgid, data.sid, data.did, **kwargs)
1867+
trans_id, connect, data.sgid, data.sid, data.did,
1868+
**kwargs)
18411869

18421870
return is_error, errmsg
18431871

18441872

1845-
@blueprint.route(
1846-
'/auto_commit/<int:trans_id>',
1847-
methods=["PUT", "POST"], endpoint='auto_commit'
1848-
)
1849-
@pga_login_required
1850-
def set_auto_commit(trans_id):
1851-
"""
1852-
This method is used to set the value for auto commit .
1853-
1854-
Args:
1855-
trans_id: unique transaction id
1856-
"""
1873+
def set_pref_options(trans_id, operation):
18571874
if request.data:
1858-
auto_commit = json.loads(request.data)
1875+
_data = json.loads(request.data)
18591876
else:
1860-
auto_commit = request.args or request.form
1877+
_data = request.args or request.form
18611878

18621879
connect = 'connect' in request.args and request.args['connect'] == '1'
18631880

@@ -1877,12 +1894,17 @@ def set_auto_commit(trans_id):
18771894
status=404)
18781895

18791896
if status and conn is not None and \
1880-
trans_obj is not None and session_obj is not None:
1897+
trans_obj is not None and session_obj is not None:
18811898

18821899
res = None
18831900

1884-
# Call the set_auto_commit method of transaction object
1885-
trans_obj.set_auto_commit(auto_commit)
1901+
if operation == 'auto_commit':
1902+
# Call the set_auto_commit method of transaction object
1903+
trans_obj.set_auto_commit(_data)
1904+
elif operation == 'auto_rollback':
1905+
trans_obj.set_auto_rollback(_data)
1906+
elif operation == 'server_cursor':
1907+
trans_obj.set_server_cursor(_data)
18861908

18871909
# As we changed the transaction object we need to
18881910
# restore it and update the session variable.
@@ -1896,56 +1918,48 @@ def set_auto_commit(trans_id):
18961918

18971919

18981920
@blueprint.route(
1899-
'/auto_rollback/<int:trans_id>',
1900-
methods=["PUT", "POST"], endpoint='auto_rollback'
1921+
'/auto_commit/<int:trans_id>',
1922+
methods=["PUT", "POST"], endpoint='auto_commit'
19011923
)
19021924
@pga_login_required
1903-
def set_auto_rollback(trans_id):
1925+
def set_auto_commit(trans_id):
19041926
"""
19051927
This method is used to set the value for auto commit .
19061928
19071929
Args:
19081930
trans_id: unique transaction id
19091931
"""
1910-
if request.data:
1911-
auto_rollback = json.loads(request.data)
1912-
else:
1913-
auto_rollback = request.args or request.form
1914-
1915-
connect = 'connect' in request.args and request.args['connect'] == '1'
1932+
return set_pref_options(trans_id, 'auto_commit')
19161933

1917-
is_error, errmsg = check_and_upgrade_to_qt(trans_id, connect)
1918-
if is_error:
1919-
return make_json_response(success=0, errormsg=errmsg,
1920-
info=ERROR_MSG_FAIL_TO_PROMOTE_QT,
1921-
status=404)
19221934

1923-
# Check the transaction and connection status
1924-
status, error_msg, conn, trans_obj, session_obj = \
1925-
check_transaction_status(trans_id)
1926-
1927-
if error_msg == ERROR_MSG_TRANS_ID_NOT_FOUND:
1928-
return make_json_response(success=0, errormsg=error_msg,
1929-
info='DATAGRID_TRANSACTION_REQUIRED',
1930-
status=404)
1931-
1932-
if status and conn is not None and \
1933-
trans_obj is not None and session_obj is not None:
1935+
@blueprint.route(
1936+
'/auto_rollback/<int:trans_id>',
1937+
methods=["PUT", "POST"], endpoint='auto_rollback'
1938+
)
1939+
@pga_login_required
1940+
def set_auto_rollback(trans_id):
1941+
"""
1942+
This method is used to set the value for auto commit .
19341943
1935-
res = None
1944+
Args:
1945+
trans_id: unique transaction id
1946+
"""
1947+
return set_pref_options(trans_id, 'auto_rollback')
19361948

1937-
# Call the set_auto_rollback method of transaction object
1938-
trans_obj.set_auto_rollback(auto_rollback)
19391949

1940-
# As we changed the transaction object we need to
1941-
# restore it and update the session variable.
1942-
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
1943-
update_session_grid_transaction(trans_id, session_obj)
1944-
else:
1945-
status = False
1946-
res = error_msg
1950+
@blueprint.route(
1951+
'/server_cursor/<int:trans_id>',
1952+
methods=["PUT", "POST"], endpoint='server_cursor'
1953+
)
1954+
@pga_login_required
1955+
def set_server_cursor(trans_id):
1956+
"""
1957+
This method is used to set the value for server_cursor.
19471958
1948-
return make_json_response(data={'status': status, 'result': res})
1959+
Args:
1960+
trans_id: unique transaction id
1961+
"""
1962+
return set_pref_options(trans_id, 'server_cursor')
19491963

19501964

19511965
@blueprint.route(
@@ -2181,12 +2195,18 @@ def start_query_download_tool(trans_id):
21812195
if not sql:
21822196
sql = trans_obj.get_sql(sync_conn)
21832197
if sql and query_commited:
2198+
if trans_obj.server_cursor:
2199+
sync_conn.release_async_cursor()
2200+
sync_conn.execute_void("BEGIN;")
21842201
# Re-execute the query to ensure the latest data is included
2185-
sync_conn.execute_async(sql)
2202+
sync_conn.execute_async(sql, server_cursor=trans_obj.server_cursor)
21862203
# This returns generator of records.
21872204
status, gen, conn_obj = \
21882205
sync_conn.execute_on_server_as_csv(records=10)
21892206

2207+
if trans_obj.server_cursor and query_commited:
2208+
sync_conn.execute_void("COMMIT;")
2209+
21902210
if not status:
21912211
return make_json_response(
21922212
data={

web/pgadmin/tools/sqleditor/command.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ def __init__(self, **kwargs):
365365
self.limit = 100
366366

367367
self.thread_native_id = None
368+
self.server_cursor = kwargs['server_cursor'] if 'server_cursor' in kwargs else None
368369

369370
def get_primary_keys(self, *args, **kwargs):
370371
return None, None
@@ -425,6 +426,8 @@ def get_thread_native_id(self):
425426
def set_thread_native_id(self, thread_native_id):
426427
self.thread_native_id = thread_native_id
427428

429+
def set_server_cursor(self, server_cursor):
430+
self.server_cursor = server_cursor
428431

429432
class TableCommand(GridCommand):
430433
"""
@@ -816,6 +819,7 @@ def __init__(self, **kwargs):
816819
self.table_has_oids = False
817820
self.columns_types = None
818821
self.thread_native_id = None
822+
self.server_cursor = False
819823

820824
def get_sql(self, default_conn=None):
821825
return None
@@ -917,6 +921,9 @@ def set_auto_rollback(self, auto_rollback):
917921
def set_auto_commit(self, auto_commit):
918922
self.auto_commit = auto_commit
919923

924+
def set_server_cursor(self, server_cursor):
925+
self.server_cursor = server_cursor
926+
920927
def __set_updatable_results_attrs(self, sql_path,
921928
table_oid, conn):
922929
# Set template path for sql scripts and the table object id

web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export default class SQLEditor {
125125
priority: 101,
126126
label: gettext('All Rows'),
127127
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
128-
}, {
128+
},{
129129
name: 'view_first_100_rows_context_' + supportedNode,
130130
node: supportedNode,
131131
module: this,

0 commit comments

Comments
 (0)