Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion application/controllers/HistoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Util\OptimizerHints;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
Expand Down Expand Up @@ -87,6 +88,8 @@ public function indexAction()
Filter::like('service.id', '*')
));

OptimizerHints::disableOptimizerForHistoryQueries($history);

yield $this->export($history);

$this->addControl($sortControl);
Expand Down
5 changes: 4 additions & 1 deletion application/controllers/HostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Icinga\Module\Icingadb\Controllers;

use ArrayIterator;
use Generator;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
Expand All @@ -20,6 +21,7 @@
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Model\ServicestateSummary;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Util\OptimizerHints;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
Expand All @@ -38,7 +40,6 @@
use ipl\Web\Control\SortControl;
use ipl\Web\Url;
use ipl\Web\Widget\Tabs;
use Generator;

class HostController extends Controller
{
Expand Down Expand Up @@ -163,6 +164,8 @@ public function historyAction(): Generator

$history->filter(Filter::lessThanOrEqual('event_time', $before));

OptimizerHints::disableOptimizerForHistoryQueries($history);

yield $this->export($history);

$this->addControl($sortControl);
Expand Down
5 changes: 4 additions & 1 deletion application/controllers/NotificationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Module\Icingadb\Model\NotificationHistory;
use Icinga\Module\Icingadb\Util\OptimizerHints;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use ipl\Stdlib\Filter;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;
Expand Down Expand Up @@ -81,6 +82,8 @@ public function indexAction()
Filter::like('history.service.id', '*')
));

OptimizerHints::disableOptimizerForHistoryQueries($notifications);

yield $this->export($notifications);

$this->addControl($sortControl);
Expand Down
5 changes: 4 additions & 1 deletion application/controllers/ServiceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Icinga\Module\Icingadb\Controllers;

use ArrayIterator;
use Generator;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
Expand All @@ -17,6 +18,7 @@
use Icinga\Module\Icingadb\Model\History;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
use Icinga\Module\Icingadb\Util\OptimizerHints;
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
use Icinga\Module\Icingadb\Web\Controller;
Expand All @@ -34,7 +36,6 @@
use ipl\Web\Control\SortControl;
use ipl\Web\Url;
use ipl\Web\Widget\Tabs;
use Generator;

class ServiceController extends Controller
{
Expand Down Expand Up @@ -311,6 +312,8 @@ public function historyAction(): Generator

$history->filter(Filter::lessThanOrEqual('event_time', $before));

OptimizerHints::disableOptimizerForHistoryQueries($history);

yield $this->export($history);

$this->addControl($sortControl);
Expand Down
73 changes: 73 additions & 0 deletions library/Icingadb/Util/OptimizerHints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/* Icinga DB Web | (c) 2025 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Icingadb\Util;

use Icinga\Application\Config;
use ipl\Orm\Query;
use ipl\Sql\Adapter\Mysql;
use ipl\Sql\Select;
use ipl\Stdlib\Str;

/**
* Helper class to allow disabling the MySQL/MariaDB query optimizer for history queries.
* Some versions of these RDBMS perform poorly with history queries,
* particularly when the optimizer changes join order or uses block nested loop joins.
* Ideally, testing across all RDBMS versions to identify when the optimizer fails and adjusting queries or
* using optimizer switches would be preferable, but this level of effort is not justified at the moment.
*/
readonly class OptimizerHints
{
public const DISABLE_OPTIMIZER_HINT = '/*+ NO_BNL() */ STRAIGHT_JOIN';

/**
* If optimizer disabling is enabled for history queries,
* injects an optimizer hint into SELECT queries for a MySQL/MariaDB Query object,
* forcing the RDBMS to disable the optimizer
*
* @param Query $q
*
* @return void
*/
public static function disableOptimizerForHistoryQueries(Query $q): void
{
if (static::shouldDisableOptimizerForHistoryQueries() && $q->getDb()->getAdapter() instanceof Mysql) {
// Locates the first string column, prepends the optimizer hint,
// and resets columns to ensure it appears first in the SELECT statement:
Copy link
Member

Choose a reason for hiding this comment

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

why the first string column? Does the query hint not work if placed before an (aliased) expression?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure it works, but it will also increase complexity. My thought was that the hint should work (or rather not add crap) for every query scenario, even if it can't be added at all as with SELECT 1 (unaliased expression). Considering that all other queries will select real columns, I decided to use this implementation.

Copy link
Member

Choose a reason for hiding this comment

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

Aye. But shouldn't it throw an error just in case the first column is not a string? I didn't try, but doubt that this works:

SELECT 1 + 1 as two, /*+ NO_BNL() */ STRAIGHT_JOIN test.name as test_name from test

$q->on(Query::ON_SELECT_ASSEMBLED, static function (Select $select) {
$columns = $select->getColumns();
foreach ($columns as $alias => $column) {
if (is_string($column)) {
if (Str::startsWith($column, static::DISABLE_OPTIMIZER_HINT)) {
return;
}

unset($columns[$alias]);
$select->resetColumns();

if (is_int($alias)) {
array_unshift($columns, static::DISABLE_OPTIMIZER_HINT . " $column");
} else {
$columns = [$alias => static::DISABLE_OPTIMIZER_HINT . " $column"] + $columns;
}

$select->resetColumns()->columns($columns);

return;
}
}
});
}
}

/**
* Determines whether to disable the query optimizer for history queries.
*
* @return bool
*/
protected static function shouldDisableOptimizerForHistoryQueries(): bool
{
return Config::module('icingadb')->get('icingadb', 'disable_optimizer_for_history_queries', false);
Copy link
Member

Choose a reason for hiding this comment

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

I think the settings section fits better. There's a doc area for it as well already.

}
}
Loading