feat: support countig till a limit

In InnoDB counting is essentially O(n) operation, it can be pretty fast
on indexes but when filters don't use any index it usually means doing a
full table scan.

Adding a limit will stop the scan as soon as that many records are
matched.
This commit is contained in:
Ankush Menat 2024-03-12 09:25:21 +05:30
parent ae649aadf0
commit a49fafbf8e
4 changed files with 35 additions and 7 deletions

View file

@ -52,14 +52,24 @@ def get_count() -> int:
if is_virtual_doctype(args.doctype):
controller = get_controller(args.doctype)
data = frappe.call(controller.get_count, args=args, **args)
count = frappe.call(controller.get_count, args=args, **args)
else:
args.distinct = sbool(args.distinct)
distinct = "distinct " if args.distinct else ""
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
data = execute(**args)[0].get("total_count")
args.limit = cint(args.limit)
if args.limit:
# Only "count until this limit"
args.fields = ["*"]
partial_query = execute(**args, run=0)
count = frappe.db.sql(
f"""with records as ( {partial_query} )
select count(*) from records""",
)[0][0]
else:
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
count = execute(**args)[0].get("total_count")
return data
return count
def execute(doctype, *args, **kwargs):

View file

@ -96,6 +96,7 @@ frappe.db = {
},
count: function (doctype, args = {}) {
let filters = args.filters || {};
let limit = args.limit;
// has a filter with childtable?
const distinct =
@ -111,6 +112,7 @@ frappe.db = {
filters,
fields,
distinct,
limit,
});
},
get_link_options(doctype, txt = "", filters = {}) {

View file

@ -946,6 +946,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return frappe.db
.count(this.doctype, {
filters: this.get_filters_for_args(),
limit: 1001,
})
.then((total_count) => {
this.total_count = total_count || current_count;

View file

@ -1187,7 +1187,7 @@ class TestReportView(FrappeTestCase):
"distinct": "false",
}
)
list_filter_response = execute_cmd("frappe.desk.reportview.get_count")
count = execute_cmd("frappe.desk.reportview.get_count")
frappe.local.form_dict = frappe._dict(
{
"doctype": "DocType",
@ -1196,8 +1196,8 @@ class TestReportView(FrappeTestCase):
}
)
dict_filter_response = execute_cmd("frappe.desk.reportview.get_count")
self.assertIsInstance(list_filter_response, int)
self.assertEqual(list_filter_response, dict_filter_response)
self.assertIsInstance(count, int)
self.assertEqual(count, dict_filter_response)
# test with child table filter
frappe.local.form_dict = frappe._dict(
@ -1218,6 +1218,21 @@ class TestReportView(FrappeTestCase):
)[0][0]
self.assertEqual(child_filter_response, current_value)
# test with limit
limit = 2
frappe.local.form_dict = frappe._dict(
{
"doctype": "DocType",
"filters": [["DocType", "is_virtual", "=", 1]],
"fields": [],
"distinct": "false",
"limit": limit,
}
)
count = execute_cmd("frappe.desk.reportview.get_count")
self.assertIsInstance(count, int)
self.assertLessEqual(count, limit)
def test_reportview_get(self):
user = frappe.get_doc("User", "test@example.com")
add_child_table_to_blog_post()