Skip to content

Commit 26f7b6b

Browse files
committed
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. Optimizer is disabled via config: `/etc/icingaweb2/modules/icingadb/config.ini`: ``` [icingadb] ... disable_optimizer_for_history_queries=1 ```
1 parent 2aaeac8 commit 26f7b6b

File tree

5 files changed

+110
-4
lines changed

5 files changed

+110
-4
lines changed

application/controllers/HistoryController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
use GuzzleHttp\Psr7\ServerRequest;
88
use Icinga\Module\Icingadb\Model\History;
9+
use Icinga\Module\Icingadb\Util\OptimizerHints;
910
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
10-
use Icinga\Module\Icingadb\Web\Controller;
1111
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
12+
use Icinga\Module\Icingadb\Web\Controller;
1213
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
1314
use ipl\Stdlib\Filter;
1415
use ipl\Web\Control\LimitControl;
@@ -87,6 +88,10 @@ public function indexAction()
8788
Filter::like('service.id', '*')
8889
));
8990

91+
if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
92+
OptimizerHints::disableOptimizer($history);
93+
}
94+
9095
yield $this->export($history);
9196

9297
$this->addControl($sortControl);

application/controllers/HostController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Icinga\Module\Icingadb\Controllers;
66

77
use ArrayIterator;
8+
use Generator;
89
use Icinga\Exception\NotFoundError;
910
use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
1011
use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
@@ -20,6 +21,7 @@
2021
use Icinga\Module\Icingadb\Model\Service;
2122
use Icinga\Module\Icingadb\Model\ServicestateSummary;
2223
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
24+
use Icinga\Module\Icingadb\Util\OptimizerHints;
2325
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
2426
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
2527
use Icinga\Module\Icingadb\Web\Controller;
@@ -38,7 +40,6 @@
3840
use ipl\Web\Control\SortControl;
3941
use ipl\Web\Url;
4042
use ipl\Web\Widget\Tabs;
41-
use Generator;
4243

4344
class HostController extends Controller
4445
{
@@ -163,6 +164,10 @@ public function historyAction(): Generator
163164

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

167+
if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
168+
OptimizerHints::disableOptimizer($history);
169+
}
170+
166171
yield $this->export($history);
167172

168173
$this->addControl($sortControl);

application/controllers/NotificationsController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66

77
use GuzzleHttp\Psr7\ServerRequest;
88
use Icinga\Module\Icingadb\Model\NotificationHistory;
9+
use Icinga\Module\Icingadb\Util\OptimizerHints;
910
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
11+
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
1012
use Icinga\Module\Icingadb\Web\Controller;
1113
use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList;
12-
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
1314
use ipl\Stdlib\Filter;
1415
use ipl\Web\Control\LimitControl;
1516
use ipl\Web\Control\SortControl;
@@ -81,6 +82,10 @@ public function indexAction()
8182
Filter::like('history.service.id', '*')
8283
));
8384

85+
if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
86+
OptimizerHints::disableOptimizer($notifications);
87+
}
88+
8489
yield $this->export($notifications);
8590

8691
$this->addControl($sortControl);

application/controllers/ServiceController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Icinga\Module\Icingadb\Controllers;
66

77
use ArrayIterator;
8+
use Generator;
89
use Icinga\Exception\NotFoundError;
910
use Icinga\Module\Icingadb\Command\Object\GetObjectCommand;
1011
use Icinga\Module\Icingadb\Command\Transport\CommandTransport;
@@ -17,6 +18,7 @@
1718
use Icinga\Module\Icingadb\Model\History;
1819
use Icinga\Module\Icingadb\Model\Service;
1920
use Icinga\Module\Icingadb\Redis\VolatileStateResults;
21+
use Icinga\Module\Icingadb\Util\OptimizerHints;
2022
use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
2123
use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher;
2224
use Icinga\Module\Icingadb\Web\Controller;
@@ -34,7 +36,6 @@
3436
use ipl\Web\Control\SortControl;
3537
use ipl\Web\Url;
3638
use ipl\Web\Widget\Tabs;
37-
use Generator;
3839

3940
class ServiceController extends Controller
4041
{
@@ -311,6 +312,10 @@ public function historyAction(): Generator
311312

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

315+
if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
316+
OptimizerHints::disableOptimizer($history);
317+
}
318+
314319
yield $this->export($history);
315320

316321
$this->addControl($sortControl);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Icinga\Module\Icingadb\Util;
4+
5+
use Icinga\Application\Config;
6+
use ipl\Orm\Query;
7+
use ipl\Sql\Adapter\Mysql;
8+
use ipl\Sql\Select;
9+
use ipl\Stdlib\Str;
10+
11+
/**
12+
* Helper class to allow disabling the MySQL/MariaDB query optimizer for history queries.
13+
* Some versions of these RDBMS perform poorly with history queries,
14+
* particularly when the optimizer changes join order or uses block nested loop joins.
15+
* Ideally, testing across all RDBMS versions to identify when the optimizer fails and adjusting queries or
16+
* using optimizer switches would be preferable, but this level of effort is not justified at the moment.
17+
*/
18+
readonly class OptimizerHints
19+
{
20+
public const DISABLE_OPTIMIZER_HINT = '/*+ NO_BNL() */ STRAIGHT_JOIN';
21+
22+
/**
23+
* Determines whether to disable the query optimizer for history queries.
24+
*
25+
* Call this method before using {@see disableOptimizer()} to check if optimizer disabling is necessary:
26+
*
27+
* ```
28+
* if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
29+
* OptimizerHints::disableOptimizer($query);
30+
* }
31+
* ```
32+
*
33+
* @return bool
34+
*/
35+
public static function shouldDisableOptimizerForHistoryQueries(): bool
36+
{
37+
return Config::module('icingadb')->get('icingadb', 'disable_optimizer_for_history_queries', false);
38+
}
39+
40+
/**
41+
* Injects an optimizer hint into SELECT queries for a MySQL/MariaDB Query object,
42+
* forcing the RDBMS to disable the optimizer.
43+
*
44+
* Call {@see shouldDisableOptimizerForHistoryQueries()} first to check if this is necessary:
45+
*
46+
* ```
47+
* if (OptimizerHints::shouldDisableOptimizerForHistoryQueries()) {
48+
* OptimizerHints::disableOptimizer($query);
49+
* }
50+
* ```
51+
*
52+
* @param Query $q
53+
*
54+
* @return void
55+
*/
56+
public static function disableOptimizer(Query $q): void
57+
{
58+
if ($q->getDb()->getAdapter() instanceof Mysql) {
59+
// Locates the first string column, prepends the optimizer hint,
60+
// and resets columns to ensure it appears first in the SELECT statement:
61+
$q->on(Query::ON_SELECT_ASSEMBLED, static function (Select $select) {
62+
$columns = $select->getColumns();
63+
foreach ($columns as $alias => $column) {
64+
if (is_string($column)) {
65+
if (Str::startsWith($column, static::DISABLE_OPTIMIZER_HINT)) {
66+
return;
67+
}
68+
69+
unset($columns[$alias]);
70+
$select->resetColumns();
71+
72+
if (is_int($alias)) {
73+
array_unshift($columns, static::DISABLE_OPTIMIZER_HINT . " $column");
74+
} else {
75+
$columns = [$alias => static::DISABLE_OPTIMIZER_HINT . " $column"] + $columns;
76+
}
77+
78+
$select->resetColumns()->columns($columns);
79+
80+
return;
81+
}
82+
}
83+
});
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)