Prerequisites
Please give us a description of what happened
On a WordPress installation with:
- ~25,000 rows in wp_posts
- ~33,000 rows in wp_yoast_seo_links
the query in Post_Link_Indexing_Action->get_count_query() takes more than 60 seconds to complete.
Given the moderate dataset size, this appears to be caused by the query structure rather than dataset scale.
The problematic pattern is:
- LEFT JOIN on wp_yoast_indexable
- LEFT JOIN on wp_yoast_seo_links
- WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
This LEFT JOIN + OR combination prevents efficient index usage and leads to inefficient execution plans.
Step-by-step reproduction instructions
- Use a WordPress installation with > 20k posts
- Ensure Yoast SEO indexables and internal links are generated
- Trigger internal link indexing
- Observe execution time of
Post_Link_Indexing_Action->get_count_query()
- Query execution takes a very long time (> 60s on our installation)
Expected results
Query should execute in milliseconds to low seconds.
Actual results
- Execution time > 60 seconds
- High DB load
Screenshots, screen recording, code snippet
Rewrite using EXISTS / NOT EXISTS instead of LEFT JOIN + OR, for example:
SELECT COUNT(*)
FROM wp_posts AS P
WHERE P.post_status = 'publish'
AND P.post_type IN (...)
AND (
NOT EXISTS (
SELECT 1
FROM wp_yoast_indexable AS I
WHERE I.object_id = P.ID
AND I.object_type = 'post'
AND I.link_count IS NOT NULL
)
OR EXISTS (
SELECT 1
FROM wp_yoast_seo_links AS L
WHERE L.post_id = P.ID
AND L.type = 'internal'
AND L.target_indexable_id IS NULL
AND L.target_post_id IS NOT NULL
AND L.target_post_id != 0
)
);
Updated queries in class Post_Link_Indexing_Action:
protected function get_count_query() {
$public_post_types = $this->post_type_helper->get_indexable_post_types();
$indexable_table = Model::get_table_name( 'Indexable' );
$links_table = Model::get_table_name( 'SEO_Links' );
// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
return $this->wpdb->prepare(
"SELECT COUNT(*)
FROM {$this->wpdb->posts} AS P
WHERE P.post_status = 'publish'
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ")
AND (
NOT EXISTS (
SELECT 1
FROM $indexable_table AS I
WHERE I.object_id = P.ID
AND I.object_type = 'post'
AND I.link_count IS NOT NULL
)
OR EXISTS (
SELECT 1
FROM $links_table AS L
WHERE L.post_id = P.ID
AND L.type = 'internal'
AND L.target_indexable_id IS NULL
AND L.target_post_id IS NOT NULL
AND L.target_post_id != 0
)
)",
$public_post_types
);
}
protected function get_select_query( $limit = false ) {
$public_post_types = $this->post_type_helper->get_indexable_post_types();
$indexable_table = Model::get_table_name( 'Indexable' );
$links_table = Model::get_table_name( 'SEO_Links' );
$replacements = $public_post_types;
$limit_query = '';
if ( $limit !== false ) {
$limit_query = 'LIMIT %d';
$replacements[] = (int) $limit;
}
// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
return $this->wpdb->prepare(
"SELECT P.ID, P.post_content
FROM {$this->wpdb->posts} AS P
WHERE P.post_status = 'publish'
AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ")
AND (
NOT EXISTS (
SELECT 1
FROM $indexable_table AS I
WHERE I.object_id = P.ID
AND I.link_count IS NOT NULL
AND I.object_type = 'post'
)
OR EXISTS (
SELECT 1
FROM $links_table AS L
WHERE L.post_id = P.ID
AND L.target_indexable_id IS NULL
AND L.type = 'internal'
AND L.target_post_id IS NOT NULL
AND L.target_post_id != 0
)
)
$limit_query",
$replacements
);
}
Please implement those changes - query time is reduced from > 60 seconds to ~0.2 seconds.
Which editor is affected (or editors)
Which browser is affected (or browsers)
Device you are using
No response
Operating system
No response
PHP version
8.2
WordPress version
6.8.3
WordPress Theme
No response
Yoast SEO version
26.9
Gutenberg plugin version (if relevant)
No response
Elementor plugin version (if relevant)
No response
Classic Editor plugin version (if relevant)
No response
Relevant plugins in case of a bug
No response
Prerequisites
Please give us a description of what happened
On a WordPress installation with:
the query in
Post_Link_Indexing_Action->get_count_query()takes more than 60 seconds to complete.Given the moderate dataset size, this appears to be caused by the query structure rather than dataset scale.
The problematic pattern is:
This LEFT JOIN + OR combination prevents efficient index usage and leads to inefficient execution plans.
Step-by-step reproduction instructions
Post_Link_Indexing_Action->get_count_query()Expected results
Query should execute in milliseconds to low seconds.
Actual results
Screenshots, screen recording, code snippet
Rewrite using EXISTS / NOT EXISTS instead of LEFT JOIN + OR, for example:
Updated queries in class
Post_Link_Indexing_Action:Please implement those changes - query time is reduced from > 60 seconds to ~0.2 seconds.
Which editor is affected (or editors)
Which browser is affected (or browsers)
Device you are using
No response
Operating system
No response
PHP version
8.2
WordPress version
6.8.3
WordPress Theme
No response
Yoast SEO version
26.9
Gutenberg plugin version (if relevant)
No response
Elementor plugin version (if relevant)
No response
Classic Editor plugin version (if relevant)
No response
Relevant plugins in case of a bug
No response