mirror of
https://github.com/nextcloud/server.git
synced 2026-06-15 19:49:38 -04:00
Implements RFC #59422. Adds four read-only diagnostic commands to the occ CLI for administrators to inspect database health without needing external tools: - db:info: shows engine version and key config variables with health check against recommended values - db:size: lists all tables ordered by total disk usage - db:index-usage: reports unused indexes via performance_schema (MySQL) or pg_stat_user_indexes (PostgreSQL) - db:locks: detects active blocking transactions and deadlocks All commands support MySQL/MariaDB and PostgreSQL. A --json flag is available for automated parsing. Includes 31 unit tests. Closes #59422 Signed-off-by: Rodrigo Correia <rodrigo.mendes.correia@tecnico.ulisboa.pt> Signed-off-by: Carolina Quinteiro <carolinafquinteiro@tecnico.ulisboa.pt> Co-authored-by: Carolina Quinteiro <carolinafquinteiro@tecnico.ulisboa.pt>
115 lines
3.9 KiB
PHP
115 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
namespace OC\Core\Command\Db;
|
|
|
|
use OC\DB\Connection;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Helper\Table;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
|
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
|
|
|
class DbIndexUsage extends Command {
|
|
|
|
public function __construct(
|
|
private readonly Connection $connection,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void {
|
|
$this
|
|
->setName('db:index-usage')
|
|
->setDescription('Report unused database indexes (indexes that slow writes but are never read)')
|
|
->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format')
|
|
->addOption('all', null, InputOption::VALUE_NONE, 'Show all indexes, not just unused ones');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int {
|
|
$platform = $this->connection->getDatabasePlatform();
|
|
$asJson = $input->getOption('json');
|
|
$showAll = $input->getOption('all');
|
|
|
|
if ($platform instanceof MySQLPlatform) {
|
|
// Requires performance_schema to be enabled (default in MySQL 5.6+/MariaDB 10.0+)
|
|
$unused_filter = $showAll ? '' : "WHERE s.count_read = 0 AND s.index_name IS NOT NULL AND s.index_name != 'PRIMARY'";
|
|
$sql = "
|
|
SELECT s.object_name AS `table`,
|
|
s.index_name AS `index`,
|
|
s.count_read AS reads,
|
|
s.count_write AS writes
|
|
FROM performance_schema.table_io_waits_summary_by_index_usage s
|
|
{$unused_filter}
|
|
ORDER BY s.object_name, s.index_name
|
|
";
|
|
} elseif ($platform instanceof PostgreSQLPlatform) {
|
|
$unused_filter = $showAll ? '' : 'AND idx_scan = 0';
|
|
$sql = "
|
|
SELECT relname AS table,
|
|
indexrelname AS index,
|
|
idx_scan AS reads,
|
|
idx_tup_read AS tuples_read,
|
|
idx_tup_fetch AS tuples_fetched
|
|
FROM pg_stat_user_indexes
|
|
JOIN pg_index USING (indexrelid)
|
|
WHERE indisunique IS FALSE
|
|
{$unused_filter}
|
|
ORDER BY relname, indexrelname
|
|
";
|
|
} else {
|
|
$output->writeln('<comment>db:index-usage is not supported for SQLite.</comment>');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
try {
|
|
$rows = $this->connection->executeQuery($sql)->fetchAllAssociative();
|
|
} catch (\Doctrine\DBAL\Exception $e) {
|
|
$output->writeln('<error>Failed to query index usage statistics. The required performance tables may not be available on this database version.</error>');
|
|
$output->writeln('<comment>Details: ' . $e->getMessage() . '</comment>');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if (empty($rows)) {
|
|
$output->writeln('<info>No unused indexes found. Great!</info>');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
if ($asJson) {
|
|
$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
$table = new Table($output);
|
|
|
|
if ($platform instanceof MySQLPlatform) {
|
|
$table->setHeaders(['Table', 'Index', 'Reads', 'Writes']);
|
|
foreach ($rows as $row) {
|
|
$table->addRow([$row['table'], $row['index'], $row['reads'], $row['writes']]);
|
|
}
|
|
} else {
|
|
$table->setHeaders(['Table', 'Index', 'Scans', 'Tuples Read', 'Tuples Fetched']);
|
|
foreach ($rows as $row) {
|
|
$table->addRow([$row['table'], $row['index'], $row['reads'], $row['tuples_read'], $row['tuples_fetched']]);
|
|
}
|
|
}
|
|
|
|
$table->render();
|
|
|
|
if (!$showAll) {
|
|
$output->writeln(sprintf(
|
|
'<comment>Found %d unused index(es). Consider removing them to improve write performance.</comment>',
|
|
count($rows)
|
|
));
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|