你肯定见过这种 sql。
查一次三十秒,一天还跑八百回,而且每次吐出来的结果一模一样。你盯着执行计划看半天,索引加了,统计信息收了,连 vacuum 都跑了一遍,没辙,照样慢。
为啥?因为这压根不是执行计划的锅。毛病出在 sql 本身在干重复劳动:同一份结果算了一遍又一遍,或者一条写得稀烂的语句你还没权限改,它就只能一遍遍那么低效地跑着。这种慢,优化器是真救不了,得换个思路。别让它重复,把算过的结果直接拿来用。空间换时间,就这么简单。
这篇文章聊三个专门干这活的好东西:物化视图、query mapping、函数结果集缓存。
前言
物化视图、query mapping、函数结果集缓存,这三个功能都是人大金仓的 kingbasees 数据库提供的性能优化手段。
为了让你更清晰地理解它们,我将这三个功能整理成了一份表格:
| 功能名称 | 核心作用 | 一句话解释 |
|---|---|---|
| query mapping | sql 语句的“智能替换” | 在不修改应用代码的前提下,将低效的 sql 自动替换为提前配置好的高效 sql。 |
| 物化视图 (materialized view) | 查询结果的“预计算存储” | 把耗时查询的结果像普通表一样存下来,下次直接读取,速度极快。 |
| 函数结果缓存 (function result cache) | 函数执行结果的“短时复用” | 在一条 sql 执行期间,相同的函数调用直接返回缓存结果,避免重复计算。 |
下面,我来为你逐一详细解释每个功能。
1. query mapping:不改代码也能优化sql
这是一个非常实用的功能,它允许dba(数据库管理员)在不修改应用程序源码的情况下,优化有性能问题的sql语句。
工作原理:你可以在数据库中预先创建一条“映射规则”,指定一个“源sql”(低效的)和“目标sql”(高效的)。当数据库收到匹配的“源sql”时,会自动替换成“目标sql”来执行。
两种工作模式:
text(文本)模式:进行简单的字符串匹配和替换,速度快,适合快速解决一些明确的sql问题。
semantics(语义)模式:会进行语法和语义检查,替换更精准、更安全,适合对准确性要求高的场景。
典型应用场景:
性能调优:例如,自动将耗时的
union操作替换为更快的union all。数据库迁移:在将其他数据库迁移到kingbasees时,自动将源库的sql语法转换为kingbasees兼容的语法。
2. 物化视图:为复杂查询加速
你可以把物化视图看作一个“快照”。它把一个复杂、耗时的查询结果,预先计算好并像普通表一样存储在数据库中。
与普通视图的区别:普通视图只是一个“虚拟表”,每次查询时都会重新执行sql去拿数据。而物化视图存储的是真实的数据,因此查询它的速度和在普通表中查询一样快。
数据刷新:由于物化视图是静态的“快照”,当基表(源表)数据变化后,它需要被刷新才能保持最新。kingbasees支持手动或自动的刷新机制。
3. 函数结果缓存:避免重复计算
这个功能专注于优化sql语句中函数的执行效率。
工作原理:当一条sql语句在执行时,如果多次调用同一个确定性函数(即输入相同,输出也相同的函数,如
immutable或stable类型的函数),并且参数也相同,数据库只会真正执行一次,后续调用直接返回缓存的结果。适用场景:特别适用于那些被频繁调用、但计算逻辑复杂的函数,尤其是在处理大量数据行时,能显著减少cpu开销。
一、先别急,有个事你得拎清楚
一个个看之前,先把一件事想明白:这哥仨治的是同一种病,重复计算。
优化器再神,也没法帮你跳过"本来该算一次、却被迫算了一百次"这种事。它能替你挑最快的路,可它不会说"这条路我都走过八百遍了,结果我背下来了"。这恰恰是这三个功能补的位。
它们各自盯的层面不一样。物化视图管的是一整个查询的结果,算一次存好,谁来查都给它现成的。query mapping 更靠前,sql 还没进优化器呢,就被它偷偷换掉了。函数结果集缓存范围最小,就管一条 sql 里被反复调用的那个函数。
所以差别其实就俩字,范围。范围最大的是物化视图,最小的是函数缓存,query mapping 夹在中间管语句。下面挨个说。
二、物化视图:把费劲算的结果先存下来
先说物化视图。
这名字听着唬人,原理土得很:把一个查询的结果,实打实地存下来。普通视图你知道吧,那玩意儿是个"别名",不存数据,每次查照样现算。物化视图不一样,它是真存,小数据放内存,大的落磁盘。
它最拿手的活儿就一件:把那些算起来要命的操作,比如大表连接、复杂分组,提前算好搁那儿。下次再有人查同样的东西,直接拿现成的,省下重算这一大笔开销。
举个我见过的真事。订单表 orders,几千万行,业务天天要按用户汇总订单数和金额。写法没毛病,就是每次都得全表扫加分组:
-- 每次现算:扫全表 + 分组聚集,数据一大就磨叽 select user_id, count(*) as order_cnt, sum(amount) as total from orders group by user_id;
慢。那建个物化视图,把这汇总结果预先算出来存着:
-- 建物化视图:把耗时的聚集预先算好、落盘 create materialized view mv_order_summary as select user_id, count(*) as order_cnt, sum(amount) as total from orders group by user_id; -- 查询直接打物化视图,秒级返回 select * from mv_order_summary where user_id = 1001;
想查得更快,给它建个索引,跟普通表一个套路:
-- 给物化视图建索引,按用户查就快了 create index idx_mv_user on mv_order_summary(user_id);
但是,重点来了,也是好多人栽跟头的地方:底表数据变了,物化视图不会自己跟着变。 本数据库的物化视图不支持自动更新,也没有增量更新这一说,你只能手动刷新,而且一刷就是全量重算:
-- 底表更新后,手动刷新(全量重算) refresh materialized view mv_order_summary; -- 想刷新时还能让人并发查、不锁表,加 concurrently(前提是有唯一索引) create unique index uk_mv_user on mv_order_summary(user_id); refresh materialized view concurrently mv_order_summary;
所以我跟你讲,物化视图是个"挑食"的优化,不是什么时候都能往上怼。它最香的就两种情况。
一种是底表更新少、但查询又重又勤。报表、看板就是它的亲儿子,数据一天才喂一两次,白天被查成千上万遍,预先算好等于白捡。
另一种是查外部库的表。跨库扫描本来就慢得要死,与其每次都跑外边去捞,不如用物化视图把数据搬到本地存着,查的时候走本地:
-- 把访问慢的外部表数据,缓存成本地物化视图(记得定期手动刷新保鲜) create materialized view mv_customer as select * from fdw_customer;
反过来,如果你的表分分钟都在写,刷新又追不上变化,那就别硬上了。缓存刚刷完就过期,纯属给自己找不痛快。
三、query mapping:sql 还没进优化器,先偷梁换柱
再说 query mapping,这个我个人最喜欢,因为它"阴"。
啥意思呢,它让你提前把"源 sql → 目标 sql"的映射关系存进系统表。用户敲进来的语句只要匹配上,数据库就偷偷换成目标语句去跑,用户全程毫无察觉。是不是有点像给 sql 戴了个面具。
它能干两件事,都特别实用。一是 sql 调优:碰到条写得很烂的 sql,偏偏你还没权限改源码(八成是第三方系统发的),这时候建个映射,偷偷把它换成等价但高效的写法,神不知鬼不觉。二是数据库迁移:把别家的方言语法,翻译成本库能跑的语法。
它分两个级别,这俩你得记牢。
text 级别,就是纯字符串匹配,啥都不检查,原样存原始 sql 和目标 sql。semantics 级别高级点,会走一遍词法语法语义检查,存的是查询树,也就是 sql 解析后的那个内部形态。
这里有个坑我必须提醒你:想跟 hint 一起用,只能选 text。因为 hint 是写在注释里的,semantics 存的是查询树,注释根本留不住,hint 也就跟着废了。还有,semantics 现在搞不定带 rownum 的语句,碰上记得绕。
用法不复杂,三步。先开开关,再建规则,然后该查查:
-- 第一步:配置文件里开启(kingbase.conf)
-- enable_query_rule = on
-- 第二步:建一条映射,把低效的 not in 换成高效的 not exists
select create_query_rule(
'qm_tune1',
'select * from t1 where id not in (select id from t2)', -- 源 sql(低效)
'select * from t1 where not exists (select 1 from t2 where t2.id = t1.id)', -- 目标 sql(高效)
true, -- 这条规则生不生效
'text' -- 级别:text 或 semantics
);
规则一立,用户再发那条 not in,底下实际跑的是 not exists,干净利落。
还能玩参数化,这点我挺喜欢。源和目标里都能用 $1、$2 代表入参,匹配的时候按位置对上:
-- 源 sql 带 3 个参数,目标 sql 只关心 val=$3 的计数
select create_query_rule(
'qm_count',
'select id,val from t1 where id<$1 and id>$2 and val=$3',
'select count(0) from t1 where val=$3',
true, 'text'
);
连参数顺序都能给你打乱交换,灵活得很:
-- 把 id<$1 and val=$2 换成 id<$2 and val=$1,入参顺序对调
select create_query_rule(
'qm_swap',
'select * from t2 where id<$1 and val=$2',
'select * from t2 where id<$2 and val=$1',
true, 'text'
);
更绝的是,它还能干"条件下推"这种深度改写。比如外层一个连接条件,正常它压不进 union 子查询里去;建条映射,把语句改成 lateral 形式(就是允许内层引用外层数据那种写法),条件就能推进去,少扫一大堆没用的数据。想看替换之后到底走的啥计划,加个标记就行:
-- 看替换后走的是什么计划 explain (usingquerymapping) select count(0) from t1, (...) as v where t1.id = v.id;
规则管起来也省心,几个函数来回用:
select drop_query_rule('qm_tune1'); -- 删掉某条规则
select enable_query_rule('qm_tune1'); -- 让某条规则生效
select disable_query_rule('qm_tune1'); -- 暂时关掉某条规则
select drop_query_rule(); -- 不传参 = 删光所有规则,悠着点
四、函数结果集缓存:同一个函数调一万遍,只算一遍
最后这个,函数结果集缓存,专门对付函数。
你有没有写过这种代码:写了个函数,然后一条 sql 里反反复复调它。select 里调一次,where 里又调一次,或者对着一堆入参挨个调。函数本身要是就慢,再乘上调那么多次,这条 sql 直接慢到你想砸键盘。
这功能治的就是这个。原理不绕:函数一旦标成 immutable 或者 stable,也就是同样的入参永远返回同样的结果,数据库在一条 sql 跑的期间,就会把"函数 + 入参"算出来的结果存起来。后面再碰到一模一样的入参,直接吐缓存,函数体压根不执行。
得解释下这几个词,不然容易懵。
immutable 最严,入参一样结果就一样,跟表、跟时间都无关,绝对的。stable 松一点,只要同一条 sql 里结果不变就行,比如去查一张很少改的配置表。volatile 就没戏了,结果会变的(取当前时间那种),没法缓存。
想命中缓存,三个条件得凑齐:函数得是 immutable 或 stable;返回值得是单个值,不能是集合;入参别超过 16 个。
来,建个标了 immutable 的折扣函数:
-- immutable 函数:同样的 (price, rate) 永远算出同样的折扣价
create or replace function cal_discount(price numeric, rate numeric)
returns numeric
language sql
immutable
as $$
select round(price * rate, 2);
$$;
开关一开,写条会反复调它的 sql:
-- 开启函数结果集缓存,并设个缓存上限 set function_result_cache = on; set function_cache_number = 1000; -- 这条 sql 里,同一组 (amount, 0.9) 入参的调用只算一次,其余命中缓存 select id, cal_discount(amount, 0.9) as dp from orders where cal_discount(amount, 0.9) > 100;
再补个 stable 的,读张几乎不变的汇率表,一个道理:
-- stable 函数:按币种查汇率,币种没变结果就不变
create or replace function get_rate(currency text)
returns numeric
language sql
stable
as $$
select rate from exchange_rate where cur = currency;
$$;
有个事得重点强调,是这功能跟物化视图最不一样的地方:缓存只在单条 sql 执行期间活着,这条 sql 一跑完就没了。 所以它治不了跨会话的重复,它就管一条 sql 内部那点事。想跨会话缓存?那是物化视图的活儿,别搞混了。
两个开关也好懂,function_result_cache 管开不开,function_cache_number 管最多存几个。
五、那到底用哪个?看场景下菜碟
道理讲完了,真到用的时候怎么选,才是关键。我给你列个表,对号入座就行:
| 维度 | 物化视图 | query mapping | 函数结果集缓存 |
|---|---|---|---|
| 治什么病 | 整个查询结果反复算 | 语句写法低效、改不动源码 | 单条 sql 内函数反复调 |
| 生效范围 | 跨会话、长期 | 针对特定语句模式 | 单条 sql 执行期间 |
| 怎么生效 | 手动全量刷新 | 建映射规则替换 | 自动命中缓存 |
| 数据新鲜度 | 有延迟,需刷新 | 不碰数据,只换写法 | 实时,入参同则结果同 |
| 典型场景 | 报表、看板、外部表 | 第三方系统 sql 调优、迁移 | 复杂计算函数高频调用 |
再啰嗦几句掏心窝的。
报表、查多写少的数据,闭眼选物化视图。但刷新频率你自己心里得有数,别数据都隔夜了还在用昨天的结果,出了事锅可是你的。
没法改源码、又非得优化某条语句,query mapping 顶上。这玩意儿最大的好处是无侵入,应用那边一行都不用动,我个人特别偏爱它。
一个计算函数被调成千上万次的,开函数结果集缓存。开之前一定确认函数确实是 immutable 或 stable,别到时候结果错了还查不出哪出的毛病。
还有,这三个不是互斥的,能搭着用。拿 query mapping 把低效语句改写成走物化视图,效果直接叠加,爽得很。
六、收个尾
说到底,这三个东西干的是同一件事:跟重复较劲。
物化视图管结果级的重复,提前算好存着;query mapping 管语句级的重复,从源头换掉烂写法;函数缓存管调用级的重复,一条 sql 内别傻算。仨人各占一段赛道,谁也替不了谁。
真想用好它们,功夫其实不在背语法上,而在你能不能先想明白一件事:我这条 sql 慢,到底慢在哪种重复上。想清楚了,对症下药,性能蹦一个数量级,真不是吹的。
下次再碰到那种优化器都救不了的慢查询,先别急着骂优化器,它也挺冤的。先问自己一句:这活儿,是不是在反复干同一件事?是的话,这仨里头,总有一个能收拾它。
这三个功能共同构成了kingbasees数据库一套强大的性能优化工具集:
query mapping 让你能低成本、无侵入地解决棘手的sql性能问题。
物化视图 为高频、复杂的查询提供了极速的访问体验。
函数结果缓存 则通过避免重复计算来提升sql的整体执行效率。
到此这篇关于sql慢得像蜗牛怎么办?kingbasees的物化视图、querymapping和函数缓存优化实战的文章就介绍到这了,更多相关kingbasees的性能优化利器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论