2026年3月2日星期一

每个 PXC 节点都需要安装 XtraBackup 吗?

Percona 论坛中经常出现的一个问题:Percona XtraDB Cluster (PXC) 中的每个节点都需要安装 XtraBackup 吗? 这是一个合理的问题,尤其是在管理混合环境或试图最小化某些节点上的软件占用时。以下是实际机制和测试所确认的内容。

简短答案(但请继续阅读)

这取决于你希望该节点做什么。 这里的细微差别非常重要,因此值得详细说明 PXC 中 State Snapshot Transfer (SST) 的工作原理,以及 XtraBackup 在给定节点上的存在——或缺失——为什么重要。

PXC 中 SST 的快速复习

当一个新节点加入 Percona XtraDB Cluster,或者一个现有节点宕机时间过长以至于 Incremental State Transfer (IST) 不再可能时,集群会执行 State Snapshot Transfer (SST)。这本质上是捐献节点向加入节点的全量数据复制。

PXC 支持多种 SST 方法,在 my.cnf 中配置:

[mysqld]
wsrep_sst_method = xtrabackup-v2

可用的 SST 方法包括:

  • xtrabackup-v2 — PXC 的推荐方法,使用 Percona XtraBackup;执行 SST 时不会长时间锁定捐献节点
  • clone — PXC 8.0.22+ 中可用,使用 MySQL 内置的 Clone Plugin;消除了 SST 对 XtraBackup 的依赖
  • mysqldump — 较慢且在传输期间锁定捐献节点;不推荐用于生产环境
  • rsync — 要求捐献节点在传输期间为只读,阻塞写入;也不推荐用于活跃集群

xtrabackup-v2 方法历来是默认方法,并在现有部署中广泛使用,正是因为它在传输期间保持捐献节点可用于写入。其他旧方法可能会阻塞捐献节点的写入,这在生产集群中通常是不可接受的。请注意,Percona 越来越推荐在 PXC 8.0.22 及更高版本的新安装中使用 clone 方法,因为它消除了 SST 层对外部工具的依赖。

XtraBackup 需要安装在哪里?

当触发使用 xtrabackup-v2 的 SST 时,捐献节点和加入节点都需要安装 XtraBackup 并可访问。以下是为什么两侧都需要的原因:

``````html
  • 捐赠者 运行 XtraBackup 来流式传输快照数据到外部
  • 加入者 运行 XtraBackup — 具体来说是 xbstreamxbcrypt 工具 — 来接收并应用这些流式传输的数据

如果加入者节点没有安装 XtraBackup,并且您尝试使用 xtrabackup-v2 将其加入集群,SST 将失败。加入者上的错误日志通常会显示类似以下内容:

[ERROR] WSREP: Failed to read 'ready <addr>' from: wsrep_sst_xtrabackup-v2
...
wsrep_sst_xtrabackup-v2: line 522: xbstream: command not found
[ERROR] WSREP: SST failed: 2 (No such file or directory)

这是一个明确且无歧义的失败模式。如果 xbstream 在加入者上不存在,SST 将无法完成。

那些永远不会成为加入者的节点呢?

从技术上讲,如果一个节点将始终充当捐赠者并且永远不需要从头重新加入集群,您可以说它只需要在捐赠者角色下安装 XtraBackup。然而,在实际操作中,任何节点都可能成为加入者 — 在崩溃后、计划维护后,或从网络分区恢复后。没有可靠的方法可以保证一个节点永远不需要接收 SST。

这里的实用指导很简单:在每个 PXC 节点上无一例外地安装 XtraBackup。 安装它的开销微乎其微。而在计划外中断期间 SST 失败的成本则不然。

Clone 插件替代方案(PXC 8.0.22+)

从 PXC 8.0.22 开始,Percona 添加了对 MySQL Clone Plugin 作为 SST 方法的支持。值得了解这一点,因为它完全消除了 SST 对 XtraBackup 的依赖:

[mysqld]
wsrep_sst_method = clone

使用 clone 方法时,所有节点必须加载 Clone 插件:

INSTALL PLUGIN clone SONAME 'mysql_clone.so';
SHOW PLUGINS WHERE Name = 'clone';
+-------+--------+-------+----------------+---------+
| Name  | Status | Type  | Library        | License |
+-------+--------+-------+----------------+---------+
| clone | ACTIVE | CLONE | mysql_clone.so | GPL     |
+-------+--------+-------+----------------+---------+

clone 方法是一个很好的标准化选项,无需将 XtraBackup 作为 SST 依赖。不过,无论您选择哪种 SST 方法,XtraBackup 对于您的 外部备份策略 仍然具有真正价值。SST 是一种集群同步机制 — 它不是备份,也不应被当作备份对待。

