Скрыть объявление
ВАШИ ПРАВА ОГРАНИЧЕНЫ!

Зарегистрируйтесь на форуме, чтобы стать полноценным участником сообщества!

Вопрос? Оптимизация dle

Тема в разделе "Вопросы- ответы", создана пользователем art994, 19 дек 2025.

19.12.25 в 07:15
26.12.25 в 09:22
26
2.121
0
  1. TopicStarter Overlay
    art994

    art994 Посетитель

    Регистрация:
    7 июн 2015
    Сообщения:
    78
    Лучших ответов:
    1
    Рейтинги:
    +7 / 8 / -0
    Всем привет. Как можно оптимизировать движок DLE если на сайте 40 000 новостей с постерами?
     
  2. ok_daa

    ok_daa Бывалый

    Регистрация:
    24 мар 2023
    Сообщения:
    329
    Лучших ответов:
    0
    Рейтинги:
    +56 / 14 / -0
    Оптимизировать надо не DLE, а VPS
     
  3. Kandi

    Kandi Бывалый

    Регистрация:
    19 апр 2019
    Сообщения:
    590
    Лучших ответов:
    1
    Рейтинги:
    +121 / 43 / -0
    art994 непонятно что вы хотите оптимизировать, чтобы что-то оптимизировать нужно начать с того в чем у вас проблема? Купите помощнее сервер, если что-то не тянет, а лучше даже возьмите выделенный сервер, у которого будет достаточно ресурсов, чтобы тянуть такие объёмы данных и толпу ваших посетителей. Сервера под кино, смотрите у меня в подписи
     
  4. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    1) Redis Cache or Memcache + период кэширования ставь больше (Смотри по расходу RAM на сервере, если забьется сервер ляжет если нет Swap - поставь Swap 1-2gb хотя бы)
    2) Кеш статики на стороне браузера пользователей настраивается в Nginx заголовками (это уже если прям много пользователей, а так не особо поможет)
    3) В базе сделай внешние ключи между таблицами связанными + uniq ключи поставь по уникальным полям это ускоряет запросы у DLE стокового треш там
    4) Если Nginx то выстави php-fpm оптимальные настройки
    5) Настрой буфера в mysql сервере и конфиг под ресурсы сервера поковыряй, выдели больше озу под данные чтобы через диск не гоняло
    6) Отключи ненужные модули в DLE там потыкать в настройках можно, они запросы делают дополнительные

    PS: Все это не поможет если сайт работает на калькуляторе-сервере, все равно сервер нужен адекватной производительности под DLE. Те кто говорят бери выделенный, не слушай дорого и нет ризона если трафика на сайт идет мало. Для 40к постов при среднем трафике хватит core4 ram8 сервера, но тут еще от процессора и типа озу зависит, не бери старье на DDR3 памяти. Желательно на Ryzen или топовом проце с хорошей частотой, Mysql любит такие процессоры и шустрее Xeon. Можно и Xeon но там по частоте похуже, но и Xeon потянет. Есть еще узкие моменты на уровне кода php и настроек php, но тут долго расписывать. Итак написал достаточно...
     
    • Лучший Лучший x 1
    • Полезно Полезно x 1
  5. TopicStarter Overlay
    art994

    art994 Посетитель

    Регистрация:
    7 июн 2015
    Сообщения:
    78
    Лучших ответов:
    1
    Рейтинги:
    +7 / 8 / -0
    scr1pt0 Спасибо за очень подробный ответ. Сейчас стоит fastpanel php-fm. Вроде с кешем работает не совсем тормознуто, но форум например работает быстрее dle.
     
  6. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    art994 Ну Fastpanel тут обновили, чет с сохранением конфигов nginx беда приходится на самом сервере файлики править, а так панель норм переезды делать удобно между серверами например 1 командой все переносишь и остается только dns у доменов заменить на новые
     
  7. TopicStarter Overlay
    art994

    art994 Посетитель

    Регистрация:
    7 июн 2015
    Сообщения:
    78
    Лучших ответов:
    1
    Рейтинги:
    +7 / 8 / -0
    scr1pt0 а nginx вы где что правите? будет очень полезно. и еще бы команду волшебную для переноса. Спасибо
     
  8. sneiks

    Команда форума VIP Кинотрафик v2

    Регистрация:
    27 янв 2016
    Сообщения:
    344
    Лучших ответов:
    0
    Рейтинги:
    +58 / 2 / -0
    Скажите измеряли скорость до и после по 3 пункту ? И как проделать такие правки, может у вас есть готовый плагин для внесение правок?
     
  9. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    sneiks Ну на холостых не заметишь разницы, тут надо нагрузочные тесты делать, но ускорять будет в любом случае базу наличие грамотно расставленных ключей. Но там и сама база не нормализована у DLE структура базы на ужасном уровне
     
  10. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    art994 Я вообще рекомендую отказаться от nginx + apache2 связки (CGI or FastCGI) и перейти на чистый nginx, он гораздо шустрее работает. Apache2 очень медленный по сравнению с Nginx. Там просто надо .htaccess файл перевести в формат конфига nginx.
     
    • Полезно Полезно x 1
  11. sneiks

    Команда форума VIP Кинотрафик v2

    Регистрация:
    27 янв 2016
    Сообщения:
    344
    Лучших ответов:
    0
    Рейтинги:
    +58 / 2 / -0
    Так есть универсальное решение правки базы? Думаю многим было бы полезно. Готов поддержать на чай с печенками
     
  12. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    sneiks Можно руками проставить ключи есть клиент на винду для работы с базой HeidiSQL называется, плагинов нет на такое насколько знаю но думаю кто нибудь напишет, но многие разрабы модулей и плагинов сами не проставляют некоторые ключи в базе замечал, как будто не знают что это такое)
     
  13. scr1pt0

    scr1pt0 Зелёный

    Регистрация:
    13 фев 2022
    Сообщения:
    24
    Лучших ответов:
    0
    Рейтинги:
    +14 / 0 / -0
    sneiks ну я как нибудь сделаю плагин оптимизатор DLE если руки дойдут, пока некогда
     
    • Нравится Нравится x 2
    • Лучший Лучший x 1
  14. ok_daa

    ok_daa Бывалый

    Регистрация:
    24 мар 2023
    Сообщения:
    329
    Лучших ответов:
    0
    Рейтинги:
    +56 / 14 / -0
    ждём
     
  15. level

    level Новичок

    Регистрация:
    26 ноя 2024
    Сообщения:
    43
    Лучших ответов:
    0
    Рейтинги:
    +2 / 3 / -0
    ok_daa потестите
    Код:
    <?xml version="1.0" encoding="utf-8"?>
    <dleplugin>
      <name>DBOptimizerPro</name>
      <description><![CDATA[
    Оптимизация БД DLE: индексы, UNIQUE, FOREIGN KEY, обслуживание таблиц (ANALYZE/CHECK/OPTIMIZE/REPAIR).
    
    Как открыть:
    - Админка → Сторонние модули → DBOptimizerPro
    - или /admin.php?mod=db_optimizer
    
    Важно:
    - ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
    - Перед применением сделайте бэкап.
    - Лог: /engine/data/dboptimizerpro.log
    ]]></description>
      <icon></icon>
      <version>1.1.0</version>
      <dleversion>18.0</dleversion>
      <versioncompare>greater</versioncompare>
      <upgradeurl></upgradeurl>
      <filedelete>0</filedelete>
      <needplugin></needplugin>
      <mnotice>0</mnotice>
    
      <mysqlinstall><![CDATA[
    INSERT IGNORE INTO `{prefix}_admin_sections` (`name`, `title`, `descr`, `icon`, `allow_groups`)
    VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1');
    ]]></mysqlinstall>
    
      <mysqlupgrade><![CDATA[]]></mysqlupgrade>
      <mysqlenable><![CDATA[]]></mysqlenable>
      <mysqldisable><![CDATA[]]></mysqldisable>
    
      <mysqldelete><![CDATA[
    DELETE FROM `{prefix}_admin_sections` WHERE `name`='db_optimizer';
    ]]></mysqldelete>
    
      <phpinstall><![CDATA[
    @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
    ]]></phpinstall>
    
      <phpupgrade><![CDATA[]]></phpupgrade>
      <phpenable><![CDATA[]]></phpenable>
      <phpdisable><![CDATA[]]></phpdisable>
    
      <phpdelete><![CDATA[
    @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
    ]]></phpdelete>
    
      <notice><![CDATA[]]></notice>
    
      <file name="engine/inc/db_optimizer.php">
        <operation action="create">
          <replacecode><![CDATA[
    <?php
    /**
     * DBOptimizerPro - модуль админки DLE
     * Версия: 1.1.0
     *
     * Реализовано:
     * - Обслуживание таблиц: ANALYZE / CHECK / OPTIMIZE / REPAIR (REPAIR только MyISAM)
     * - Индексы (обычные)
     * - UNIQUE (с проверкой дублей)
     * - FOREIGN KEY (с проверками: InnoDB, типы, индекс, сироты)
     *
     * Важно:
     * - FOREIGN KEY сам по себе не "ускоритель". Чаще ускоряют индексы.
     * - Любой ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
     * - Сделайте бэкап перед применением.
     */
    
    if (!defined('DATALIFEENGINE') || !defined('LOGGED_IN')) {
        die('Hacking attempt!');
    }
    
    if (!isset($member_id) || !is_array($member_id) || (int)$member_id['user_group'] !== 1) {
        if (function_exists('msg')) {
            msg('error', 'Доступ запрещён', 'Модуль доступен только администраторам (группа 1).');
        }
        die('Access denied');
    }
    
    define('DBOP_VERSION', '1.1.0');
    
    // ---------------- helpers ----------------
    
    function dbop_h($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }
    
    function dbop_bytes($bytes) {
        $bytes = (float)$bytes;
        if ($bytes < 1024) return sprintf('%.0f B', $bytes);
        $u = array('KB','MB','GB','TB','PB');
        $i = 0;
        $bytes /= 1024;
        while ($bytes >= 1024 && $i < count($u)-1) { $bytes /= 1024; $i++; }
        return sprintf('%.2f %s', $bytes, $u[$i]);
    }
    
    function dbop_escape_like($s) {
        return str_replace(array('\\', '%', '_'), array('\\\\', '\\%', '\\_'), (string)$s);
    }
    
    function dbop_require_hash() {
        global $dle_login_hash;
        if (!isset($_REQUEST['user_hash']) || $_REQUEST['user_hash'] !== $dle_login_hash) {
            msg('error', 'Ошибка безопасности', 'Неверный ключ безопасности (user_hash). Обновите страницу и повторите действие.');
        }
    }
    
    function dbop_log($line) {
        $file = ENGINE_DIR . '/data/dboptimizerpro.log';
        $ts = date('Y-m-d H:i:s');
        @file_put_contents($file, '['.$ts.'] '.$line."\n", FILE_APPEND);
    }
    
    function dbop_is_safe_identifier($s) {
        return (bool)preg_match('/^[A-Za-z0-9_]+$/', (string)$s);
    }
    
    // --------- DB introspection ---------
    
    function dbop_get_tables($prefix, $only_prefix=true) {
        global $db;
    
        $out = array();
    
        $where = "TABLE_SCHEMA = DATABASE()";
        if ($only_prefix && $prefix !== '') {
            // хотим только таблицы DLE: PREFIX_*
            $like = dbop_escape_like($prefix . '_') . '%';
            $where .= " AND TABLE_NAME LIKE '{$like}' ESCAPE '\\\\'";
        }
    
        $sql = "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, DATA_FREE
                FROM information_schema.TABLES
                WHERE {$where}
                ORDER BY TABLE_NAME";
        $db->query($sql);
        while ($r = $db->get_row()) {
            $out[$r['TABLE_NAME']] = array(
                'table'  => $r['TABLE_NAME'],
                'engine' => $r['ENGINE'],
                'rows'   => (int)$r['TABLE_ROWS'],
                'data'   => (int)$r['DATA_LENGTH'],
                'index'  => (int)$r['INDEX_LENGTH'],
                'free'   => (int)$r['DATA_FREE'],
            );
        }
        $db->free();
        return $out;
    }
    
    function dbop_get_columns($table) {
        global $db;
        $cols = array();
        $db->query("SHOW COLUMNS FROM `{$table}`");
        while ($r = $db->get_row()) $cols[] = $r['Field'];
        $db->free();
        return $cols;
    }
    
    function dbop_get_column_types($table) {
        global $db;
        $types = array();
        $db->query("SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
                    FROM information_schema.COLUMNS
                    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '".$db->safesql($table)."'");
        while ($r = $db->get_row()) {
            $types[$r['COLUMN_NAME']] = array(
                'type' => $r['COLUMN_TYPE'],
                'null' => $r['IS_NULLABLE'],
                'def'  => $r['COLUMN_DEFAULT'],
            );
        }
        $db->free();
        return $types;
    }
    
    function dbop_get_indexes($table) {
        global $db;
        $idx = array();
        $db->query("SHOW INDEX FROM `{$table}`");
        while ($r = $db->get_row()) {
            $name = $r['Key_name'];
            if (!isset($idx[$name])) {
                $idx[$name] = array(
                    'unique' => ((int)$r['Non_unique'] === 0),
                    'cols'   => array(),
                );
            }
            $seq = (int)$r['Seq_in_index'];
            $idx[$name]['cols'][$seq] = $r['Column_name'];
        }
        $db->free();
        foreach ($idx as $k => $v) {
            ksort($idx[$k]['cols']);
            $idx[$k]['cols'] = array_values($idx[$k]['cols']);
        }
        return $idx;
    }
    
    function dbop_has_index_prefix($indexes, $cols, $need_unique=false) {
        $n = count($cols);
        foreach ($indexes as $idx) {
            if ($need_unique && empty($idx['unique'])) continue;
            if (count($idx['cols']) < $n) continue;
            $ok = true;
            for ($i=0; $i<$n; $i++) {
                if ((string)$idx['cols'][$i] !== (string)$cols[$i]) { $ok = false; break; }
            }
            if ($ok) return true;
        }
        return false;
    }
    
    function dbop_fk_exists($table, $col, $ref_table, $ref_col) {
        global $db;
        $sql = "SELECT CONSTRAINT_NAME
                FROM information_schema.KEY_COLUMN_USAGE
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = '".$db->safesql($table)."'
                  AND COLUMN_NAME = '".$db->safesql($col)."'
                  AND REFERENCED_TABLE_NAME = '".$db->safesql($ref_table)."'
                  AND REFERENCED_COLUMN_NAME = '".$db->safesql($ref_col)."'
                LIMIT 1";
        $row = $db->super_query($sql);
        return !empty($row['CONSTRAINT_NAME']);
    }
    
    function dbop_get_table_engine($tables, $table) {
        return isset($tables[$table]['engine']) ? strtoupper((string)$tables[$table]['engine']) : '';
    }
    
    // --------- Definitions (indexes, unique, foreign keys) ---------
    
    function dbop_defs($prefix, $tables) {
    
        $p = ($prefix !== '') ? ($prefix . '_') : '';
    
        $t_post   = $p . 'post';
        $t_comm   = $p . 'comments';
        $t_users  = $p . 'users';
        $t_extra  = $p . 'post_extras';
        $t_tags   = $p . 'tags';
    
        $has = function($t) use ($tables) { return isset($tables[$t]); };
    
        $defs = array(
            'indexes' => array(),
            'unique'  => array(),
            'fks'     => array(),
        );
    
        // ---- обычные индексы ----
        if ($has($t_post)) {
            $defs['indexes'][] = array(
                'id'    => 'idx_post_approve_date',
                'table' => $t_post,
                'name'  => 'idx_approve_date',
                'cols'  => array('approve','date'),
                'why'   => 'Частая выборка: approve=1 + сортировка по date (главная/категории).',
            );
            $defs['indexes'][] = array(
                'id'    => 'idx_post_autor',
                'table' => $t_post,
                'name'  => 'idx_autor',
                'cols'  => array('autor'),
                'why'   => 'Выборки по автору.',
            );
        }
    
        if ($has($t_comm)) {
            $defs['indexes'][] = array(
                'id'    => 'idx_comments_post_approve_date',
                'table' => $t_comm,
                'name'  => 'idx_post_approve_date',
                'cols'  => array('post_id','approve','date'),
                'why'   => 'Вывод комментариев: post_id + approve + сортировка по date.',
            );
        }
    
        if ($has($t_tags)) {
            $defs['indexes'][] = array(
                'id'    => 'idx_tags_tag',
                'table' => $t_tags,
                'name'  => 'idx_tag',
                'cols'  => array('tag'),
                'why'   => 'Поиск по тегу.',
            );
            $defs['indexes'][] = array(
                'id'    => 'idx_tags_news_id',
                'table' => $t_tags,
                'name'  => 'idx_news_id',
                'cols'  => array('news_id'),
                'why'   => 'Связь тегов с новостями.',
            );
        }
    
        // ---- UNIQUE ----
        if ($has($t_users)) {
            $defs['unique'][] = array(
                'id'    => 'uq_users_name',
                'table' => $t_users,
                'name'  => 'uq_name',
                'cols'  => array('name'),
                'why'   => 'Имя пользователя обычно должно быть уникальным.',
                'dup_sql' => "SELECT `name` AS v, COUNT(*) AS c FROM `{$t_users}` GROUP BY `name` HAVING c>1 LIMIT 50",
            );
            $defs['unique'][] = array(
                'id'    => 'uq_users_email',
                'table' => $t_users,
                'name'  => 'uq_email',
                'cols'  => array('email'),
                'why'   => 'Email часто уникален. Если у вас разрешены одинаковые email - не ставьте.',
                'dup_sql' => "SELECT `email` AS v, COUNT(*) AS c FROM `{$t_users}` WHERE `email`<>'' GROUP BY `email` HAVING c>1 LIMIT 50",
            );
        }
    
        if ($has($t_extra)) {
            $defs['unique'][] = array(
                'id'    => 'uq_post_extras_news_id',
                'table' => $t_extra,
                'name'  => 'uq_news_id',
                'cols'  => array('news_id'),
                'why'   => 'Обычно 1 запись post_extras на 1 новость. Ускоряет JOIN по news_id и защищает от дублей.',
                'dup_sql' => "SELECT `news_id` AS v, COUNT(*) AS c FROM `{$t_extra}` GROUP BY `news_id` HAVING c>1 LIMIT 50",
            );
        }
    
        if ($has($t_tags)) {
            $defs['unique'][] = array(
                'id'    => 'uq_tags_news_tag',
                'table' => $t_tags,
                'name'  => 'uq_news_tag',
                'cols'  => array('news_id','tag'),
                'why'   => 'Убирает дубли связей (news_id + tag).',
                'dup_sql' => "SELECT CONCAT(`news_id`,':',`tag`) AS v, COUNT(*) AS c FROM `{$t_tags}` GROUP BY `news_id`,`tag` HAVING c>1 LIMIT 50",
            );
        }
    
        // ---- FOREIGN KEYS ----
        if ($has($t_comm) && $has($t_post)) {
            $defs['fks'][] = array(
                'id'        => 'fk_comments_post',
                'table'     => $t_comm,
                'col'       => 'post_id',
                'ref_table' => $t_post,
                'ref_col'   => 'id',
                'name'      => 'fk_comments_post',
                'on_delete' => 'CASCADE',
                'on_update' => 'CASCADE',
                'why'       => 'Комментарии должны ссылаться на существующую новость. При удалении новости логично удалять комментарии.',
                'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_comm}` c LEFT JOIN `{$t_post}` p ON p.`id`=c.`post_id` WHERE c.`post_id`<>0 AND p.`id` IS NULL",
            );
        }
    
        if ($has($t_extra) && $has($t_post)) {
            $defs['fks'][] = array(
                'id'        => 'fk_post_extras_post',
                'table'     => $t_extra,
                'col'       => 'news_id',
                'ref_table' => $t_post,
                'ref_col'   => 'id',
                'name'      => 'fk_post_extras_post',
                'on_delete' => 'CASCADE',
                'on_update' => 'CASCADE',
                'why'       => 'post_extras должен ссылаться на новость. При удалении новости extras должны удаляться.',
                'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_extra}` e LEFT JOIN `{$t_post}` p ON p.`id`=e.`news_id` WHERE e.`news_id`<>0 AND p.`id` IS NULL",
            );
        }
    
        if ($has($t_tags) && $has($t_post)) {
            $defs['fks'][] = array(
                'id'        => 'fk_tags_post',
                'table'     => $t_tags,
                'col'       => 'news_id',
                'ref_table' => $t_post,
                'ref_col'   => 'id',
                'name'      => 'fk_tags_post',
                'on_delete' => 'CASCADE',
                'on_update' => 'CASCADE',
                'why'       => 'Связи тегов должны ссылаться на существующую новость.',
                'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_tags}` t LEFT JOIN `{$t_post}` p ON p.`id`=t.`news_id` WHERE t.`news_id`<>0 AND p.`id` IS NULL",
            );
        }
    
        return $defs;
    }
    
    // ------------- Validation for defs -------------
    
    function dbop_validate_index_def($def, $tables) {
        $table = $def['table'];
        if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
        $cols = dbop_get_columns($table);
        $set = array_flip($cols);
        foreach ($def['cols'] as $c) {
            if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
        }
        $idxs = dbop_get_indexes($table);
        if (dbop_has_index_prefix($idxs, $def['cols'], false)) return array(false, 'Индекс уже есть');
        return array(true, '');
    }
    
    function dbop_validate_unique_def($def, $tables) {
        $table = $def['table'];
        if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
        $cols = dbop_get_columns($table);
        $set = array_flip($cols);
        foreach ($def['cols'] as $c) {
            if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
        }
        $idxs = dbop_get_indexes($table);
        if (dbop_has_index_prefix($idxs, $def['cols'], true)) return array(false, 'UNIQUE уже есть');
        global $db;
        if (!empty($def['dup_sql'])) {
            $db->query($def['dup_sql']);
            $row = $db->get_row();
            $hasDup = !empty($row);
            $db->free();
            if ($hasDup) return array(false, 'Есть дубли (сначала исправьте)');
        }
        return array(true, '');
    }
    
    function dbop_validate_fk_def($def, $tables) {
        global $db;
    
        $t  = $def['table'];
        $rt = $def['ref_table'];
        $c  = $def['col'];
        $rc = $def['ref_col'];
    
        if (!isset($tables[$t]) || !isset($tables[$rt])) return array(false, 'Таблица/ссылка не найдена');
    
        $eng1 = dbop_get_table_engine($tables, $t);
        $eng2 = dbop_get_table_engine($tables, $rt);
        if ($eng1 !== 'INNODB' || $eng2 !== 'INNODB') return array(false, 'Требуется InnoDB (обе таблицы)');
    
        $cols1 = array_flip(dbop_get_columns($t));
        $cols2 = array_flip(dbop_get_columns($rt));
        if (!isset($cols1[$c]))  return array(false, 'Нет колонки '.$c.' в '.$t);
        if (!isset($cols2[$rc])) return array(false, 'Нет колонки '.$rc.' в '.$rt);
    
        $types1 = dbop_get_column_types($t);
        $types2 = dbop_get_column_types($rt);
        $ct1 = isset($types1[$c]['type']) ? strtolower($types1[$c]['type']) : '';
        $ct2 = isset($types2[$rc]['type']) ? strtolower($types2[$rc]['type']) : '';
        if ($ct1 && $ct2 && $ct1 !== $ct2) {
            return array(false, 'Типы колонок не совпадают: '.$ct1.' vs '.$ct2);
        }
    
        $idx1 = dbop_get_indexes($t);
        $hasChildIndex = dbop_has_index_prefix($idx1, array($c), false) || dbop_has_index_prefix($idx1, array($c), true);
        if (!$hasChildIndex) return array(false, 'Нужен индекс на '.$t.'.'.$c);
    
        if (!empty($def['orphan_sql'])) {
            $row = $db->super_query($def['orphan_sql']);
            $cnt = isset($row['c']) ? (int)$row['c'] : 0;
            if ($cnt > 0) return array(false, 'Есть сироты: '.$cnt);
        }
    
        if (dbop_fk_exists($t, $c, $rt, $rc)) return array(false, 'FK уже существует');
    
        return array(true, '');
    }
    
    // --------- Apply operations ---------
    
    function dbop_apply_index($def) {
        global $db;
        $table = $def['table'];
        $name  = $def['name'];
        $cols  = $def['cols'];
    
        if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
        foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');
    
        $cols_sql = '`' . implode('`,`', $cols) . '`';
        $sql = "ALTER TABLE `{$table}` ADD INDEX `{$name}` ({$cols_sql})";
        dbop_log('Alter: '.$sql);
        $db->query($sql);
        return array(true, $sql);
    }
    
    function dbop_apply_unique($def) {
        global $db;
        $table = $def['table'];
        $name  = $def['name'];
        $cols  = $def['cols'];
    
        if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
        foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');
    
        $cols_sql = '`' . implode('`,`', $cols) . '`';
        $sql = "ALTER TABLE `{$table}` ADD UNIQUE INDEX `{$name}` ({$cols_sql})";
        dbop_log('Alter: '.$sql);
        $db->query($sql);
        return array(true, $sql);
    }
    
    function dbop_apply_fk($def) {
        global $db;
        $t  = $def['table'];
        $c  = $def['col'];
        $rt = $def['ref_table'];
        $rc = $def['ref_col'];
        $name = $def['name'];
    
        foreach (array($c,$rc,$name) as $x) {
            if (!dbop_is_safe_identifier($x)) return array(false, 'Плохой идентификатор');
        }
    
        $onDel = !empty($def['on_delete']) ? strtoupper($def['on_delete']) : 'RESTRICT';
        $onUpd = !empty($def['on_update']) ? strtoupper($def['on_update']) : 'RESTRICT';
        $allowed = array('RESTRICT','CASCADE','SET NULL','NO ACTION');
        if (!in_array($onDel, $allowed, true)) $onDel = 'RESTRICT';
        if (!in_array($onUpd, $allowed, true)) $onUpd = 'RESTRICT';
    
        $sql = "ALTER TABLE `{$t}` ADD CONSTRAINT `{$name}` FOREIGN KEY (`{$c}`) REFERENCES `{$rt}`(`{$rc}`) ON DELETE {$onDel} ON UPDATE {$onUpd}";
        dbop_log('Alter: '.$sql);
        $db->query($sql);
        return array(true, $sql);
    }
    
    function dbop_convert_engine($table, $engine) {
        global $db;
        $engine = strtoupper($engine);
        if (!in_array($engine, array('INNODB','MYISAM'), true)) return array(false, 'Недопустимый engine');
        $sql = "ALTER TABLE `{$table}` ENGINE={$engine}";
        dbop_log('Alter: '.$sql);
        $db->query($sql);
        return array(true, $sql);
    }
    
    // ---------------- Controller ----------------
    
    $prefix = defined('PREFIX') ? PREFIX : ((isset($config['dbprefix']) && $config['dbprefix']) ? $config['dbprefix'] : '');
    $only_prefix = true;
    if (isset($_GET['show_all']) && (int)$_GET['show_all'] === 1) $only_prefix = false;
    
    $tables = dbop_get_tables($prefix, $only_prefix);
    $defs   = dbop_defs($prefix, $tables);
    
    $tab = isset($_REQUEST['tab']) ? (string)$_REQUEST['tab'] : 'home';
    $action = isset($_REQUEST['action']) ? (string)$_REQUEST['action'] : '';
    
    function dbop_json_exit($arr) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($arr, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        exit;
    }
    
    // AJAX actions
    if ($action) {
        dbop_require_hash();
    
        if ($action === 'sections') {
            // добавить/убрать из "Сторонние модули" (на случай, если запись не создалась при установке)
            global $db;
            $row = $db->super_query("SELECT id FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
            if ($row) {
                $db->query("DELETE FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
                @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
                dbop_json_exit(array('success'=>true,'message'=>'Ссылка убрана из меню «Сторонние модули»'));
            } else {
                $db->query("INSERT IGNORE INTO " . PREFIX . "_admin_sections (name, title, descr, icon, allow_groups)
                            VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1')");
                @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
                dbop_json_exit(array('success'=>true,'message'=>'Ссылка добавлена в меню «Сторонние модули»'));
            }
        }
    
        if ($action === 'maintenance') {
            $cmd = strtolower((string)($_POST['cmd'] ?? 'analyze'));
            $allowed_cmds = array('analyze'=>'ANALYZE','check'=>'CHECK','optimize'=>'OPTIMIZE','repair'=>'REPAIR');
            if (!isset($allowed_cmds[$cmd])) dbop_json_exit(array('success'=>false,'message'=>'Неизвестная команда'));
    
            $sel = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
            $sel = array_values(array_unique(array_map('strval',$sel)));
            $safe = array();
            foreach ($sel as $t) if (isset($tables[$t])) $safe[] = $t;
            if (!$safe) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));
    
            $results = array();
            foreach ($safe as $tname) {
                $sqlCmd = $allowed_cmds[$cmd];
    
                if ($sqlCmd === 'REPAIR') {
                    $eng = strtoupper((string)$tables[$tname]['engine']);
                    if ($eng !== 'MYISAM') {
                        $results[] = array('table'=>$tname,'status'=>'SKIP','msg'=>'REPAIR только для MyISAM');
                        continue;
                    }
                }
    
                $q = "{$sqlCmd} TABLE `{$tname}`";
                dbop_log('Run: '.$q);
                $db->query($q);
                while ($r = $db->get_row()) {
                    $results[] = array('table'=>$r['Table'],'type'=>$r['Msg_type'],'msg'=>$r['Msg_text']);
                }
                $db->free();
            }
    
            dbop_json_exit(array('success'=>true,'message'=>'Готово','results'=>$results));
        }
    
        if ($action === 'apply_indexes' || $action === 'apply_unique' || $action === 'apply_fks') {
            $items = isset($_POST['items']) && is_array($_POST['items']) ? $_POST['items'] : array();
            $items = array_values(array_unique(array_map('strval',$items)));
            if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Ничего не выбрано'));
    
            $kind = ($action === 'apply_indexes') ? 'indexes' : (($action === 'apply_unique') ? 'unique' : 'fks');
            $defsList = $defs[$kind];
    
            $map = array();
            foreach ($defsList as $d) $map[$d['id']] = $d;
    
            $applied = array();
            $skipped = array();
    
            foreach ($items as $id) {
                if (!isset($map[$id])) { $skipped[] = array('id'=>$id,'reason'=>'unknown'); continue; }
                $d = $map[$id];
    
                if ($kind === 'indexes') {
                    list($ok,$why) = dbop_validate_index_def($d, $tables);
                    if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                    list($ok2,$sql) = dbop_apply_index($d);
                    if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                    $applied[] = $sql;
                }
                if ($kind === 'unique') {
                    list($ok,$why) = dbop_validate_unique_def($d, $tables);
                    if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                    list($ok2,$sql) = dbop_apply_unique($d);
                    if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                    $applied[] = $sql;
                }
                if ($kind === 'fks') {
                    list($ok,$why) = dbop_validate_fk_def($d, $tables);
                    if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                    list($ok2,$sql) = dbop_apply_fk($d);
                    if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                    $applied[] = $sql;
                }
            }
    
            dbop_json_exit(array(
                'success'=>true,
                'message'=>'Операция завершена',
                'applied'=>$applied,
                'skipped'=>$skipped,
            ));
        }
    
        if ($action === 'convert_engine') {
            $items = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
            $items = array_values(array_unique(array_map('strval',$items)));
            $target = strtoupper((string)($_POST['target'] ?? 'INNODB'));
            if (!in_array($target, array('INNODB','MYISAM'), true)) $target = 'INNODB';
            if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));
    
            $applied = array();
            $skipped = array();
    
            foreach ($items as $t) {
                if (!isset($tables[$t])) { $skipped[] = array('table'=>$t,'reason'=>'unknown'); continue; }
                $cur = strtoupper((string)$tables[$t]['engine']);
                if ($cur === $target) { $skipped[] = array('table'=>$t,'reason'=>'уже '.$target); continue; }
                list($ok,$sql) = dbop_convert_engine($t, $target);
                if (!$ok) { $skipped[] = array('table'=>$t,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
    
            dbop_json_exit(array('success'=>true,'message'=>'Готово','applied'=>$applied,'skipped'=>$skipped));
        }
    
        dbop_json_exit(array('success'=>false,'message'=>'Unknown action'));
    }
    
    // ---------------- UI (Admin) ----------------
    
    $__name  = 'DBOptimizerPro';
    $__descr = 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)';
    
    echoheader($__name, $__descr);
    
    $adminUrl = $config['admin_path'] ?? 'admin.php';
    $userHash = $dle_login_hash;
    
    $box = ($config['version_id'] >= 12) ? 'panel' : 'box';
    $boxHeader = ($config['version_id'] >= 12) ? 'panel-heading' : 'box-header';
    $boxContent = 'box-content';
    $boxFooter = ($config['version_id'] >= 12) ? 'panel-footer' : 'box-footer';
    $tabsClass = ($config['version_id'] >= 12) ? 'nav-tabs-solid' : 'nav-tabs-left';
    
    function dbop_tab_link($tab, $label, $icon='') {
        $u = 'admin.php?mod=db_optimizer&tab='.$tab.(isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : '');
        $i = $icon ? '<i class="'.$icon.'"></i> ' : '';
        $active = ((isset($_REQUEST['tab']) && $_REQUEST['tab']===$tab) || (!isset($_REQUEST['tab']) && $tab==='home')) ? ' class="active"' : '';
        return '<li'.$active.'><a href="'.$u.'">'.$i.dbop_h($label).'</a></li>';
    }
    
    echo '<style>
    /* читабельные табы (у вас были "белое на белом") */
    .dbop-wrap .nav-tabs>li>a{color:#333 !important; background:#f6f7f9; border:1px solid #e2e5ea; margin-right:6px; border-bottom:none;}
    .dbop-wrap .nav-tabs>li.active>a{background:#ffffff !important; color:#000 !important; font-weight:600;}
    .dbop-wrap .nav-tabs{border-bottom:1px solid #e2e5ea;}
    .dbop-steps .step{border:1px solid #e5e5e5;border-radius:10px;padding:12px;margin:10px 0;background:#fff;}
    .dbop-steps .step h4{margin:0 0 6px 0;}
    .dbop-small{color:#666;font-size:12px;}
    .dbop-badge{display:inline-block;padding:3px 8px;border-radius:999px;font-size:12px;border:1px solid #ddd;background:#fafafa;}
    .dbop-badge.ok{background:#e8fff0;border-color:#bfe8cd;}
    .dbop-badge.bad{background:#fff0f0;border-color:#f1c1c1;}
    .dbop-badge.warn{background:#fff8e6;border-color:#f2d49a;}
    .dbop-table code{font-size:12px;}
    </style>';
    
    echo '<div class="dbop-wrap '.$box.'">';
    echo '  <div class="'.$boxHeader.'">';
    echo '    <ul class="nav nav-tabs '.$tabsClass.'">';
    echo          dbop_tab_link('home', 'Быстрый старт', 'icon-home');
    echo          dbop_tab_link('tables', 'Таблицы', 'icon-database');
    echo          dbop_tab_link('indexes', 'Индексы', 'icon-list');
    echo          dbop_tab_link('unique', 'UNIQUE', 'icon-check');
    echo          dbop_tab_link('fks', 'FOREIGN KEY', 'icon-link');
    echo          dbop_tab_link('docs', 'Инструкция', 'icon-book');
    echo '    </ul>';
    echo '  </div>';
    
    echo '  <div class="'.$boxContent.'">';
    
    echo '<div class="alert alert-warning" style="margin-bottom:10px;">
    <b>DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</b><br>
    Изменения структуры (ALTER TABLE, OPTIMIZE) могут быть долгими и блокировать таблицы.
    Перед применением сделайте бэкап. Лог: <code>/engine/data/dboptimizerpro.log</code>
    </div>';
    
    if ($prefix === '') {
        echo '<div class="alert alert-danger"><b>Проблема:</b> не удалось определить PREFIX. Модуль не знает, какие таблицы считать DLE.</div>';
    } else {
        echo '<div class="alert alert-info" style="margin-bottom:10px;">
    <b>Фильтр:</b> сейчас показаны '.($only_prefix ? 'только таблицы DLE (<code>'.dbop_h($prefix).'_*</code>)' : 'все таблицы текущей БД').'.
    <a href="admin.php?mod=db_optimizer&tab='.dbop_h($tab).($only_prefix ? '&show_all=1' : '').'">'.($only_prefix ? 'Показать все' : 'Показать только DLE').'</a>
    </div>';
    }
    
    if ($tab === 'home') {
    
        $totalData = 0; $totalIdx = 0;
        foreach ($tables as $t) { $totalData += $t['data']; $totalIdx += $t['index']; }
    
        echo '<div class="dbop-steps">';
    
        echo '<div class="step">
            <h4>Сценарий 1. Безопасно и быстро (рекомендуется)</h4>
            <div class="dbop-small">Обновить статистику БД и добавить обычные индексы (без UNIQUE и FOREIGN KEY).</div>
            <ol style="margin:8px 0 0 18px;">
              <li>Вкладка <b>Таблицы</b> → выделите таблицы → <b>ANALYZE</b> → "Выполнить".</li>
              <li>Вкладка <b>Индексы</b> → отметьте "можно добавить" → "Добавить выбранные индексы".</li>
            </ol>
        </div>';
    
        echo '<div class="step">
            <h4>Сценарий 2. Уникальность данных (UNIQUE)</h4>
            <div class="dbop-small">UNIQUE не применится, если в таблице есть дубли - модуль покажет примеры.</div>
            <ol style="margin:8px 0 0 18px;">
              <li>Вкладка <b>UNIQUE</b> → проверьте статус.</li>
              <li>Если есть дубли - сначала исправьте, потом применяйте.</li>
            </ol>
        </div>';
    
        echo '<div class="step">
            <h4>Сценарий 3. FOREIGN KEY (осторожно)</h4>
            <div class="dbop-small">FOREIGN KEY повышает целостность. Требует InnoDB и отсутствие "сирот".</div>
            <ol style="margin:8px 0 0 18px;">
              <li>Вкладка <b>FOREIGN KEY</b> → убедитесь, что всё "можно добавить".</li>
              <li>Если таблицы не InnoDB - вкладка <b>Таблицы</b> → конвертация в InnoDB.</li>
            </ol>
        </div>';
    
        echo '</div>';
    
        echo '<hr>';
        echo '<div class="row"><div class="col-md-6">
            <div class="alert alert-info">
            <b>Сводка</b><br>
            Таблиц: <b>'.count($tables).'</b><br>
            Data: <b>'.dbop_h(dbop_bytes($totalData)).'</b><br>
            Index: <b>'.dbop_h(dbop_bytes($totalIdx)).'</b><br>
            Total: <b>'.dbop_h(dbop_bytes($totalData+$totalIdx)).'</b>
            </div>
        </div><div class="col-md-6">
            <div class="alert alert-default">
            <b>Что реально ускоряет чаще всего</b><br>
            1) индексы под реальные WHERE/ORDER BY<br>
            2) кэш (Redis/Memcached), PHP-FPM, настройки MySQL<br>
            3) анализ slow query log и правка запросов
            </div>
        </div></div>';
    }
    
    if ($tab === 'tables') {
    
        echo '<h3 style="margin-top:0;">Таблицы и обслуживание</h3>';
        echo '<div class="dbop-small">Команды: <b>ANALYZE</b> (рекомендуется), <b>CHECK</b>, <b>OPTIMIZE</b> (осторожно), <b>REPAIR</b> (только MyISAM).</div>';
    
        echo '<div class="alert alert-warning" style="margin-top:10px;">
    <b>OPTIMIZE</b> на InnoDB часто равен перестройке таблицы и может быть очень долгим.
    Используйте после массовых удалений или если видите реальную фрагментацию/раздувание.
    </div>';
    
        echo '<form id="dbop-maint-form" onsubmit="return false;">';
        echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';
    
        echo '<div style="margin:8px 0;">';
        echo '<select name="cmd" class="uniform">
                <option value="analyze">ANALYZE (рекомендуется)</option>
                <option value="check">CHECK</option>
                <option value="optimize">OPTIMIZE (осторожно)</option>
                <option value="repair">REPAIR (только MyISAM)</option>
              </select> ';
        echo '<button class="btn btn-primary" onclick="dbop_runMaintenance();">Выполнить команду на выбранных таблицах</button>';
        echo '</div>';
    
        echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
        echo '<thead><tr>
            <th style="width:30px;"><input type="checkbox" onclick="dbop_toggleAll(this, \'tables[]\');"></th>
            <th>Таблица</th>
            <th>Engine</th>
            <th style="text-align:right;">Rows*</th>
            <th style="text-align:right;">Data</th>
            <th style="text-align:right;">Index</th>
            <th style="text-align:right;">Total</th>
            <th style="text-align:right;">Free</th>
        </tr></thead><tbody>';
    
        foreach ($tables as $t) {
            $total = $t['data'] + $t['index'];
            $eng = strtoupper((string)$t['engine']);
            $badge = ($eng==='INNODB') ? 'ok' : (($eng==='MYISAM') ? 'warn' : 'warn');
            echo '<tr>
                <td><input type="checkbox" name="tables[]" value="'.dbop_h($t['table']).'"></td>
                <td><code>'.dbop_h($t['table']).'</code></td>
                <td><span class="dbop-badge '.$badge.'">'.dbop_h($eng).'</span></td>
                <td style="text-align:right;">'.dbop_h($t['rows']).'</td>
                <td style="text-align:right;">'.dbop_h(dbop_bytes($t['data'])).'</td>
                <td style="text-align:right;">'.dbop_h(dbop_bytes($t['index'])).'</td>
                <td style="text-align:right;"><b>'.dbop_h(dbop_bytes($total)).'</b></td>
                <td style="text-align:right;">'.dbop_h(dbop_bytes($t['free'])).'</td>
            </tr>';
        }
    
        echo '</tbody></table>';
        echo '<div class="dbop-small">* TABLE_ROWS в InnoDB - оценка, не точное значение.</div>';
        echo '</form>';
    
        echo '<hr><h4>Переключить движок таблиц (для FOREIGN KEY нужен InnoDB)</h4>';
        echo '<div class="dbop-small">Конвертация - это ALTER TABLE и может быть долгой. Делайте в непиковое время.</div>';
        echo '<div style="margin:8px 0;">';
        echo '<select id="dbop-engine-target" class="uniform"><option value="INNODB">INNODB</option><option value="MYISAM">MYISAM</option></select> ';
        echo '<button class="btn btn-warning" onclick="dbop_convertEngine();">Конвертировать выбранные таблицы</button>';
        echo '</div>';
    
        echo '<div id="dbop-maint-result"></div>';
    }
    
    if ($tab === 'indexes') {
    
        echo '<h3 style="margin-top:0;">Индексы (обычные)</h3>';
        echo '<div class="dbop-small">Индексы обычно безопаснее, чем UNIQUE/FK. Они ускоряют SELECT, но увеличивают размер БД и могут немного замедлить INSERT/UPDATE.</div>';
    
        if (!$defs['indexes']) {
            echo '<div class="alert alert-warning">Список рекомендуемых индексов пуст. Обычно это значит, что не найдены таблицы <code>'.dbop_h($prefix).'_post</code>/<code>'.dbop_h($prefix).'_comments</code> и т.д.</div>';
        }
    
        $rows = array();
        foreach ($defs['indexes'] as $d) {
            $table = $d['table'];
            $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();
            list($ok,$reason) = dbop_validate_index_def($d, $tables);
            $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], false) : false;
            $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
            $rows[] = array($d, $status);
        }
    
        echo '<form id="dbop-index-form" onsubmit="return false;">';
        echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';
    
        echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
        echo '<thead><tr>
            <th style="width:30px;"></th>
            <th>Статус</th>
            <th>Таблица</th>
            <th>Индекс</th>
            <th>Колонки</th>
            <th>Зачем</th>
        </tr></thead><tbody>';
    
        foreach ($rows as $r) {
            $d = $r[0];
            $st = $r[1];
            $can = ($st[0] === 'warn');
            echo '<tr>
                <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
                <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
                <td><code>'.dbop_h($d['table']).'</code></td>
                <td><code>'.dbop_h($d['name']).'</code></td>
                <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
                <td>'.dbop_h($d['why']).'</td>
            </tr>';
        }
    
        echo '</tbody></table>';
    
        echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_indexes\', \'#dbop-index-form\');">Добавить выбранные индексы</button>';
        echo '<div id="dbop-index-result" style="margin-top:10px;"></div>';
        echo '</form>';
    }
    
    if ($tab === 'unique') {
    
        echo '<h3 style="margin-top:0;">UNIQUE ключи (с проверкой дублей)</h3>';
        echo '<div class="dbop-small">UNIQUE не применится, если уже есть дубликаты. Плагин покажет примеры дублей.</div>';
    
        if (!$defs['unique']) {
            echo '<div class="alert alert-warning">Список UNIQUE пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
        }
    
        $rows = array();
        foreach ($defs['unique'] as $d) {
            $table = $d['table'];
            $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();
    
            $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], true) : false;
    
            list($ok,$reason) = dbop_validate_unique_def($d, $tables);
            $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
            $rows[] = array($d,$status);
        }
    
        echo '<form id="dbop-unique-form" onsubmit="return false;">';
        echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';
    
        echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
        echo '<thead><tr>
            <th style="width:30px;"></th>
            <th>Статус</th>
            <th>Таблица</th>
            <th>UNIQUE</th>
            <th>Колонки</th>
            <th>Комментарий</th>
            <th>Дубли (если есть)</th>
        </tr></thead><tbody>';
    
        foreach ($rows as $r) {
            $d = $r[0]; $st = $r[1];
            $can = ($st[0] === 'warn');
            $dupPreview = '';
            if ($st[0] === 'bad' && strpos($st[1], 'дубли') !== false && !empty($d['dup_sql'])) {
                $db->query($d['dup_sql']);
                $i = 0;
                $list = array();
                while (($row = $db->get_row()) && $i < 5) {
                    $list[] = $row['v'].' (x'.$row['c'].')';
                    $i++;
                }
                $db->free();
                if ($list) $dupPreview = implode('<br>', array_map('dbop_h', $list));
            }
    
            echo '<tr>
                <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
                <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
                <td><code>'.dbop_h($d['table']).'</code></td>
                <td><code>'.dbop_h($d['name']).'</code></td>
                <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
                <td>'.dbop_h($d['why']).'</td>
                <td>'.($dupPreview ?: '<span class="dbop-small">-</span>').'</td>
            </tr>';
        }
    
        echo '</tbody></table>';
    
        echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_unique\', \'#dbop-unique-form\');">Добавить выбранные UNIQUE</button>';
        echo '<div id="dbop-unique-result" style="margin-top:10px;"></div>';
        echo '</form>';
    }
    
    if ($tab === 'fks') {
    
        echo '<h3 style="margin-top:0;">FOREIGN KEY (внешние ключи)</h3>';
        echo '<div class="alert alert-info">
    <b>Что будет сделано:</b> добавятся связи между связанными таблицами DLE (например comments.post_id → post.id).<br>
    <b>Почему это может не примениться:</b> не InnoDB, разные типы колонок, нет индекса на дочерней колонке, есть "сироты".
    </div>';
    
        if (!$defs['fks']) {
            echo '<div class="alert alert-warning">Список FOREIGN KEY пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
        }
    
        $needInno = array();
        foreach ($defs['fks'] as $d) {
            $needInno[$d['table']] = true;
            $needInno[$d['ref_table']] = true;
        }
    
        $notInno = array();
        foreach (array_keys($needInno) as $t) {
            if (isset($tables[$t])) {
                $eng = strtoupper((string)$tables[$t]['engine']);
                if ($eng !== 'INNODB') $notInno[] = $t.' ('.$eng.')';
            }
        }
    
        if ($notInno) {
            echo '<div class="alert alert-warning"><b>Сейчас не InnoDB:</b><br>'.implode('<br>', array_map('dbop_h', $notInno)).'<br>
            Сначала переведите эти таблицы в InnoDB (вкладка "Таблицы").</div>';
        } elseif ($defs['fks']) {
            echo '<div class="alert alert-success">Нужные таблицы уже InnoDB.</div>';
        }
    
        $rows = array();
        foreach ($defs['fks'] as $d) {
            list($ok,$reason) = dbop_validate_fk_def($d, $tables);
            $exists = dbop_fk_exists($d['table'], $d['col'], $d['ref_table'], $d['ref_col']);
            $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
    
            $orphanInfo = '';
            if (!empty($d['orphan_sql'])) {
                $row = $db->super_query($d['orphan_sql']);
                $cnt = isset($row['c']) ? (int)$row['c'] : 0;
                $orphanInfo = ($cnt>0) ? '<span class="dbop-badge bad">сироты: '.dbop_h($cnt).'</span>' : '<span class="dbop-badge ok">сирот нет</span>';
            }
    
            $rows[] = array($d,$status,$orphanInfo);
        }
    
        echo '<form id="dbop-fk-form" onsubmit="return false;">';
        echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';
    
        echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
        echo '<thead><tr>
            <th style="width:30px;"></th>
            <th>Статус</th>
            <th>Связь</th>
            <th>Действия</th>
            <th>Зачем</th>
        </tr></thead><tbody>';
    
        foreach ($rows as $r) {
            $d = $r[0]; $st = $r[1]; $or = $r[2];
            $can = ($st[0] === 'warn');
            $rel = '<code>'.dbop_h($d['table']).'.'.$d['col'].'</code> → <code>'.dbop_h($d['ref_table']).'.'.$d['ref_col'].'</code>';
            $act = 'ON DELETE '.dbop_h($d['on_delete']).', ON UPDATE '.dbop_h($d['on_update']).' &nbsp; '.$or;
            echo '<tr>
                <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
                <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
                <td>'.$rel.'<br><span class="dbop-small">constraint: <code>'.dbop_h($d['name']).'</code></span></td>
                <td>'.$act.'</td>
                <td>'.dbop_h($d['why']).'</td>
            </tr>';
        }
    
        echo '</tbody></table>';
    
        echo '<div class="alert alert-warning"><b>Перед применением FK:</b> убедитесь, что у вас есть бэкап. Если таблицы большие, ALTER может идти долго.</div>';
        echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_fks\', \'#dbop-fk-form\');">Добавить выбранные FOREIGN KEY</button>';
        echo '<div id="dbop-fk-result" style="margin-top:10px;"></div>';
        echo '</form>';
    }
    
    if ($tab === 'docs') {
    
        echo '<h3 style="margin-top:0;">Инструкция</h3>';
    
        echo '<div class="panel panel-default"><div class="panel-heading"><b>1) Что это делает</b></div><div class="panel-body">
        <ul>
          <li><b>ANALYZE / CHECK / OPTIMIZE / REPAIR</b> - обслуживание таблиц.</li>
          <li><b>Индексы</b> - предлагает и добавляет индексы (если их нет).</li>
          <li><b>UNIQUE</b> - добавляет уникальные ключи только если нет дублей.</li>
          <li><b>FOREIGN KEY</b> - добавляет внешние ключи только если таблицы InnoDB, типы совпадают, нет сирот и есть индекс на дочерней колонке.</li>
        </ul>
        </div></div>';
    
        echo '<div class="panel panel-default"><div class="panel-heading"><b>2) Как работать - по шагам</b></div><div class="panel-body">
          <div class="dbop-steps">
            <div class="step">
              <h4>Шаг A: Обновить статистику</h4>
              <div>Вкладка <b>Таблицы</b> → выделить таблицы → <b>ANALYZE</b> → "Выполнить".</div>
            </div>
            <div class="step">
              <h4>Шаг B: Добавить индексы</h4>
              <div>Вкладка <b>Индексы</b> → отметить "можно добавить" → "Добавить выбранные индексы".</div>
            </div>
            <div class="step">
              <h4>Шаг C: UNIQUE (по желанию)</h4>
              <div>Вкладка <b>UNIQUE</b> → если есть дубли - сначала исправить дубли, потом добавлять.</div>
            </div>
            <div class="step">
              <h4>Шаг D: FOREIGN KEY (по желанию)</h4>
              <div>Вкладка <b>FOREIGN KEY</b> → проверить InnoDB + сироты. Если нужно - вкладка <b>Таблицы</b> → конвертация в InnoDB. После этого применить.</div>
            </div>
          </div>
        </div></div>';
    
        echo '<div class="panel panel-default"><div class="panel-heading"><b>3) Если пункт меню не появился</b></div><div class="panel-body">
          <div class="dbop-small">Иногда запись в <code>'.dbop_h(PREFIX).'_admin_sections</code> не добавляется (например, из-за ошибки установки). Нажмите кнопку ниже.</div>
          <button class="btn btn-primary" onclick="dbop_toggleSections(); return false;">Ссылка в меню «Сторонние модули» (вкл/выкл)</button>
          <div id="dbop-sections-result" style="margin-top:10px;"></div>
        </div></div>';
    
        echo '<div class="panel panel-default"><div class="panel-heading"><b>4) Логи</b></div><div class="panel-body">
          <div>Лог всех ALTER/команд: <code>/engine/data/dboptimizerpro.log</code></div>
        </div></div>';
    }
    
    echo '  </div>'; // content
    
    echo '  <div class="'.$boxFooter.'" style="display:flex;justify-content:space-between;align-items:center;">';
    echo '    <div class="dbop-small">DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</div>';
    echo '    <div class="dbop-small">prefix: <code>'.dbop_h($prefix).'</code></div>';
    echo '  </div>';
    
    echo '</div>'; // box
    
    ?>
    <script>
    function dbop_toggleAll(cb, name){
      var c = cb.checked;
      document.querySelectorAll('input[name="'+name+'"]').forEach(function(x){ x.checked = c; });
    }
    function dbop_applySelected(action, formSelector){
      var form = document.querySelector(formSelector);
      var items = [];
      form.querySelectorAll('input[name="items[]"]:checked').forEach(function(x){ items.push(x.value); });
      if (!items.length) { DLEalert('Ничего не выбрано', 'DBOptimizerPro'); return; }
      if (!confirm('Применить выбранные изменения? На больших таблицах это может занять время.')) return;
    
      var fd = new FormData();
      fd.append('user_hash', '<?=dbop_h($userHash)?>');
      items.forEach(function(v){ fd.append('items[]', v); });
    
      fetch('admin.php?mod=db_optimizer&action='+encodeURIComponent(action)+'&tab=<?=dbop_h($tab)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
        method: 'POST',
        body: fd,
        credentials: 'same-origin'
      }).then(r=>r.json()).then(function(data){
        var boxId = (action==='apply_indexes') ? 'dbop-index-result' : (action==='apply_unique' ? 'dbop-unique-result' : 'dbop-fk-result');
        var el = document.getElementById(boxId);
        if (!el) return;
    
        var html = '';
        if (data.success) {
          html += '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
          if (data.applied && data.applied.length) {
            html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
          }
          if (data.skipped && data.skipped.length) {
            html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
          }
        } else {
          html += '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
        }
        el.innerHTML = html;
      }).catch(function(e){
        DLEalert('Ошибка: '+e, 'DBOptimizerPro');
      });
    }
    function dbop_runMaintenance(){
      var form = document.getElementById('dbop-maint-form');
      if (!form) return;
      var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
      if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
      if (!confirm('Выполнить команду обслуживания на выбранных таблицах?')) return;
    
      var fd = new FormData(form);
      fetch('admin.php?mod=db_optimizer&action=maintenance&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
        method: 'POST',
        body: fd,
        credentials: 'same-origin'
      }).then(r=>r.json()).then(function(data){
        var el = document.getElementById('dbop-maint-result');
        if (!el) return;
        if (!data.success) {
          el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
          return;
        }
        var rows = data.results || [];
        var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
        html += '<div class="alert alert-info"><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(rows, null, 2))+'</pre></div>';
        el.innerHTML = html;
      }).catch(function(e){
        DLEalert('Ошибка: '+e, 'DBOptimizerPro');
      });
    }
    function dbop_convertEngine(){
      var form = document.getElementById('dbop-maint-form');
      if (!form) return;
      var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
      if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
      if (!confirm('Конвертировать выбранные таблицы? Это ALTER TABLE и может быть долгим.')) return;
    
      var fd = new FormData();
      fd.append('user_hash', '<?=dbop_h($userHash)?>');
      var target = document.getElementById('dbop-engine-target') ? document.getElementById('dbop-engine-target').value : 'INNODB';
      fd.append('target', target);
      form.querySelectorAll('input[name="tables[]"]:checked').forEach(function(x){ fd.append('tables[]', x.value); });
    
      fetch('admin.php?mod=db_optimizer&action=convert_engine&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
        method: 'POST',
        body: fd,
        credentials: 'same-origin'
      }).then(r=>r.json()).then(function(data){
        var el = document.getElementById('dbop-maint-result');
        if (!el) return;
        if (!data.success) {
          el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
          return;
        }
        var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
        if (data.applied && data.applied.length) html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
        if (data.skipped && data.skipped.length) html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
        el.innerHTML = html;
      }).catch(function(e){
        DLEalert('Ошибка: '+e, 'DBOptimizerPro');
      });
    }
    function dbop_toggleSections(){
      fetch('admin.php?mod=db_optimizer&action=sections&tab=docs&user_hash=<?=dbop_h($userHash)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
        method: 'GET',
        credentials: 'same-origin'
      }).then(r=>r.json()).then(function(data){
        var el = document.getElementById('dbop-sections-result');
        if (!el) { DLEalert(data.message||'Готово', 'DBOptimizerPro'); return; }
        el.innerHTML = data.success ? '<div class="alert alert-success">'+escapeHtml(data.message)+'</div>' : '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
      }).catch(function(e){
        DLEalert('Ошибка: '+e, 'DBOptimizerPro');
      });
    }
    function escapeHtml(s){
      return String(s).replace(/[&<>"']/g, function(m){
        return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]);
      });
    }
    </script>
    <?php echofooter(); ?>
    
    ]]></replacecode>
          <enabled>1</enabled>
        </operation>
      </file>
    
    </dleplugin>
    
     
    • Нравится Нравится x 1
  16. Skyworker

    Skyworker Бывалый

    Регистрация:
    4 май 2025
    Сообщения:
    354
    Лучших ответов:
    0
    Рейтинги:
    +81 / 19 / -0
    1. Взять сервер шустрее, более производительный с хорошим процом и большим объемом памяти;
    2. Установить и настроить Redis на сервере
    3. После того, как возьмешь другой сервер, настрой и оптимизируй MySQL для быстрой работы;
    4. Найти плагины, которые ускоряют и оптимизируют базу данных, желательно платные, чтобы они не сделали вредоносные инъекции.
     
  17. Олег Lego

    Олег Lego Местный

    Регистрация:
    11 май 2020
    Сообщения:
    2.006
    Лучших ответов:
    1
    Рейтинги:
    +492 / 217 / -0
    Вот так примерно можно оптимизировать сервер с сайтом на dle и с более большей базой. Подогнать под своё железо Настройка-оптимизация VPS сервера (Стр. 1) - Visavi.net
     
    #17 Олег Lego, 23 дек 2025
    Последнее редактирование: 24 дек 2025
    • Нравится Нравится x 1
  18. sneiks

    Команда форума VIP Кинотрафик v2

    Регистрация:
    27 янв 2016
    Сообщения:
    344
    Лучших ответов:
    0
    Рейтинги:
    +58 / 2 / -0
    Вы автор плагина?
     
  19. level

    level Новичок

    Регистрация:
    26 ноя 2024
    Сообщения:
    43
    Лучших ответов:
    0
    Рейтинги:
    +2 / 3 / -0
    да и нет (с приминением ии)


    сделйте бэкап бд и протестите.

    - анализ
    - добавление индексов
    - уникальные ключи
    - первичные ключи
     
    #19 level, 24 дек 2025
    Последнее редактирование: 24 дек 2025
  20. level

    level Новичок

    Регистрация:
    26 ноя 2024
    Сообщения:
    43
    Лучших ответов:
    0
    Рейтинги:
    +2 / 3 / -0
    art994 сообщи пожалуйста о результатах, может что то доработать нужно (кроме CSS плагина)
     
Похожие темы
  1. Serhii
    Ответов:
    2
    Просмотров:
    1.369
  2. pread
    Ответов:
    196
    Просмотров:
    13.212
  3. Str0ng
    Ответов:
    8
    Просмотров:
    2.607
  4. PunPun
    Ответов:
    99
    Просмотров:
    22.149
  5. Ivan000
    Ответов:
    24
    Просмотров:
    5.372
Загрузка...
Яндекс.Метрика