1. 这不是教科书里的“理论推演”,而是数据库工程师每天在SQL执行计划里真实踩过的坑
关系代数表达式优化步骤——这八个字,听起来像数据库原理课上一页翻过去的定义,但如果你正在调一条跑得比泡面还慢的报表SQL,或者刚被DBA拉着看执行计划里那个刺眼的Nested Loop Join,又或者在凌晨三点盯着pg_stat_statements里top 3的CPU消耗语句发呆……那它就是你手边最硬核的生存工具。我干了12年数据库底层开发和性能调优,从Oracle RAC集群到TiDB分布式事务,再到PostgreSQL高并发OLTP场景,所有“快”都不是靠加机器堆出来的,而是把关系代数这门老手艺,一锤一钉地砸进每条查询的执行路径里。它不讲花哨的AI向量索引,也不谈云原生弹性扩缩容,就专注一件事:让σ(选择)、π(投影)、×(笛卡尔积)、⋈(连接)、∪(并)、−(差)这些基础算子,在物理执行前,用数学规则和工程直觉,重新排兵布阵。新手常误以为这是DBMS自动完成的黑盒,实则不然——MySQL 8.0的cost-based optimizer会因统计信息陈旧而选错连接顺序;PostgreSQL的join_collapse_limit默认值为8,超过就放弃重排;而ClickHouse这类列存引擎,甚至要求你手动把π(投影)尽可能前置,否则全列扫描的IO代价直接翻倍。这篇文章不讲抽象公理,只拆解我在金融风控实时计算、电商大促订单归因、物联网时序数据聚合这三类高压场景中,亲手写、亲手改、亲手压测验证过的五步优化法:从原始表达式解析开始,到等价变换的边界判断,再到物理算子选择的权衡取舍,最后落到执行计划反向验证。每一步都附带真实SQL片段、执行耗时对比(单位:ms)、以及我当年在监控面板上看到那个红色告警时,到底改了哪一行逻辑。适合DBA、后端工程师、数据平台开发者,也适合刚学完《数据库系统概论》想把纸面知识焊进生产环境的同学。你不需要记住所有代数定律,但必须清楚:什么时候该信优化器,什么时候该亲手干预,以及干预时,哪一步动错了,会让性能雪崩而非提升。
2. 为什么不能直接交给优化器?——五步法背后的工程现实与数学约束
2.1 优化器不是神,它受限于三个硬性天花板
数据库优化器(Optimizer)本质是一个基于代价模型(Cost Model)的搜索器,它试图在所有可能的执行计划空间中,找到一个预估总代价最低的方案。但这个“所有可能”是被严格限定的,绝非穷举。理解这三重限制,是掌握关系代数优化的前提:
第一重限制:搜索空间剪枝策略(Search Space Pruning)
优化器不会真的生成并评估每一种连接顺序组合。以5张表JOIN为例,理论上存在(5-1)! = 24种左深树(Left-deep Tree)连接顺序,若再考虑右深树(Right-deep)和稠密树(Bushy Tree),组合数呈指数爆炸。因此,所有主流数据库都采用动态规划(如Selinger算法)或遗传算法进行剪枝。PostgreSQL使用的是基于动态规划的“exhaustive search”,但其join_collapse_limit参数默认为8,意味着当FROM子句中显式列出的表超过8个时,优化器会强制将前8个表视为一个不可分割的整体,放弃对它们之间连接顺序的重排。我曾在线上遇到一个9表JOIN的报表SQL,执行时间从12s飙升至287s,原因正是第9张表被当作“外挂”强行嵌套,导致本可优化的星型连接(Star Join)变成了链式嵌套循环。这不是优化器能力不足,而是工程上对编译耗时的主动妥协——毕竟,用户无法接受一条SQL光编译就等半分钟。
第二重限制:代价模型的固有偏差(Cost Model Bias)
代价模型依赖统计信息(Statistics)估算行数、IO次数、CPU开销。但统计信息永远滞后于真实数据分布。例如,某电商订单表按order_time分区,新分区每日增量500万行,但ANALYZE任务每周才跑一次。当优化器基于过期统计认为WHERE order_time > '2024-06-01'会返回10万行时,实际可能只有5000行(因促销活动提前结束)。此时,它可能错误选择Hash Join(需构建哈希表),而最优解其实是Index Nested Loop Join(利用order_time索引快速定位)。更隐蔽的问题在于代价权重——MySQL默认将随机IO代价设为顺序IO的10倍,但在NVMe SSD上,这个比值实际接近1.5。我们调优时发现,将random_page_cost从默认的4.0调低至1.1,能让优化器在SSD集群上更倾向选择Index Scan而非Seq Scan,TPS提升17%。这说明,代数优化的第一步,永远是校准代价模型的“感官”。
第三重限制:等价变换的语义鸿沟(Semantic Gap of Equivalence)
关系代数中的等价规则(如选择下推、投影下推、连接结合律)在数学上成立,但落地到物理执行时,存在语义断层。最典型的是σ(A>10 ∧ B=5)下推到单表扫描 vsσ(A>10)和σ(B=5)分别下推再交集。数学上等价,但物理上:前者可利用复合索引(A,B)高效过滤;后者若只有单列索引(A)和(B),则需两次索引扫描+Merge Join,IO翻倍。另一个致命陷阱是外连接(Outer Join)的结合律失效——(R ⋈_L S) ⋈_L T与R ⋈_L (S ⋈_L T)在结果集上并不等价,因为外连接的NULL补全行为依赖于连接顺序。我曾在迁移Oracle SQL到Greenplum时栽过跟头:Oracle优化器自动重排外连接顺序且保证语义正确,而Greenplum 6.x的优化器在特定条件下会错误应用结合律,导致LEFT JOIN结果多出NULL行。因此,“等价”必须打上“物理可实现”的钢印——任何变换,必须同时满足数学等价性和执行器支持性。
2.2 五步法不是线性流程,而是带反馈的闭环
很多教材把优化步骤画成一条直线:语法树→逻辑计划→等价变换→物理计划→执行。但在真实世界,它是带反馈的闭环。我的工作台常年开着三个窗口:SQL编辑器、EXPLAIN ANALYZE输出、以及pg_statistic元数据查询。五步法的每一步,都可能因后续验证失败而退回上一步重构。例如:
- **Step 3(连接顺序重排)**完成后,执行
EXPLAIN (ANALYZE, BUFFERS)发现Hash Join的内存溢出(Work_mem不足),这时必须回到Step 2,考虑是否将某个大表的σ条件进一步下推,减少参与Join的行数,而非强行换Join算法; - **Step 4(索引选择)**选定
idx_order_user_status后,EXPLAIN显示仍走Seq Scan,查pg_indexes才发现该索引因bloat_ratio > 0.3而被优化器弃用,需先VACUUM FULL; - **Step 5(执行计划验证)**发现
Parallel Seq Scan未启用,检查max_parallel_workers_per_gather配置为0,这属于基础设施层问题,需协同运维调整。
因此,五步法的真正内核是“假设-验证-修正”循环。每一步的输出,都是下一个步骤的输入,也是上一步结论的验证凭证。没有哪一步能脱离执行计划的实证而存在。这也是为什么我坚持要求团队新人:写完优化方案,必须贴出三组数据——原始SQL的EXPLAIN ANALYZE、优化后SQL的EXPLAIN ANALYZE、以及关键中间结果(如SELECT COUNT(*) FROM (σ(...)) AS t)的实际行数。数字不说谎,它比任何代数推导都更有说服力。
3. 五步法详解:从纸面代数到生产执行的完整链路
3.1 Step 1:原始表达式解析与执行计划基线捕获
这一步看似简单,却是整个优化过程的地基。很多人跳过此步,直接看执行计划,结果连“慢在哪”都没找准。必须做三件事:
第一,还原标准关系代数表达式
以一条真实风控SQL为例:
SELECT u.user_name, o.order_amount, p.product_name FROM users u JOIN orders o ON u.user_id = o.user_id JOIN products p ON o.product_id = p.product_id WHERE u.status = 'active' AND o.order_time >= '2024-06-01' AND p.category IN ('electronics', 'books');其标准关系代数表达式为:π_{u.name, o.amount, p.name} ( σ_{u.status='active'}(users) ⋈_{u.id=o.uid} σ_{o.time≥'2024-06-01'}(orders) ⋈_{o.pid=p.id} σ_{p.cat∈{...}}(products) )
注意:这里已隐含了选择下推(Selection Pushdown)——WHERE条件被分配到各自基表上。但原始SQL并未显式写出,需人工补全。这步的关键是识别所有谓词(Predicate)的归属表,避免后续误判。例如,u.status='active'只能作用于users表,若错误下推到orders表,逻辑即错。
第二,捕获未经优化的执行计划基线
在目标数据库中执行:
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <your_sql>;重点抓取以下字段:
Plan Rows:优化器预估行数(vsActual Rows真实行数,偏差>5倍即统计失真)Buffers:shared hit/read/dirtied,反映缓存效率Planning Time:若>100ms,说明优化器搜索耗时过长,需检查join_collapse_limit或统计信息Node Type:识别瓶颈节点(如Seq Scan、Nested Loop、Hash Join)
我习惯用Python脚本自动解析JSON输出,提取关键指标生成对比表。例如,上述SQL基线显示:
| Node Type | Plan Rows | Actual Rows | Buffers Read | Cost |
|---|---|---|---|---|
| Seq Scan (orders) | 12,500,000 | 8,200,000 | 142,356 | 284,500 |
| Hash Join | 1,250,000 | 98,500 | 0 | 312,000 |
可见,orders表全表扫描是最大瓶颈,Plan/Actual行数比1.5倍尚可接受,但Buffers Read高达14万次,说明缓存未生效。
第三,建立可量化的性能基线
用pgbench或sysbench对SQL进行10轮压测,记录:
- 平均响应时间(P50/P95/P99)
- QPS(Queries Per Second)
- CPU利用率(
top -p <pid>) - 关键等待事件(
pg_stat_activity.wait_event_type)
提示:基线必须在业务低峰期、关闭其他干扰查询的环境下获取。我曾因在测试库跑基线时未清空
shared_buffers,导致后续优化效果被缓存掩盖,误判方案无效。
3.2 Step 2:等价变换可行性分析与安全边界划定
代数变换不是“越变越好”,而是“在安全边界内求最优”。必须回答三个问题:能否变?为何变?变后是否可控?我用一张决策树快速判断:
是否涉及外连接? → 是 → 检查连接顺序是否影响NULL补全 → 否则禁止重排 ↓否 谓词是否可下推? → 查表统计信息:若σ条件选择率<10%,且存在对应索引 → 可下推 ↓否 是否可分解? → 对复杂JOIN,尝试用子查询分解:(R ⋈ S) ⋈ T → R ⋈ (S ⋈ T) → 验证结果集行数是否一致 ↓ 是否可合并? → 多个σ可合并为σ_{cond1 ∧ cond2},但需确认数据库是否支持复合索引以风控SQL为例,分析如下:
选择下推(Selection Pushdown)
u.status='active':users表status列有索引,ANALYZE显示active占比35%,选择率尚可,下推安全;o.order_time >= '2024-06-01':orders表该列有索引,且日期范围小(仅30天),选择率约2.1%,强烈建议下推;p.category IN (...):products表category列无索引,但值域小(仅12个分类),IN列表短,下推收益有限,暂不处理。
投影下推(Projection Pushdown)
原始SQL需u.user_name, o.order_amount, p.product_name。但users表有50+列,orders表有30+列。若在JOIN前只取所需列,可大幅减少内存占用。PostgreSQL支持SELECT u.name, o.amount, p.name FROM ...,但需注意:投影下推不能破坏连接条件所需的列。例如,u.user_id虽不在最终输出,但它是JOIN条件,必须保留在中间结果中。因此,安全下推表达式为:π_{u.name, u.id, o.amount, o.user_id, o.product_id, p.name, p.id} ( ... )
其中u.id, o.user_id, o.product_id, p.id是连接键,不可省略。
连接顺序重排(Join Order Reordering)
当前顺序:users ⋈ orders ⋈ products。行数估算:
users:10M行,σ_{status}后≈3.5Morders:12.5M行,σ_{time}后≈260Kproducts:50K行,σ_{cat}后≈8K
按左深树,先users ⋈ orders:3.5M × 260K = 910B行(笛卡尔积!),显然灾难。最优顺序应是最小结果集驱动:products (8K) ⋈ orders (260K) ⋈ users (3.5M)。但需验证products ⋈ orders的连接基数——products.product_id是主键,orders.product_id是外键,1:N关系,结果约为260K行,远小于910B。这就是重排的核心逻辑:让小表做驱动,大表做被驱动,避免中间结果爆炸。
实操心得:我用Excel建了个简易计算器,输入各表过滤后行数、连接类型(1:1, 1:N, N:N),自动计算不同顺序的中间结果大小。比心算快10倍,且不易出错。公式很简单:
Result_Size = Left_Rows × Right_Rows / Selectivity,其中Selectivity是连接列的唯一值比例。
3.3 Step 3:连接算法与物理算子选型
代数层面确定了products ⋈ orders ⋈ users的顺序,但物理执行时,每一对JOIN用什么算法,决定性能生死。主流算法有三种,选择逻辑如下:
| 算法 | 适用场景 | 内存需求 | IO特征 | 我的选型口诀 |
|---|---|---|---|---|
| Nested Loop Join | 驱动表极小(<1000行),被驱动表有高效索引 | 极低 | 驱动表每行触发一次被驱动表索引查找 | “小驱大索,NLJ稳如狗” |
| Hash Join | 两表都较大,内存充足(work_mem足够建哈希表) | 高(需2×被驱动表大小) | 一次全扫被驱动表建哈希表,驱动表流式探测 | “内存够,Hash快,OOM就跪” |
| Merge Join | 两表均已按连接列排序(有索引或已排序) | 中等 | 双指针顺序扫描,IO最友好 | “都排好,Merge秒,乱序别碰” |
针对products ⋈ orders:
products过滤后8K行,orders过滤后260K行products.product_id是主键(天然有序),orders.product_id有索引(可快速定位)work_mem设置为256MB,足够为260K行建哈希表(≈260K×20B=5MB)- 但
orders表无product_id排序,Merge Join需额外排序,成本高
决策:Hash Join。理由:内存绰绰有余,且避免排序开销。
针对[products ⋈ orders] ⋈ users:
- 中间结果260K行,
users表3.5M行 users.user_id是主键(有序),中间结果user_id来自orders,无索引work_mem剩余约200MB,建3.5M行哈希表需≈70MB,安全
决策:Hash Join。但需确保users表扫描走Index Scan而非Seq Scan——检查users.user_id是否有索引(必有,主键),且ANALYZE统计准确。
关键操作:强制指定连接算法(Hint)
PostgreSQL不支持传统Hint,但可用SET enable_hashjoin = off等GUC参数临时禁用。更稳妥的是重写SQL,引导优化器:
-- 引导Hash Join:显式用子查询物化中间结果 WITH filtered_products AS ( SELECT product_id, product_name FROM products WHERE category IN ('electronics', 'books') ), filtered_orders AS ( SELECT user_id, product_id, order_amount FROM orders WHERE order_time >= '2024-06-01' ) SELECT fp.product_name, fo.order_amount, u.user_name FROM filtered_products fp JOIN filtered_orders fo ON fp.product_id = fo.product_id JOIN users u ON fo.user_id = u.user_id WHERE u.status = 'active';子查询filtered_products和filtered_orders会被优化器物化(Materialize),其结果集大小明确,极大提升JOIN顺序和算法选择的确定性。实测此写法使EXPLAIN中Hash Join出现概率从62%提升至100%。
3.4 Step 4:索引策略与物理存储适配
代数优化再完美,没有匹配的物理结构支撑,也是空中楼阁。索引设计必须服务于具体的变换后的执行路径。针对优化后的SQL:
第一步:识别所有访问路径
从执行计划看,关键访问有:
products表:WHERE category IN (...)→ 需category索引orders表:WHERE order_time >= ...+JOINonproduct_id→ 需复合索引(order_time, product_id)users表:WHERE status = ...+JOINonuser_id→ 需复合索引(status, user_id)
第二步:验证索引有效性
创建索引后,必须验证是否被选用:
-- 检查索引使用率 SELECT indexrelname, idx_scan FROM pg_stat_all_indexes WHERE relname = 'orders' AND indexrelname LIKE 'idx%';若idx_scan为0,说明索引未被使用,需检查:
- 谓词是否匹配索引最左前缀(
WHERE order_time = ?可用,WHERE product_id = ?不可用) - 数据类型是否隐式转换(
order_time是timestamp,但查询用字符串'2024-06-01',触发类型转换,索引失效)
第三步:处理索引膨胀(Bloat)
高写入表(如orders)的索引易膨胀。用以下SQL检测:
SELECT schemaname, tablename, indexname, ROUND(bloat_ratio::numeric, 1) AS bloat_pct FROM ( SELECT schemaname, tablename, indexname, CASE WHEN bs * (index_tuple_count + coalesce(tup_deleted, 0)) > 0 THEN 100 * (bs * (index_tuple_count + coalesce(tup_deleted, 0)) - (bs - 4) * index_tuple_count) / (bs * (index_tuple_count + coalesce(tup_deleted, 0)))::float ELSE 0 END AS bloat_ratio FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace CROSS JOIN (SELECT current_setting('block_size')::integer AS bs) AS bs WHERE c.relkind = 'i' AND n.nspname NOT IN ('pg_catalog', 'information_schema') ) AS t WHERE bloat_ratio > 20;若bloat_pct > 30%,需REINDEX INDEX idx_orders_time_pid;。我坚持每月自动巡检,因为索引膨胀是性能衰减最隐蔽的杀手——它让原本高效的Index Scan退化为Seq Scan,而你却在EXPLAIN里看不到任何异常。
3.5 Step 5:执行计划验证与效果量化
优化不是改完SQL就结束,而是用数据证明价值。我要求每项优化必须提供三组证据:
证据一:执行计划对比图
用EXPLAIN (ANALYZE, BUFFERS)输出生成对比表。优化前后关键指标变化:
| 指标 | 优化前 | 优化后 | 变化 | 说明 |
|---|---|---|---|---|
| Planning Time | 142ms | 28ms | ↓80% | 连接顺序简化,搜索空间缩小 |
| Execution Time | 12,450ms | 862ms | ↓93% | Hash Join替代Nested Loop,IO大幅降低 |
| Buffers Read | 142,356 | 18,942 | ↓87% | 索引精准过滤,减少块读取 |
| Shared Hit | 92% | 98% | ↑6% | 热数据缓存命中率提升 |
证据二:真实业务指标
在生产环境灰度发布后,监控核心业务指标:
- 报表生成延迟:从T+2小时降至T+15分钟
- 订单查询P95响应时间:从3200ms降至210ms
- 数据库CPU峰值:从92%降至65%
证据三:回归测试报告
用pg_dump导出优化前后1000行结果,MD5比对确保逻辑一致性。特别关注:
- NULL值处理(外连接场景)
- 重复行去重(
DISTINCT或GROUP BY) - 排序稳定性(
ORDER BY是否仍保持)
常见问题:优化后SQL执行更快,但业务方反馈“数据少了”。排查发现:
products表category列有NULL值,WHERE category IN (...)自动过滤了NULL行,而原始SQL因未下推,NULL行参与了JOIN。解决方案:显式添加OR category IS NULL,或在products表增加CHECK (category IS NOT NULL)约束。代数优化必须敬畏业务语义,数学等价不等于业务等价。
4. 那些教科书不会写的实战陷阱与避坑指南
4.1 “选择下推”不是万能钥匙:三类典型失效场景
场景一:函数索引的陷阱WHERE to_char(order_time, 'YYYY-MM') = '2024-06',即使order_time有索引,也无法下推,因为to_char函数破坏了索引有序性。正确做法:改用范围查询order_time >= '2024-06-01' AND order_time < '2024-07-01',或创建函数索引CREATE INDEX idx_orders_month ON orders ((to_char(order_time, 'YYYY-MM')));。但后者需确保查询条件完全匹配函数调用,且维护成本高。
场景二:OR条件的索引失效WHERE status = 'active' OR status = 'pending',若status列选择率高(如active占80%),优化器可能放弃索引,选择Seq Scan。此时应改用IN:WHERE status IN ('active', 'pending'),或拆分为UNION ALL(需保证无重叠)。
场景三:隐式类型转换WHERE user_id = '12345'(user_id是BIGINT),字符串'12345'需转为数字,索引失效。必须写成WHERE user_id = 12345。我在代码审查中,用正则WHERE\s+\w+\s*=\s*['"]\d+['"]自动扫描此类风险点。
4.2 连接顺序重排的“死亡之环”:N:N连接的指数爆炸
当遇到多对多(N:N)连接时,重排可能引发灾难。例如:students ⋈ enrollments ⋈ courses ⋈ instructors,其中enrollments是关联表,students和courses是多对多。若错误重排为students ⋈ courses(笛卡尔积),10万学生×5千课程=500亿行,内存瞬间打满。安全法则:N:N连接必须通过关联表(enrollments)作为枢纽,且关联表必须是第一个JOIN对象。即:enrollments ⋈ students ⋈ courses ⋈ instructors,确保中间结果始终受enrollments行数约束(通常远小于两端主表)。
4.3 统计信息“假繁荣”:ANALYZE不是万能药
ANALYZE更新统计信息,但并非总能解决问题。常见误区:
- 采样率不足:大表默认采样率
default_statistics_target=100,对倾斜数据(如90%行status='active',10%行status='blocked')估算严重失真。解决方案:ALTER TABLE users SET STATISTICS 1000; ANALYZE users;提高采样精度。 - 分区表统计缺失:
ANALYZE默认不分析分区,需ANALYZE VERBOSE partitions;或设置autovacuum_analyze_scale_factor=0.01。 - 统计信息过期:高频写入表,
ANALYZE间隔应缩短。我为orders表设置autovacuum_analyze_threshold=5000(5千行变更即触发)。
4.4 执行计划“幻觉”:为什么EXPLAIN说快,实际却慢?
EXPLAIN ANALYZE显示862ms,但应用端监控显示3200ms。原因通常是:
- 网络传输耗时:
EXPLAIN ANALYZE只测数据库内执行,不包括结果集网络传输。大结果集(如百万行)序列化+网络发送占大头。解决方案:前端分页,或数据库端LIMIT。 - 锁等待:
EXPLAIN ANALYZE在无竞争环境下运行,生产环境可能因行锁、页锁阻塞。查pg_locks和pg_stat_activity,重点关注wait_event为Lock或IO的会话。 - 资源争抢:同一节点其他查询抢占CPU/内存。用
htop和iostat -x 1交叉分析。
我的终极检查清单:当优化效果不符预期,立即执行
SELECT * FROM pg_stat_statements WHERE query LIKE '%your_sql%' ORDER BY total_time DESC LIMIT 1;SELECT pid, wait_event_type, wait_event, state FROM pg_stat_activity WHERE state = 'active' AND query LIKE '%your_sql%';SELECT * FROM pg_stat_io;(看IO压力)
这三步,90%的“幻觉”问题都能定位。
5. 从“会优化”到“懂优化”:我的三年实践心法
关系代数优化,入门门槛不高,但要达到“一眼看穿执行计划瓶颈”的境界,需要跨越三道坎。这是我带团队时总结的“三年心法”:
第一年:信规则,练肌肉记忆
死记硬背等价规则:选择下推、投影下推、连接交换律/结合律。每天手写10条SQL的代数表达式,用EXPLAIN验证。目标是形成条件反射——看到WHERE条件,立刻想到“能否下推”;看到多表JOIN,本能思考“哪个表最小”。这阶段,错不怕,怕的是不验证。我要求新人提交的每份优化方案,必须附带EXPLAIN截图和行数对比,少一项打回重做。
第二年:疑规则,建工程直觉
开始质疑教科书。为什么σ(A>10) ⋈ σ(B=5)比σ(A>10 ∧ B=5)慢?因为前者需两次索引扫描,后者一次复合索引即可。为什么ORDER BY有时加速JOIN?因为排序后Merge Join比Hash Join更省内存。这阶段,要建立“代数变换→物理代价→硬件特性”的映射。我让团队成员轮流负责数据库内核模块(如PostgreSQL的src/backend/optimizer/),哪怕只读懂pathkeys.c中排序键生成逻辑,也能深刻理解ORDER BY对Join的影响。
第三年:破规则,创场景方案
不再拘泥于标准代数,而是针对特定场景创新。例如:
- 实时风控场景:牺牲部分精确性,用
APPROX_COUNT_DISTINCT替代COUNT(DISTINCT),将O(n)复杂度降为O(1); - 时序数据场景:放弃传统JOIN,改用
LATERAL子查询+时间窗口函数,让orders按order_time分片,products按category分片,实现数据局部性; - 超大宽表场景:将
SELECT * FROM huge_table WHERE ...拆解为SELECT id FROM huge_table WHERE ...(轻量查询)+SELECT * FROM huge_table WHERE id IN (...)(批量获取),规避宽表IO瓶颈。
最后分享一个小技巧:我桌面常备一张A4纸,标题“优化决策树”,内容只有三问:
- 这条SQL的业务SLA是什么?(报表可容忍分钟级,交易必须毫秒级)
- 它的数据分布特征是什么?(均匀?倾斜?稀疏?)
- 基础设施瓶颈在哪?(CPU?内存?IO?网络?)
答案不同,优化策略天壤之别。比如同样是COUNT(*),OLAP场景用物化视图预计算,OLTP场景用pg_stat_database.tup_returned近似值。代数是骨架,业务是血肉,基础设施是大地——脱离任何一者谈优化,都是纸上谈兵。