检查当前的 SST 配置

您可以使用以下命令验证当前的 SST 方法和 Galera 相关设置:

SHOW VARIABLES LIKE 'wsrep_sst_method';
+------------------+---------------+
| Variable_name    | Value         |
+------------------+---------------+
| wsrep_sst_method | xtrabackup-v2 |
+------------------+---------------+

检查集群状态并确认哪个节点可能充当捐赠者:

SHOW STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name             | Value  |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+
SHOW STATUS LIKE 'wsrep_connected';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| wsrep_connected | ON    |
+-----------------+-------+
SHOW STATUS LIKE 'wsrep_cluster_size';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+

实际观察

直接处理 PXC 环境时值得注意的几点:

``````html
  • XtraBackup 的版本必须与您的 PXC 版本匹配。使用 XtraBackup 2.x 与 PXC 8.0 会导致 SST 失败。请使用 Percona XtraBackup 8.0 与 PXC 8.0,并在任何升级后确认版本对齐。
  • 即使您切换到 clone SST 方法,也要保留安装 XtraBackup,用于计划备份。您的备份策略与 SST 方法是独立的关注点,应分别处理。
  • wsrep_sst_donor 变量允许您指定首选捐赠节点,这对于将 SST 引导远离最繁忙或延迟最敏感的成员非常有用。
  • 如果您在 PXC 旁边运行 Percona Toolkit,请注意您特定 PXC 版本中 DDL replication 的工作方式 — Total Order Isolation (TOI) 与 Rolling Schema Upgrade (RSU) 的行为不同,在生产环境中运行架构更改之前值得专门查看。

总结

直接回答问题:如果您使用 xtrabackup-v2 作为 SST 方法 — 这在许多现有 PXC 部署中仍是默认方法 — 那么是的,每个集群成员都需要安装 XtraBackup。 任何节点都可能根据情况成为捐赠节点或加入节点,使用此方法时两种角色都需要 XtraBackup。

如果您使用 PXC 8.0.22 或更高版本,并希望在 SST 层消除该依赖,Clone Plugin 方法是一个可行的替代方案,并且是 Percona 越来越推荐的新部署选择。如果您是从头开始,PXC 8.4 LTS 是当前长期支持版本,也是新安装的推荐目标。即使使用 clone 进行 SST,XtraBackup 仍是实际备份任务的正确工具。

不要为了节省选定节点上的几兆字节磁盘空间而跳过 XtraBackup。此决定最终导致的 SST 失败不是值得的权衡。

资源

2026年2月22日星期日

MySQL + Neo4j 用于 AI 工作负载:为什么关系型数据库仍然重要

所以我想是时候记录一下如何使用你已经熟悉的数据库为 AI 代理构建持久内存了。不是向量数据库 - 而是 MySQL 和 Neo4j。

这不是理论。我每天都在使用这种架构,在多个项目中处理 AI 代理内存。这是真正有效的 schema 和查询模式。

架构

AI 代理需要两种类型的内存:

  • 结构化内存 - 发生了什么、何时、为什么 (MySQL)
  • 模式内存 - 什么与什么相连 (Neo4j)

向量数据库适用于相似性搜索。它们不适合跟踪工作流状态或决策历史。为此,你需要 ACID 事务和正确的关系。

MySQL Schema

这是 AI 代理持久内存的实际 schema:

-- Architecture decisions the AI made
CREATE TABLE architecture_decisions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    decision TEXT NOT NULL,
    rationale TEXT,
    alternatives_considered TEXT,
    status ENUM('accepted', 'rejected', 'pending') DEFAULT 'accepted',
    decided_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    tags JSON,
    INDEX idx_project_date (project_id, decided_at),
    INDEX idx_status (status)
) ENGINE=InnoDB;

-- Code patterns the AI learned
CREATE TABLE code_patterns (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    code_example TEXT,
    language VARCHAR(50),
    confidence_score FLOAT DEFAULT 0.5,
    usage_count INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_project_category (project_id, category),
    INDEX idx_confidence (confidence_score)
) ENGINE=InnoDB;

-- Work session tracking
CREATE TABLE work_sessions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(255) UNIQUE NOT NULL,
    project_id INT NOT NULL,
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    ended_at DATETIME,
    summary TEXT,
    context JSON,
    INDEX idx_project_session (project_id, started_at)
) ENGINE=InnoDB;

-- Pitfalls to avoid (learned from mistakes)
CREATE TABLE pitfalls (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    how_to_avoid TEXT,
    severity ENUM('critical', 'high', 'medium', 'low'),
    encountered_count INT DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_project_severity (project_id, severity)
) ENGINE=InnoDB;

