Skip to content

ENH Optimise SQL queries to improve performance#11805

Merged
emteknetnz merged 1 commit intosilverstripe:6from
creative-commoners:pulls/6/improve-queries
Aug 6, 2025
Merged

ENH Optimise SQL queries to improve performance#11805
emteknetnz merged 1 commit intosilverstripe:6from
creative-commoners:pulls/6/improve-queries

Conversation

@GuySartorelli
Copy link
Copy Markdown
Member

Comment thread src/Security/Group.php
'GroupID' => $this->Parent()->collateAncestorIDs(),
'Code' => $privilegedCodes,
]);
if ($inheritedCodes->exists()) {
Copy link
Copy Markdown
Member Author

@GuySartorelli GuySartorelli Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two optimisations in this query:

  1. filtering by the privileged permissions means if there's no privileged codes for that group, the database can just say "nope nothing there" instead of fetching and returning data that we then have to filter through ourselves
  2. using exists() is more efficient than fetching columns

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality is covered by GroupTest::testValidatesPrivilegeLevelOfParent()

),
'required'
);
$valid = false;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug fix. We were setting a validation error, but then returning true so the member would get updated anyway!
There was no test coverage for this scenario, so I've added some.

Comment on lines -150 to +151
$adminGroups = array_intersect(
$data['DirectGroups'] ?? [],
Permission::get_groups_by_permission('ADMIN')->column()
);

if (count($adminGroups ?? []) === 0) {
$adminGroups = Permission::get_groups_by_permission('ADMIN')->filter(['ID' => $data['DirectGroups']]);
if (!$adminGroups->exists()) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimisations here are:

  1. Filter the query directly by $data['DirectGroups'] - if there's no matches, the database doesn't have to grab and return results that we're just gonna filter out anyway.
  2. Using exists() is more efficient than selecting, returning, and array_intersecting some data.

Copy link
Copy Markdown
Member Author

@GuySartorelli GuySartorelli Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is covered by the new MemberTest::testMemberValidatorRemoveAdmin().
I've also updated the existing MemberTest::testMemberValidator() to use data providers and added some more scenarios that weren't included in the test before.

@GuySartorelli GuySartorelli force-pushed the pulls/6/improve-queries branch from 7e2a025 to ebbbf22 Compare July 29, 2025 01:12
Comment on lines -598 to +599
$otherPerms = DB::withPrimary(
fn() => DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column()
);

$otherPerms = Permission::get()->exclude(['Code' => $flatCodeArray])->column('Code');
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of fetching all of the permission codes and then excluding the codes from $flatCodesArray in PHP, it's more efficient to just tell the database to never return those in the results in the first place.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is covered by PermissionTest::testGetCodesGrouped() and PermissionTest::testGetCodesUngrouped(). Not super rigorous tests but they serve their purpose for this change at least.

@GuySartorelli GuySartorelli force-pushed the pulls/6/improve-queries branch from ebbbf22 to f4930dc Compare July 29, 2025 04:52
if ($isNewRecord) {
return $controller->redirect($this->Link());
} elseif ($this->gridField->getList()->byID($this->record->ID)) {
} elseif ($this->gridField->getList()->filter('ID', $this->record->ID)->exists()) {
Copy link
Copy Markdown
Member Author

@GuySartorelli GuySartorelli Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While not strictly reducing the number of queries, I've updated a bunch of calls to find() and byID() (which were only being used to check if a record exists) to instead use filter()->exists() which is more efficient.

@GuySartorelli GuySartorelli force-pushed the pulls/6/improve-queries branch from f4930dc to 65e4a89 Compare July 30, 2025 23:44
@GuySartorelli GuySartorelli marked this pull request as ready for review July 31, 2025 00:42
Comment on lines -36 to +38
$parentGroup = Group::get()->setUseCache(true)->find('Code', $record['ParentCode']);
if ($parentGroup) {
$group->ParentID = $parentGroup->ID;
$parentGroupIDs = Group::get()->setUseCache(true)->filter('Code', $record['ParentCode'])->column();
if (!empty($parentGroupIDs)) {
$group->ParentID = $parentGroupIDs[0];
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of getting all the data about the group just to get its ID, we just get the ID directly.

@emteknetnz emteknetnz merged commit 78df925 into silverstripe:6 Aug 6, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants