• Resolved carlos978

    (@carlos978)


    ## Problem Description

    I'm using the **Redirect Redirection** plugin and Query Monitor is showing a critical performance issue: **81 duplicate SQL queries** on every page load.

    ## Duplicate Queries Detected

    ### 1. getRules() - Executed 4 times per request

    sql<br><br>SELECT * FROM wp_irrp_redirections<br><br>WHERE type = 'redirection_rule' AND status = 1<br><br>ORDER BY timestamp DESC, id DESC<br><br>

    **Impact:** Same query called 4 times per request

    ### 2. getMeta() - Executed 75 times (with 25 redirect rules)

    sql<br><br>SELECT meta_value FROM wp_irrp_redirectionmeta<br><br>WHERE redirect_id = X AND meta_key = 'criterias'<br><br>LIMIT 1<br><br>

    **Impact:** 25 rules × 3 calls = 75 queries

    ### 3. getMatched() - Executed 2 times per request

    sql<br><br>SELECT * FROM wp_irrp_redirections<br><br>WHERE (LOWER(match) LIKE LOWER('/url'))<br><br>AND status = '1' AND type = 'redirection'<br><br>LIMIT 1<br><br>

    ## Root Cause

    This is a classic **N+1 query problem**:

    1. irrpInit() calls getRules()**1 query**

    2. For each rule, getMeta() is called in:

       - isAre404sRuleExists()**25 queries**

       - isAllURLsRuleExists()**25 queries**

       - getRedirectData()**25 queries**

    3. getRedirectData() calls getRules() again → **3 more queries**

    **Total:** 81 queries for what should be ~6-8 queries

    ## Affected Files

    - redirect-redirection.php (line 122)

    - includes/irrp-db-manager.php (lines 518, 566, 589)

    - includes/irrp-helper.php (lines 326, 421)

    ## Proposed Solution

    ### Option 1: In-Memory Cache (Simplest Fix)

    Add a simple caching system to IRRPDBManager class:

    php<br><br>class IRRPDBManager implements IRRPConstants {<br><br>    // Add cache property<br><br>    private $cache = &#091;<br><br>        'rules' => null,<br><br>        'meta' => &#091;]<br><br>    ];<br><br>    /**<br><br>     * Get rules with caching<br><br>     */<br><br>    public function getRules($status = 1) {<br><br>        $cache_key = 'rules_' . $status;<br><br>        // Return cached result if available<br><br>        if ($this->cache&#091;'rules']&#091;$cache_key] !== null) {<br><br>            return $this->cache&#091;'rules']&#091;$cache_key];<br><br>        }<br><br>        // Original query<br><br>        global $wpdb;<br><br>        $sql = "SELECT * FROM {$this->tblRedirects} WHERE type = '" . self::TYPE_REDIRECTION_RULE . "'";<br><br>        if (trim($status) !== "") {<br><br>            $sql .= " AND status = " . (int) $status;<br><br>        }<br><br>        $sql .= " ORDER BY timestamp DESC, id DESC;";<br><br>        $results = $wpdb->get_results($sql, ARRAY_A);<br><br>        // Cache the result<br><br>        $this->cache&#091;'rules']&#091;$cache_key] = $results;<br><br>        return $results;<br><br>    }<br><br>    /**<br><br>     * Get meta with caching<br><br>     */<br><br>    public function getMeta($id, $metaKey = "") {<br><br>        $cache_key = $id . '_' . $metaKey;<br><br>        // Return cached result if available<br><br>        if (isset($this->cache&#091;'meta']&#091;$cache_key])) {<br><br>            return $this->cache&#091;'meta']&#091;$cache_key];<br><br>        }<br><br>        // Original query<br><br>        global $wpdb;<br><br>        if ($metaKey) {<br><br>            $sql = $wpdb->prepare(<br><br>                "SELECT meta_value FROM $this->tblRedirectMeta<br><br>                WHERE redirect_id = %d AND meta_key = %s LIMIT 1;",<br><br>                $id, $metaKey<br><br>            );<br><br>            $result = maybe_unserialize($wpdb->get_var($sql));<br><br>        } else {<br><br>            $sql = $wpdb->prepare(<br><br>                "SELECT meta_key, meta_value FROM $this->tblRedirectMeta<br><br>                WHERE redirect_id = %d;",<br><br>                $id<br><br>            );<br><br>            $metadata = $wpdb->get_results($sql, ARRAY_A);<br><br>            $result = &#091;];<br><br>            if ($metadata && is_array($metadata)) {<br><br>                foreach ($metadata as $data) {<br><br>                    $key = trim($data&#091;"meta_key"]);<br><br>                    $value = maybe_unserialize($data&#091;"meta_value"]);<br><br>                    $result&#091;$key] = $value;<br><br>                }<br><br>            }<br><br>        }<br><br>        // Cache the result<br><br>        $this->cache&#091;'meta']&#091;$cache_key] = $result;<br><br>        return $result;<br><br>    }<br><br>}<br><br>

    ### Option 2: WordPress Transients (More Robust)

    For persistent caching across requests:

    php<br><br>/**<br><br> * Get rules with WordPress Transients<br><br> */<br><br>public function getRules($status = 1) {<br><br>    $transient_key = 'irrp_rules_' . $status;<br><br>    $rules = get_transient($transient_key);<br><br>    if ($rules === false) {<br><br>        // Execute original query<br><br>        global $wpdb;<br><br>        $sql = "SELECT * FROM {$this->tblRedirects} WHERE type = '" . self::TYPE_REDIRECTION_RULE . "'";<br><br>        if (trim($status) !== "") {<br><br>            $sql .= " AND status = " . (int) $status;<br><br>        }<br><br>        $sql .= " ORDER BY timestamp DESC, id DESC;";<br><br>        $rules = $wpdb->get_results($sql, ARRAY_A);<br><br>        // Cache for 1 hour<br><br>        set_transient($transient_key, $rules, HOUR_IN_SECONDS);<br><br>    }<br><br>    return $rules;<br><br>}<br><br>/**<br><br> * Clear cache when adding/editing redirects<br><br> */<br><br>public function add($data) {<br><br>    global $wpdb;<br><br>    $wpdb->insert($this->tblRedirects, $data, &#091;"%s", "%s", "%s", "%d", "%d", "%s"]);<br><br>    // Clear cache<br><br>    delete_transient('irrp_rules_1');<br><br>    delete_transient('irrp_rules_0');<br><br>    return (int) $wpdb->insert_id;<br><br>}<br><br>public function edit($id, $data, $dataFormat) {<br><br>    global $wpdb;<br><br>    $result = $wpdb->update($this->tblRedirects, $data, &#091;"id" => $id], $dataFormat, &#091;"%d"]);<br><br>    // Clear cache<br><br>    delete_transient('irrp_rules_1');<br><br>    delete_transient('irrp_rules_0');<br><br>    return $result !== false;<br><br>}<br><br>

    ### Option 3: Optimize with SQL JOIN (Best Performance)

    Replace N+1 queries with a single JOIN query:

    php<br><br>public function getRulesWithMeta($status = 1) {<br><br>    global $wpdb;<br><br>    $sql = "<br><br>        SELECT<br><br>            r.*,<br><br>            GROUP_CONCAT(<br><br>                CONCAT(rm.meta_key, ':', rm.meta_value)<br><br>                SEPARATOR '||'<br><br>            ) as all_meta<br><br>        FROM {$this->tblRedirects} r<br><br>        LEFT JOIN {$this->tblRedirectMeta} rm ON r.id = rm.redirect_id<br><br>        WHERE r.type = '" . self::TYPE_REDIRECTION_RULE . "'<br><br>    ";<br><br>    if (trim($status) !== "") {<br><br>        $sql .= " AND r.status = " . (int) $status;<br><br>    }<br><br>    $sql .= " GROUP BY r.id ORDER BY r.timestamp DESC, r.id DESC";<br><br>    $results = $wpdb->get_results($sql, ARRAY_A);<br><br>    // Parse metadata<br><br>    foreach ($results as &$result) {<br><br>        if (!empty($result&#091;'all_meta'])) {<br><br>            $meta_pairs = explode('||', $result&#091;'all_meta']);<br><br>            $result&#091;'meta'] = &#091;];<br><br>            foreach ($meta_pairs as $pair) {<br><br>                list($key, $value) = explode(':', $pair, 2);<br><br>                $result&#091;'meta']&#091;$key] = maybe_unserialize($value);<br><br>            }<br><br>        }<br><br>        unset($result&#091;'all_meta']);<br><br>    }<br><br>    return $results;<br><br>}<br><br>

    ## Performance Impact

    ### Before Fix:

    - **81 SQL queries** per page load

    - ~0.024-0.057 seconds wasted on duplicate queries

    - Scales poorly with more redirect rules

    ### After Fix (with caching):

    - **6-8 SQL queries** per page load

    - **80-90% reduction** in database queries

    - Consistent performance regardless of redirect count

    ## Testing Results

    With 25 redirect rules:

    - **Before:** 81 queries (4 getRules + 75 getMeta + 2 getMatched)

    - **After:** 7 queries (1 getRules + 5 getMeta + 1 getMatched)

    - **Improvement:** 91% reduction

    ## Questions for Plugin Developers

    1. Are there plans to implement query caching in future versions?

    2. Would you accept a pull request with these optimizations?

    3. Is there a recommended way to implement this without modifying core plugin files?

    ## Temporary Workaround

    Until this is fixed in the plugin, you can create a custom plugin to override these methods:

    php<br><br><?php<br><br>/**<br><br> * Plugin Name: Redirect Redirection Performance Fix<br><br> * Description: Adds caching to Redirect Redirection plugin<br><br> * Version: 1.0<br><br> */<br><br>add_action('plugins_loaded', function() {<br><br>    if (class_exists('IRRPDBManager')) {<br><br>        // Override with cached version<br><br>        // (Implementation details above)<br><br>    }<br><br>}, 999);<br><br>
Viewing 1 replies (of 1 total)
  • Plugin Support MixHa

    (@mixha)

    Hello @carlos978 ,

    Thank you for your detailed report and for taking the time to analyze the performance issues with the Redirect Redirection plugin. We really appreciate your thorough investigation and suggested solutions.

    I’m happy to inform you that we’ve implemented the necessary optimizations:

    irrpInit() now only runs on the plugin dashboard instead of on every request, reducing unnecessary queries.
    Caching has been added to getRules() and getMeta() methods, which dramatically cuts down duplicate SQL queries.

    With these changes, the N+1 query problem is resolved, and the plugin now performs efficiently even with a large number of redirect rules. These improvements will be included in the next plugin release.

    To address your questions:

    1. Plans for query caching: Yes — caching is now part of our upcoming release, ensuring improved performance across all installations.
    2. Pull requests with optimizations: As a security policy, we do not accept pull requests, so external contributions cannot be merged into the plugin at this time. However, you are always welcome to request features or suggest improvements, and we carefully consider these for future releases.
    3. Implementing without modifying core files: You can temporarily override the methods via a small custom plugin as you suggested. However, with the next release, these optimizations will be included in the core plugin, so no temporary override will be necessary.

    We sincerely appreciate your feedback — Please feel free to test the upcoming release once it’s available, and let us know if you encounter any issues.

    Thanks again for your support and detailed insights!

    Kind regards

Viewing 1 replies (of 1 total)

You must be logged in to reply to this topic.