外键。检查约束。正确的索引。这是关系型数据库擅长的领域。

查询模式

这是实际为 AI 代理内存查询的方式:

-- Get recent decisions for context
SELECT title, decision, rationale, decided_at
FROM architecture_decisions
WHERE project_id = ?
  AND decided_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY decided_at DESC
LIMIT 10;

-- Find high-confidence patterns
SELECT category, name, description, code_example
FROM code_patterns
WHERE project_id = ?
  AND confidence_score >= 0.80
ORDER BY usage_count DESC, confidence_score DESC
LIMIT 20;

-- Check for known pitfalls before implementing
SELECT title, description, how_to_avoid
FROM pitfalls
WHERE project_id = ?
  AND category = ?
  AND severity IN ('critical', 'high')
ORDER BY encountered_count DESC;

-- Track session context across interactions
SELECT context
FROM work_sessions
WHERE session_id = ?
ORDER BY started_at DESC
LIMIT 1;

这些是直截了当的 SQL 查询。EXPLAIN 显示索引使用完全符合预期。没有惊喜。

Neo4j 层

MySQL 处理结构化数据。Neo4j 处理关系:

// Create nodes for decisions
CREATE (d:Decision {
  id: 'dec_123',
  title: 'Use FastAPI',
  project_id: 1,
  embedding: [0.23, -0.45, ...]  // Vector for similarity
})

// Create relationships
CREATE (d1:Decision {id: 'dec_123', title: 'Use FastAPI'})
CREATE (d2:Decision {id: 'dec_45', title: 'Used Flask before'})
CREATE (d1)-[:SIMILAR_TO {score: 0.85}]->(d2)
CREATE (d1)-[:CONTRADICTS]->(d3:Decision {title: 'Avoid frameworks'})

// Query: Find similar past decisions
MATCH (current:Decision {id: $decision_id})
MATCH (current)-[r:SIMILAR_TO]-(similar:Decision)
WHERE r.score > 0.80
RETURN similar.title, r.score
ORDER BY r.score DESC

// Query: What outcomes followed this pattern?
MATCH (d:Decision)-[:LEADS_TO]->(o:Outcome)
WHERE d.title CONTAINS 'Redis'
RETURN d.title, o.type, o.success_rate

它们如何协同工作

流程如下所示:

  1. AI 代理生成内容或做出决策
  2. 将结构化数据存储到 MySQL(什么、何时、为什么、完整上下文)
  3. 生成嵌入,向量存储到 Neo4j,并与相似项目建立关系
  4. 下次会话:Neo4j 找到相关的相似决策
  5. MySQL 提供这些决策的完整细节

MySQL 是真相之源。Neo4j 是模式查找器。

为什么不只用向量数据库?

我见过团队试图仅用 Pinecone 或 Weaviate 构建 AI 代理内存。但效果不佳,因为:

向量数据库擅长:

  • 找到与查询相似的文档
  • 语义搜索 (RAG)
  • “类似这样的东西”

向量数据库不擅长:

  • “我们 3 月 15 日决定了什么?”
  • “显示导致中断的决策”
  • “这个工作流的当前状态是什么?”
  • “哪些模式置信度 > 0.8 且使用次数 > 10?”

这些查询需要结构化过滤、连接和事务。这是关系型数据库的领域。

MCP 和未来

模型上下文协议 (MCP) 正在标准化 AI 系统处理上下文的方式。早期的 MCP 实现正在发现我们早已知道的事情:你需要结构化存储和图关系两者兼备。

``````html

MySQL 处理 MCP 的“resources”和“tools”目录。Neo4j 处理上下文项之间的“relationships”。Vector embeddings 只是拼图中的一块。

生产环境说明

当前运行此架构的系统:

  • MySQL 8.0, 48 tables, ~2GB data
  • Neo4j Community, ~50k nodes, ~200k relationships
  • Query latency: MySQL <10ms, Neo4j <50ms
  • Backup: Standard mysqldump + neo4j-admin dump
  • Monitoring: Same Percona tools I've used for years

运营复杂度低,因为这些是成熟的数据库,具有良好理解的运营模式。

何时使用什么

Use CaseDatabase
Workflow state, decisions, audit trailMySQL/PostgreSQL
Pattern detection, similarity, relationshipsNeo4j
Semantic document search (RAG)Vector DB (optional)

从 MySQL 开始用于状态管理。当需要模式识别时添加 Neo4j。只有在实际进行语义文档检索时才添加 vector DB。

总结

