What Is Autoload in WordPress? How a Bloated wp_options Table Weighs Down Every Request

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

除了文章和页面之外,每个 WordPress 数据库都有一个名为 wp_options 的表。它存储了全站范围的设置:站点标题、固定链接结构、已启用插件列表以及每个插件的配置,每个设置占一行。该表有一个名为 autoload 的列。它看起来像是一个简单的标志,但任何启用了 autoload 的行都会在每次请求时被加载,无论该请求是否需要它。当一个长期运行的站点的管理界面逐渐变慢时,臃肿的自动加载集是首先值得检查的内容之一。继上一篇关于数据库索引基础的文章之后,本文使用本博客 WordPress 数据库(7.1 系列)的测量数据,仅使用只读查询,来解释自动加载的工作原理以及当它变得过大时会发生什么。

Besides posts and pages, every WordPress database has a table called wp_options. It stores site-wide settings: the site title, the permalink structure, the list of active plugins, and the configuration of each plugin, one row per setting. That table has a column named autoload. It looks like a simple flag, but any row with autoload enabled is loaded on every single request, whether that request needs it or not. When a long-running site’s admin screens gradually get heavier, a bloated autoload set is one of the first things worth checking. Following the previous article on database index basics, this post uses measurements taken on this blog’s own WordPress database (7.1 series), using read-only queries only, to explain how autoload works and what happens when it grows too large.

wp_options:全站设置的家园。wp_options 是一个简单的键值表:option_name、option_value 和 autoload 列。在本博客中,它包含 215 行。核心程序、主题和插件都通过 get_option() 和 update_option() 对其进行读写,核心设置与插件设置并存。

wp_options: the home of site-wide settings. wp_options is a simple key-value table: option_name, option_value, and an autoload column. On this blog it holds 215 rows. Core, themes and plugins all read and write it through get_option() and update_option(), and core settings live side by side with plugin settings.

自动加载意味着“预先全部读取”。在每次请求的早期,WordPress 会调用 wp_load_alloptions(),它会在单个查询中获取每一行自动加载的数据,并将结果保存在内存中。此后,任何针对已自动加载选项的 get_option() 调用都会直接从内存中响应,而无需触及数据库。禁用了自动加载的选项则仅在实际需要时才会被单独获取。

Autoload means “read it all up front”. Early in each request, WordPress calls wp_load_alloptions(), which fetches every autoloaded row in a single query and keeps the result in memory. After that, any get_option() call for an autoloaded option is answered from memory without touching the database. Options with autoload disabled are fetched individually, only when something actually asks for them.

自动加载状态读取时间查询方式对未使用请求的开销
启用每次请求早期,一次性读取所有选项共用一个查询无论如何都会加载
禁用仅在请求时每个选项一个查询无
Autoload statusWhen it is readQueriesCost on requests that do not use it
EnabledEarly in every request, all at onceOne query for all of themLoaded anyway
DisabledOnly when requestedOne query per option usedNone

这种设计对于几乎每个页面都需要的值来说是有意义的。问题在于最后一行:一个仅被某个冷门管理界面使用的自动加载设置,或者被你多年前删除的插件遗留下来的设置,仍然会在每次页面加载时被读取。

That design makes sense for values nearly every page needs. The catch is the last row: an autoloaded setting used by one obscure admin screen, or left behind by a plugin you deleted years ago, is still read on every page load.

测量本博客的自动加载集。按 autoload 值对本博客的 wp_options 进行分组,结果如下:SELECT autoload, COUNT(*) AS cnt, SUM(LENGTH(option_value)) AS bytes FROM wp_options GROUP BY autoload;

Measuring this blog’s autoload set. Grouping this blog’s wp_options by the autoload value gives the following: SELECT autoload, COUNT(*) AS cnt, SUM(LENGTH(option_value)) AS bytes FROM wp_options GROUP BY autoload;

自动加载值行数总字节数是否预先加载?
on10138,669是
auto8410,063是
yes340是
off246,022否
no33否
Autoload valueRowsTotal value bytesLoaded up front?
on10138,669Yes
auto8410,063Yes
yes340Yes
off246,022No
no33No

215 行中有 188 行(约 48 KB 的值)是预先加载的。通过 wp eval 计算 wp_load_alloptions() 的结果,得到的也是 188 行。对于像本博客这样的小型站点来说,这无需担心。

188 of 215 rows, about 48 KB of values, are loaded up front. Counting the result of wp_load_alloptions() via wp eval gives the same 188. For a small blog like this one, that is nothing to worry about.

为什么值比“yes”和“no”多?WordPress 6.6 重构了自动加载机制。除了传统的 yes/no(仍然有效)之外,你现在还会看到 on/off(明确设置)、auto(留给 WordPress 决定)以及 auto-on/auto-off(自动决定)。哪些值算作自动加载由 wp_autoload_values_to_autoload() 定义;在本博客中,它返回 yes、on、auto-on 和 auto。自 6.6 版本起,如果一个选项在保存时没有明确的自动加载选择,且其序列化大小超过 150,000 字节(wp_max_autoloaded_option_size 过滤器的默认值),WordPress 将不会对其进行预加载。但这在插件明确要求自动加载时没有帮助,因此它本身无法防止臃肿。