AI 代理需要持久化内存。不仅仅是 vector database 中的 embeddings - 结构化、关系型、时序内存,带有模式识别。

MySQL 处理结构化状态。Neo4j 处理图关系。它们共同提供 vector databases 单独无法提供的功能。

不要为了 AI 工作负载而放弃关系型数据库。为每个任务使用正确的工具,那就是将两者结合使用。

有关此架构的 AI 代理视角,参见 3k1o 的配套文章。

MySQL 8.0 JSON 函数:实用示例与索引

This article was originally published in English at AnotherMySQLDBA.

本文详细介绍了 MySQL 8.0 的 JSON 函数的实际操作演练。JSON 支持从 MySQL 5.7 开始引入,但 8.0 增加了一组重要的改进——更好的索引策略、新函数以及多值索引——这些使得处理 JSON 数据变得更加实用。以下文档介绍了几个最常用的模式,包括 EXPLAIN 输出和值得了解的性能观察。

这不是一篇“JSON 与关系型”的辩论文章。如果你选择在 MySQL 中存储 JSON,你可能已经有自己的理由。此处的目标是确保你有效利用现有的工具。

环境

mysql> SELECT @@version, @@version_comment\G
*************************** 1. row ***************************
        @@version: 8.0.36
@@version_comment: MySQL Community Server - GPL

测试在一台具有 8GB RAM 的虚拟机上进行,并将 innodb_buffer_pool_size 设置为 4G。值得一提的一个维护注意事项:query_cache_type 在 8.0 中无关紧要,因为查询缓存已被完全移除。如果你从 5.7 实例迁移过来,并且在 my.cnf 中仍有该变量,请删除它——MySQL 8.0 会抛出启动错误。

设置测试表

测试表模拟了一个相当常见的模式——应用程序将用户配置文件数据和事件元数据存储为 JSON 块:

CREATE TABLE user_events (
  id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id     INT UNSIGNED NOT NULL,
  event_data  JSON NOT NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  INDEX idx_user (user_id)
) ENGINE=InnoDB;

INSERT INTO user_events (user_id, event_data) VALUES
(1, '{"action":"login","ip":"192.168.1.10","tags":["mobile","vpn"],"score":88}'),
(1, '{"action":"purchase","ip":"192.168.1.10","tags":["desktop"],"score":72,"amount":49.99}'),
(2, '{"action":"login","ip":"10.0.0.5","tags":["mobile"],"score":91}'),
(3, '{"action":"logout","ip":"10.0.0.9","tags":["desktop","vpn"],"score":65}'),
(2, '{"action":"purchase","ip":"10.0.0.5","tags":["mobile"],"score":84,"amount":129.00}');

基本提取:JSON_VALUE 与 JSON_EXTRACT

JSON_VALUE() 在 MySQL 8.0.21 中引入,是提取标量值并带有内置类型转换的更简洁方式。在此之前,你使用 JSON_EXTRACT()(或 -> 简写)并手动转换类型,虽然有效但会增加查询的噪音。

-- Pre-8.0.21 approach
SELECT user_id,
       JSON_EXTRACT(event_data, '$.action') AS action,
       CAST(JSON_EXTRACT(event_data, '$.score') AS UNSIGNED) AS score
FROM user_events;

-- Cleaner 8.0.21+ approach
SELECT user_id,
       JSON_VALUE(event_data, '$.action') AS action,
       JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) AS score
FROM user_events;

第二个查询的输出:

+---------+----------+-------+
| user_id | action   | score |
+---------+----------+-------+
|       1 | login    |    88 |
|       1 | purchase |    72 |
|       2 | login    |    91 |
|       3 | logout   |    65 |
|       2 | purchase |    84 |
+---------+----------+-------+
5 rows in set (0.00 sec)

RETURNING 子句确实非常有用。它消除了尴尬的双重转换模式,并在后续阅读查询代码时使意图更清晰。

多值索引:真正的变革者

这才是 8.0 真正提升 JSON 工作负载的地方。从 MySQL 8.0.17 开始支持的多值索引,让你可以直接对 JSON 列中的数组元素进行索引。实际效果如下所示:

ALTER TABLE user_events
  ADD INDEX idx_tags ((CAST(event_data->'$.tags' AS CHAR(64) ARRAY)));

以下是按标签值过滤查询前后的 EXPLAIN 显示:

-- Without the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data->'$.tags', '"vpn"')\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 100.00
        Extra: Using where

-- After adding the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data->'$.tags', '"vpn"')\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: range
possible_keys: idx_tags
          key: idx_tags
      key_len: 67
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using where