Why there are more values than yes and no. WordPress 6.6 reworked autoload. Besides the legacy yes/no (still honored), you will now see on/off (explicitly set), auto (left for WordPress to decide), and auto-on/auto-off (decided automatically). Which values count as autoloaded is defined by wp_autoload_values_to_autoload(); on this blog it returns yes, on, auto-on and auto. Since 6.6, if an option is saved without an explicit autoload choice and its serialized size exceeds 150,000 bytes (the default of the wp_max_autoloaded_option_size filter), WordPress keeps it out of the up-front load. This does not help when a plugin explicitly asks for autoload, so it cannot prevent bloat on its own.

最大的行展示了为什么它们每次都需要被加载。核心程序最大的条目都有明确的理由在每次请求时加载:路由、权限检查和 WP-Cron 到期事件检查。最顶部的条目是一个瞬态数据(transient),存储在同一个表中。在核心的 set_transient() 中,带有过期时间的瞬态数据在保存时会禁用自动加载,而没有过期时间的则会被自动加载。因此,任何保存大型且无过期时间瞬态数据的代码都会不断增加预加载的负担。

The largest rows show why they are needed every time. Core’s largest entries each have a clear reason to be loaded on every request: routing, permission checks, and the WP-Cron due-event check. The top entry is a transient, stored in the same table. In core’s set_transient(), a transient saved with an expiration is stored with autoload disabled, while one saved without an expiration is autoloaded. So any code that saves large transients without an expiration keeps adding to the up-front load.

预加载查询是全表扫描。对自动加载查询运行 EXPLAIN:EXPLAIN SELECT option_name, option_value FROM wp_options WHERE autoload IN ('yes', 'on', 'auto-on', 'auto'); 尽管 autoload 上存在索引,但优化器仍选择了全表扫描,因为 215 行中有 188 行匹配。这是上一篇文章中提到的“大多数行匹配,因此扫描更便宜”的情况。结论是:自动加载的成本在于读取了多少数据,而不是如何找到它。没有任何索引能解决这个问题;只有减少自动加载的数据量才能解决。

The up-front query is a full table scan. Running EXPLAIN on the autoload query: EXPLAIN SELECT option_name, option_value FROM wp_options WHERE autoload IN (‘yes’, ‘on’, ‘auto-on’, ‘auto’); An index on autoload exists, yet the optimizer chooses a full scan, because 188 of 215 rows match. This is the “most rows match, so scanning is cheaper” case from the previous article. The takeaway: autoload cost is about how much is read, not how it is found. No index fixes it; only reducing the autoloaded data does.

当自动加载臃肿时,什么会变慢?所有未缓存的请求。页面缓存会为匿名访问者绕过 PHP,但管理界面、已登录浏览、REST API 调用、WP-Cron 运行以及缓存过期后的第一次点击都会付出代价。这就是为什么“只有管理员感到慢”通常指向这里的原因。PHP 内存。加载的值在整个请求期间保留在内存中,序列化数组在反序列化时会膨胀,使请求更接近 WP_MEMORY_LIMIT。持久对象缓存。使用 Redis 或 Memcached 时,自动加载集被存储为一个大的 alloptions 项,在每次请求时被获取并反序列化。一些缓存还会限制单个项的大小;如果集合超过该限制,它就无法被缓存,每次都会回退到数据库。

What gets heavier when autoload bloats. Every uncached request. Page caching bypasses PHP for anonymous visitors, but admin screens, logged-in browsing, REST API calls, WP-Cron runs, and the first hit after a cache expires all pay the cost. That is why “only the admin feels heavy” often points here. PHP memory. The loaded values stay in memory for the whole request, and serialized arrays expand when unserialized, pushing requests closer to WP_MEMORY_LIMIT. Persistent object caches. With Redis or Memcached, the autoload set is stored as one large alloptions item that is fetched and unserialized on each request. Some caches also cap the size of a single item; if the set outgrows that cap, it cannot be cached and falls back to the database every time.

常见原因:已删除插件遗留的设置且没有卸载清理;保存时没有过期时间的瞬态数据;插件不断向单个选项追加日志或统计信息;主要用于单个屏幕的大型设置数组,却被存储为单个自动加载选项。这些在某一天都不会破坏任何东西,但它们会在多年的运行中积累。

Common causes: Settings left behind by deleted plugins that have no uninstall cleanup; Transients saved without an expiration; Plugins that keep appending logs or statistics to a single option; Large settings arrays, mostly used on one screen, stored as one autoloaded option. None of these break anything on a given day. They accumulate over years of operation.

站点健康阈值。工具 → 站点健康会检查总的自动加载大小。在核心程序中,当超过 800,000 字节时会触发警告。

The Site Health threshold. Tools → Site Health checks the total autoload size. In core, the warning fires above 800,000 bytes.