从全表扫描变为范围扫描。在 5 行数据时这微不足道,但对于拥有数百万行数据且频繁按标签过滤的表,这种差异非常显著。改进效果直接与表大小和查询频率成正比。

一个重要的注意事项:MEMBER OF()JSON_OVERLAPS() 也能受益于多值索引,但 JSON_SEARCH() 不能。这在设计时选择查询模式时很重要:

-- This WILL use the multi-valued index:
SELECT * FROM user_events
WHERE 'vpn' MEMBER OF (event_data->'$.tags');

-- This will NOT use it:
SELECT * FROM user_events
WHERE JSON_SEARCH(event_data->'$.tags', 'one', 'vpn') IS NOT NULL;

聚合和转换 JSON

几个值得深入了解的聚合函数:

``````html
-- Build a JSON array of actions per user
SELECT user_id,
       JSON_ARRAYAGG(JSON_VALUE(event_data, '$.action')) AS actions
FROM user_events
GROUP BY user_id;

+---------+----------------------+
| user_id | actions              |
+---------+----------------------+
|       1 | ["login","purchase"] |
|       2 | ["login","purchase"] |
|       3 | ["logout"]           |
+---------+----------------------+
3 rows in set (0.01 sec)

-- Summarize into a JSON object keyed by action
SELECT user_id,
       JSON_OBJECTAGG(
         JSON_VALUE(event_data, '$.action'),
         JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)
       ) AS score_by_action
FROM user_events
GROUP BY user_id;

+---------+--------------------------------+
| user_id | score_by_action                |
+---------+--------------------------------+
|       1 | {"login": 88, "purchase": 72}  |
|       2 | {"login": 91, "purchase": 84}  |
|       3 | {"logout": 65}                 |
+---------+--------------------------------+
3 rows in set (0.00 sec)

JSON_OBJECTAGG() 如果组内有重复键,会抛出错误。在生产 ETL 管道中遇到之前了解这一点很有价值。那时,你需要在上游去重,或在数据到达此聚合步骤前在应用逻辑中处理。

JSON 重查询后检查 SHOW STATUS

在评估查询模式时,检查 handler 指标是一个有用的习惯:

FLUSH STATUS;

SELECT * FROM user_events
WHERE JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) > 80;

SHOW STATUS LIKE 'Handler_read%';

+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Handler_read_first         | 1     |
| Handler_read_key           | 0     |
| Handler_read_last          | 0     |
| Handler_read_next          | 4     |
| Handler_read_prev          | 0     |
| Handler_read_rnd           | 0     |
| Handler_read_rnd_next      | 6     |
+----------------------------+-------+
7 rows in set (0.00 sec)

Handler_read_rnd_next 值确认了全表扫描——这并不意外,因为 score 值上没有函数索引。对于大规模的基于 score 的过滤,使用带索引的生成列是正确答案:

ALTER TABLE user_events
  ADD COLUMN score_val TINYINT UNSIGNED
    GENERATED ALWAYS AS (JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)) VIRTUAL,
  ADD INDEX idx_score (score_val);

添加后,相同查询会降级为正确的索引范围扫描。JSON 字段上的生成列在 MySQL 8.0 和 Percona Server 8.0 中均可用,它们仍然是任何有意义的规模下标量 JSON 字段过滤的最可靠路径。

如果你在使用 Percona Server,来自 Percona Toolkitpt-query-digest 仍然是识别哪些 JSON 重查询在生产中真正造成问题的最实用方法,在你开始推测性地添加索引之前。

实际观察

  • 多值索引(8.0.17+)是一个迟来的改进,当你的查询模式与 JSON_CONTAINS()MEMBER OF() 匹配时工作良好
  • 带 RETURNING 的 JSON_VALUE()(8.0.21+)比旧的提取后转换模式更简洁,值得一致采用
  • 生成列加索引仍然是大规模标量 JSON 字段过滤的最可靠路径
  • 注意分组数据中的 JSON_OBJECTAGG() 重复键错误——它在 ETL 管道中表现为硬错误,如果你的样本数据恰好干净,在测试中很容易遗漏
  • 始终使用 EXPLAIN 验证索引使用——优化器并不总能在复杂 WHERE 子句中选择多值索引,值得确认而非假设

总结

MySQL 8.0 的 JSON 改进确实很有用,特别是多值索引和带类型转换的 JSON_VALUE()。它们不能取代良好的架构设计,但对于 JSON 存储合适或已继承的情况,你现在有了真正的工具可用,而不是仅仅希望优化器能搞定。特别是生成列模式,如果知道某些 JSON 字段将定期用于 WHERE 子句,值得及早评估。

有用参考: