Enhanced Subquery Optimizations in Oracle

简介: 在前面的文章中,介绍了Oracle是如何做cost-based query transformation的,其中也包含Oracle基于rules/cost所做的若干种常见的query rewrite。对于CBQT的极简描述是对一个query做deep copy后,尝试一系列transform并利用physical optimizer计算cost,如果cost更低则保留该变换,否则抛弃。详细介绍可以参考之前的文章。这篇paper算是之前那篇的后续,把重点放在了对subquery的处理上。subquery是一种在分析型query中非常常见的SQL construct,非常便于表达一些语义因此被

在前面的文章中,介绍了Oracle是如何做cost-based query transformation的,其中也包含Oracle基于rules/cost所做的若干种常见的query rewrite。对于CBQT的极简描述是对一个query做deep copy后,尝试一系列transform并利用physical optimizer计算cost,如果cost更低则保留该变换,否则抛弃。详细介绍可以参考之前的文章。

这篇paper算是之前那篇的后续,把重点放在了对subquery的处理上。subquery是一种在分析型query中非常常见的SQL construct,非常便于表达一些语义因此被广为使用。例如Decision support benchmark TPC-H的查询中,有近一半使用了subquery(Query 2, 4, 11, 15, 16, 17, 18, 20, 21, 22),其中大多数是相关子查询且包含聚集函数。

paper中主要介绍的是更为先进的Oracle 11g,其中包含了一些新的transformation。(subquery coalescing/subquery removal/filter join elimination...) 此外还介绍了一些parallel execution技术,这些并行技术提高了query执行的扩展性,并可以和transformation结合在一起。

Subquery处理概述

对于相关子查询,其最朴素的执行语义就是针对外表一行,将相关列的值带入内层子查询,并执行子查询计算结果,在用结果对外表进行过滤,这是一种类似Nested-loop的执行方式,如果不做unnesting,则无法考虑更多的access path/join order/join method的可能。

Oracle对子查询的unnesting主要分为2类:

  1. 转换为derived table
  2. merge到外层query block中

前者是基于cost来决策的,后者则基于启发式规则。

如果将non-scalar的subquery merge到外层,一般会形成semi join/anti join,Oracle会使用3种执行方式来做join: index-lookup / hash / merge

  1. 如果在内层表有索引可以使用,可以使用index lookup
  2. 否则如果是等值join时,会使用hash的执行方式

对于以上2种方式,当left table中有很多duplicate时,会针对这些duplicate行,将semi/anti join的结果cache起来,从而避免了反复计算,算是一个局部执行优化。

3. 如果是 > ANY, < ALL 这样的非等值join,就使用sort merge join。

4. 对于 NOT IN, <> ALL 这样的subquery,在10g中是无法merge到外层的,在11g中引入了null-aware anti join,实现了merge,这篇paper也对NAAJ进行了介绍。

5. 在某些情况下,会使用window function对subquery进行消除,其思路和之前一篇文章介绍的DB2的WinMagic如出一辙

Subquery coalescing

这里针对的subquery首先是作为outer query的filter条件存在的,可以将多个这样的filter中,等价的subquery合并为一个,这样就减少了subquery本身的执行次数和代价。虽然合并是要两两进行的,但可以反复执行,从而将多个subquery合并在一起。

如果两个subquery,可以产生相同的multi-set(bag) result,则认为他们是语义上等价的,在结构/语法上完全相同(identical)通常就意味着两者的等价。

如果其中一个qb2,相对于另一个qb1,除了包含有更多的AND谓词p2外,其余是完全identical的(一定是AND!!),则其结果必然是qb1的结果的子集,则认为qb1包含qb2,两者具有包含关系。

在11g中,Oracle支持对Exist/Not Exist出现在AND/OR中的多种组合情况的合并。

极简情况

如果两个subquery是相同的(identical),则无论是AND还是OR,都可以直接消除其中一个。

subquery具有相同类型

假设qb1包含qb2,qb2有更多的谓词条件p2,因此包含更少的结果数据(更严格)。

  1. EXIST qb1 AND EXIST qb2 =>  EXIST qb2,这个很容易推导,因为qb2更严格,满足它必然满足qb1。
  2. NOT EXIST qb1 OR NOT EXIST qb2 => NOT EXIST qb2,这个和1等价。
  3. EXIST qb1 OR EXIST qb2 => EXIST qb1,这个也容易推导,qb1满足条件时,即为true,qb1不满足时,qb2因为数据更少更无法满足,因此为false。
  4. NOT EXIST qb1 AND NOT EXIST qb2 => NOT EXIST qb1,这个和3等价。

对于EXIST qb1 OR EXIST qb2 / NOT EXIST qb1 AND NOT EXIST qb2这样的谓词形态,即使qb1,qb2之间不存在包含关系,但他们只在某些conjunctive谓词上有所区别,例如qb1有p1,qb2有p2,但除去不同部分,其余是等价的,则两个subquery也可以合并。这其实是OR-expansion的反过程。

SELECT o_orderpriority, COUNT(*)
FROM orders
WHERE o_orderdate >= '1993-07-01' AND
      EXISTS (SELECT *
              FROM lineitem
              WHERE l_orderkey = o_orderkey AND
                    l_returnflag = 'R')  OR
      EXISTS (SELECT *
              FROM lineitem
              WHERE l_orderkey = o_orderkey AND
                    l_receiptdate > l_commitdate)
GROUP BY o_orderpriority;
=>
SELECT o_orderpriority, COUNT(*)
FROM orders
WHERE o_orderdate >= '1993-07-01' AND
      EXISTS (SELECT *
              FROM lineitem
              WHERE l_orderkey = o_orderkey AND
                    (l_returnflag = 'R' OR
                     l_receiptdate > l_commitdate))
GROUP BY o_orderpriority;

如上例,将2个EXISTS中的subquery,”求同存异“,不同的部分predicate被OR了起来。

subquery具有不同类型

仍假设qb1包含qb2,qb2有更多的谓词条件p2,因此包含更少的结果数据(更严格)。

1.EXIST qb2 and not EXIST qb1

外表的一行,如果可以满足qb2的p2(更严格),则一定满足qb1,所以后者为false,总体为false。如果不能满足qb2,直接为false。因此总体一定为false。

2.EXIST qb1 and not EXIST qb2

外表的一行,如果在qb1中,存在可以满足qb1的p1的行,且能满足qb2的p2条件,则结果为false。如果存在可以满足qb1的p1的行,但这些行都不满足qb2的p2,则为true。

将以上2种情况结合在一起考虑的话,等价的结果是:

如果存在满足qb1中p1的行,但都不满足qb2中的p2,则可以为true,但其中有任一行满足p2,则为false。

这个等价的语义可以用如下合并来描述:

EXIST common_qb(where p1=true having sum(case when p2=true then 1 else 0) == 0)

<=>

只有当满足p1的所有行,都不满足p2,having才返回true,才能有结果,否则having过滤掉所有行!

SELECT s_name
FROM supplier, lineitem L1
WHERE s_suppkey = l_suppkey AND
      EXISTS (SELECT *
              FROM lineitem L2
              WHERE l_orderkey = L1.l_orderkey AND
                    l_suppkey <> L1.l_suppkery)
      AND NOT EXISTS (SELECT *
                      FROM lineitem L3
                      WHERE l_orderkey = L1.l_orderkey AND
                            l_suppkey <> L1.l_suppkery AND
                            l_receiptdate > l_commitdate);
=>
SELECT s_name
FROM supplier, lineitem L1
WHERE s_suppkey = l_suppkey AND
      EXISTS (SELECT 1
              FROM lineitem L2
              WHERE l_orderkey = L1.l_orderkey AND
                    l_suppkey <> L1.l_suppkery
              HAVING SUM(CASE WHEN l_receiptdate > l_commitdate
                         THEN 1 ELSE 0 END) == 0);

观察上例,第一个EXIST子查询qb1比第二个NOT EXIST子查询qb2只少一个条件p2: l_receiptdate > l_commitdate。基于上面的等价变换,将这个条件放入having中对整个子查询结果进行过滤,这样只有当所有满足qb1的行都不满足p2时,having才会为true,整个子查询才为true,否则为false。

与其他transformation的交互

和其他rewrite一样,subquery coalescing也会影响到其他的transformation,例如上例中的查询改写后,还可以做unnesting,转换为inline view(derived table)。

SELECT s_name
FROM supplier, lineitem L1
WHERE s_suppkey = l_suppkey AND
      EXISTS (SELECT 1
              FROM lineitem L2
              WHERE l_orderkey = L1.l_orderkey AND
                    l_suppkey <> L1.l_suppkery
              HAVING SUM(CASE WHEN l_receiptdate > l_commitdate
                         THEN 1 ELSE 0 END) == 0); 
=>
SELECT s_name
FROM supplier, lineitem L1,
     (SELECT LX.rowid as xrowid
      FROM lineitem L2, lineitem LX
      WHERE L2.l_orderkey = LX.l_orderkey AND
            L2.l_suppkey <> LX.l_suppkery
      GROUP BY LX.rowid
      HAVING SUM(CASE WHEN L2.l_receiptdate > L2.l_commitdate
                         THEN 1 ELSE 0 END) == 0) V
WHERE s_suppkey = l_suppkey AND L1.rowid = V.xrowid;

这个变换将外层的相关表L1推入内层子查询中变为LX,并需要加入group by子句来保证针对LX的每一行,计算一个聚集结果。

完成unnesting后,生成了derived table,因此可以进一步考虑view merging:

SELECT s_name
FROM supplier, lineitem L1,
     (SELECT LX.rowid as xrowid
      FROM lineitem L2, lineitem LX
      WHERE L2.l_orderkey = LX.l_orderkey AND
            L2.l_suppkey <> LX.l_suppkey
      GROUP BY LX.rowid
      HAVING SUM(CASE WHEN L2.l_receiptdate > L2.l_commitdate
                         THEN 1 ELSE 0 END) == 0) V
WHERE s_suppkey = l_suppkey AND L1.rowid = V.xrowid;
=>
SELECT s_name
FROM supplier, lineitem L1, lineitem L2
WHERE s_suppkey = L1.l_suppkey AND
      L2.l_orderkey = L1.l_orderkey AND
      L2.l_suppkey <> L1.l_suppkey
GROUP BY L1.rowid, supplier.rowid, s_name
HAVING SUM(CASE WHEN L2.l_receiptdate > L2.l_commitdate
                         THEN 1 ELSE 0 END) == 0);

将view merge到外层后,group by需要加入外层表的rowid,这里由于LX和L1是同一个表,self join可以去掉LX。

所有这些变换都有可能发生,需要基于cost决定采用其中哪些。

执行侧优化

这里介绍了一个对group by + aggregation的小优化,例如上面示例中的HAVING子句中,对一个分组来说,只要有一行满足了L2.l_receiptdate > L2.l_commitdate这个条件,having就会将这整个分组过滤掉,因此可以考虑在执行中一旦遇到这样的行,直接将整组扔掉,避免无谓的计算。类似的early stop策略对于SUM(xx) < 10,MIN(*) < 5 这样的过滤条件都适用。

这个策略在并行执行中也生效,关于Oracle的并行执行策略请看这篇文章。

简略来说,Oracle采用如下的并行方式:

image.png

先在join本地,针对数据做local aggregation,这样有利于对抗data skew,也可以减少后续传输的数据量,然后再在group by key上做repartition/range distribution,然后并行完成二次group by + aggregation,得到最终结果。那么可以把这种early stop的条件下压到local aggregation中,尽早丢弃掉不用的分组。

Group by view elimination

这里主要介绍了一种叫做filtering table elimination的技术,利用的是filtering join的思想。

定义filtering join: 如果是semi-join,或者等值inner join且一侧的join列是unique key,这样对于另一侧来说,实际上是起到了filtering的效果,该表的cardinality不会增加。

对于这种filtering join,如果用在AND谓词的后方,且前面有更为严格的筛选条件保证filtering一定为true,则filter join整个可以去除。

假设R是一个row source(derived table/base table),T1和T2是两个实例,但T1带有更严格的谓词条件p1,T2更宽松。unique key用前缀u来表示,semi join用S=来表示:

  • R.x = T1.uy AND R.x = T2.uy, 则后者可以删除

这个很好理解,对R.x,如果满足 == T1.uy,则一定满足== T2.uy,因为前者数据是后者的子集。

  • R.x = T1.y AND R.x S= T2.y ,则后者可以删除

R.x 与T1中y列如果有相同值,由于T1是T2的子集,则R.x一定与T2中y列有相同值,后面子条件一定为true。

  • R.x S= T1.y AND R.x S= T2.y,后者可以删除

与上面是一个道理

SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, lineitem L1, customers
WHERE o_orderkey = l_orderkey AND
      c_custkey = o_custkey AND
      o_orderkey IN
      (SELECT l_orderkey
       FROM lineitem l2
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30)
GROUP BY o_orderkey, c_custkey;
=>
SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, lineitem L1, customers
     (SELECT l_orderkey
       FROM lineitem l2
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30) V2
WHERE o_orderkey = l_orderkey AND
      c_custkey = o_custkey AND
      o_orderkey = V2.l_orderkey
GROUP BY o_orderkey, c_custkey;

上面原始查询先做了一次subquery unnesting,生成derived table V2

通过group by placement变形,将V2中的group by pullup,再推入到L1中,形成另一个view

SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, lineitem L1, customers
     (SELECT l_orderkey
       FROM lineitem L2
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30) V2
WHERE o_orderkey = l_orderkey AND
      c_custkey = o_custkey AND
      o_orderkey = V2.l_orderkey
GROUP BY o_orderkey, c_custkey;
=>
SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, customers,
     (SELECT l_orderkey, SUM(l_quantity)
       FROM lineitem L1
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30) V2,
      (SELECT l_orderkey, SUM(l_quantity)
       FROM lineitem L1
       GROUP BY l_orderkey) V1
WHERE o_orderkey = V1.l_orderkey AND
      o_orderkey = V2.l_orderkey
      c_custkey = o_custkey
GROUP BY o_orderkey, c_custkey;

经过这些变换后可以看到,V1和V2是两个基本相同的view,只是V2包含更为严格的过滤条件,此外由于V1/V2与order表的join条件都是基于其group by列l_orderkey的,等于在V1/V2中,l_orderkey具有唯一性,这两个join都是filtering join!!

因此可以利用前面讲到的消除机制1,将更为宽松的view V1消除掉!得到如下query:

SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, customers,
     (SELECT l_orderkey, SUM(l_quantity)
       FROM lineitem L1
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30) V2,
      (SELECT l_orderkey, SUM(l_quantity)
       FROM lineitem L1
       GROUP BY l_orderkey) V1
WHERE o_orderkey = V1.l_orderkey AND
      o_orderkey = V2.l_orderkey
      c_custkey = o_custkey
GROUP BY o_orderkey, c_custkey;
=>
SELECT o_orderkey, c_custkey, SUM(l_quantity)
FROM orders, customers,
     (SELECT l_orderkey, SUM(l_quantity)
       FROM lineitem L1
       GROUP BY l_orderkey
       HAVING sum(l_quantity) > 30) V2
WHERE o_orderkey = V2.l_orderkey
      c_custkey = o_custkey
GROUP BY o_orderkey, c_custkey;

通过这个例子,详细大家已经感受到了transformation之间相互作用所带来的神奇作用和复杂性,这在MySQL甚至PostgreSQL这样的开源数据库系统中是很难想象的。

Subquery Removal using window function

相关子查询

针对相关子查询进行remove,使用的思路和DB2的WinMagic完全一致,大致来说就是:

如果外层qb,包含有内层qb的所有table + predicates,则外层qb subsume内层qb,只有满足subsume条件的subquery(相关/非相关),才能用window function替换,且subquery中有数值型聚集函数(min/max/sum...),这些聚集函数有window function的等价函数。

相关子查询的通用形式:

SELECT T1.x
FROM T1, T2 
WHERE T1.y = T2.y
AND T1.z relop (
    select AGG(T2.w)
    FROM T2
    WHERE T1.y = T2.y
  );

这里T1,T2可以是derived table/base table/多个表join,因此可以表示很复杂的查询。

T1.y是主键,T2.y是外键

这是相关子查询可以用window function替代的通用pattern : 外层的join条件和内层的相关条件相同,且内层有aggregation函数,外层用这个aggr函数结果做过滤。

原理基于相关子查询的执行语义:

对于T1的一行T1.y,可以和T2.y join产生rowset1,这个rowset1,和内层代入T1.y产生的rowset是完全相同的(subsume的作用),在内层这个rowset1用来做aggr后得到X,对外层的T1.y做过滤。所以可以用wf等价为,对T1的每一行,用相关列T2.y做partition列,在partition内做aggregation得到相同的X,附在每一行T1.y后面,基于这个结果对每行过滤即可。

SELECT V.x
FROM (T1.x, T1.z SELECT AGG(T2.w) over (PARTITION BY T2.y) as agg_win
    FROM T1, T2
    WHERE T1.y = T2.y) V
WHERE V.z relop V.agg_win;

如果T1.y不是主键,也没关系,从语义上,只要PARTITION BY T1.pkey, T2.y,就可以了。

这里讲的有些粗略,具体细节请看这篇文章。

非相关子查询

对于具有subsumed关系的非相关子查询,处理起来会更简单些。直接看例子

WITH V AS (SELECT l_suppkey,
           SUM(l_extprice) revenue
           FROM lineitem
           WHERE l_shipdate >= '1996-01-01'
           GROUP BY l_suppkey)
SELECT s_suppkey, s_name, V.revenue
FROM supplier, V
WHERE s_suppkey = V.l_suppkey AND
      V.revenue = (SELECT MAX(V.revenue) FROM V);

上面这个SQL是TPC-H Q15的简化版,观察一下其语义:

在supplier join V时,除了在suppkey的join条件外,还有个使用非相关子查询的过滤,过滤的条件是V的revenue要等于V中revenue列的最大值。

这就很容易想到,如果直接能获取到V中revenue列的最大值,作为V的额外一列附加上,这个过滤条件就只针对这列就可以了,因此可以变成如下形式:

SELECT s_suppkey, s_name, V.revenue
FROM supplier,
     (SELECT l_suppkey,
           SUM(l_extprice) revenue,
           MAX(SUM(l_extprice)) OVER() gt_rev
           FROM lineitem
           WHERE l_shipdate >= '1996-01-01'
           GROUP BY l_suppkey) V
WHERE s_suppkey = V.l_suppkey AND
      V.revenue = V.gt_rev;

这样就把非相关子查询消除了,使用了OVER()这样的window function,在Oracle中称为grand-total wf,即所有行作为一个partition,计算一个统一的结果(这里是max值)。

扩展性的parallel execution

由于window function在subquery消除中的重要性,Oracle对window function的并行执行进行了很多优化。

原始的执行方式是基于partition by key做hash distribution,然后对每个partition并行完成window function的计算。但这种方法的并行度受限于partition by key的NDV,对于grand-total这种wf则完全没法并行,因此提出了扩展性方式。

对于上例中的最终query形态,其并行执行方式为:

image.png

  1. 先是在各个并行worker上,针对部分数据,做一个local的partion内聚集计算,得到结果后,把各个partition的local result,形成数组,发送到QC端
  2. QC端只做汇总操作,计算各个partition的global aggregation result,再形成partition的数组,带着global result返回给各个worker
  3. 各个worker将收到的数组和本地每行数据,按parition做一个sort-merge join,把global结果附加到了每行数据上,再发送给QC
  4. QC收到数据按partition排好后发送即可

这样大部分的工作是在worker完成的,coordinator只做很少的聚集计算因为每个worker只会发送本地聚集的一行结果。

对于非grand-total的window function,也有办法扩展其并行度,思路与上面类似,但需要在worker和coordinator间交互更多信息,具体细节就不详述了,可以参看文章。

NULL-aware anti join(NAJJ)

众所周知SQL中对于NULL的处理是存在3值语义的:

TRUE/FALSE/UNKNOWN

任何value与NULL的计算都会是UNKNOWN结果,除非计算表达式是IS NULL / IS NOT NULL。而对于UNKNOWN的处理,不同系统有所不同,但肯定不会是TRUE。 

对于NOT IN subquery本质上存在如下等价形式:

NOT IN 
<=> NOT (o1 = inner.row1.i1 or o1 = inner.row2.i2 or o1 = inner.row3.i2 ...) 
<=> (o1 != inner.row1.i2 and o1 != inner.row2.i2 and o1 = inner.row3.i3 ...) 
注:这里ix只是表示第几行的相关列,都是同一列i

如果在相关列o1/i1上存在NULL值,则要特殊处理:

对外层的一行o1,这时如果内表中任一行ix是NULL,条件将是UNKNOWN。而antijoin的语义是,对外表一行,如果内表所有行,在join条件上都是FALSE,该行才能保留,现在join条件是UNKNOWN,则外表所有行都不能保留,结果是空。

而普通的anti-join没有处理内层有NULL值的情况(Oracle 10g),需引入NAAJ(Oracle 11g),其处理流程如下:

if (内表为空) {
  // condition 1
  外表所有行保留
} else if (内表任一行,在join列上有NULL值) {
  // condition 2
  结果为空(如上分析)
} else if (外表当前行在join列上是null值) {
  // condition 3
  当前行丢弃(同样道理,只不过外表一行为NULL)
} else if (外表当前行,对内表所有行,join条件都为false) {
  // condition 4
  当前行保留
} else {
  // default
  当前行丢弃
}

条件4从语义上包含了条件2、3,但2,3可以用来做快速计算。

总结

本篇基本详尽的描述了Oracle对于subquery的主要变换机制,还是非常复杂的,尤其涉及到transformation的相互影响时。这篇paper是我到阿里工作后最早接触的优化器论文之一,当时感觉相当艰深难以理解。

从paper中可以感受到,商业数据库在优化器的能力上要领先一般的开源数据库几个档次,即使是号称最先进的PostgreSQL也相差甚远。所以说,query optimizer才是商业数据库的宝贵财富,是他们多年沉淀与积累的核心竞争力。而作为PolarDB内核优化器的开发人员,我们还有很长的路要走。。。

目录
相关文章
clion中cpp文件显示This file does not belong to any project ,code insight features might not work【解决方案】
clion中cpp文件显示This file does not belong to any project ,code insight features might not work【解决方案】
clion中cpp文件显示This file does not belong to any project ,code insight features might not work【解决方案】
|
存储 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:单机性能优化篇
阿里云PolarDB云原生数据库在TPC-C基准测试中,以20.55亿tpmC的成绩打破性能与性价比世界纪录。此外,国产轻量版PolarDB已上线,提供更具性价比的选择。
|
2月前
|
人工智能 缓存 自然语言处理
Kimi K3 上线千问AI平台,支持长程编程、知识工作与复杂推理
Kimi K3 现已上线千问AI平台。模型拥有 2.8 万亿参数、原生多模态能力和 100 万 token 上下文窗口,面向长程编程、智能知识工作与复杂推理场景。
402 0
Kimi K3 上线千问AI平台,支持长程编程、知识工作与复杂推理
|
JSON JavaScript 前端开发
初识别三层架构
本文介绍了三层架构在Web开发中的工作流程,从前端请求到后端处理,再到数据返回与页面渲染,详细解析了客户端、Controller、Service、Dao各层之间的调用关系及前后端交互过程。
|
SQL 关系型数据库 PostgreSQL
CTE vs 子查询:深入拆解PostgreSQL复杂SQL的隐藏性能差异
本文深入探讨了PostgreSQL中CTE(公共表表达式)与子查询的选择对SQL性能的影响。通过分析两者底层机制,揭示CTE的物化特性及子查询的优化融合优势,并结合多场景案例对比执行效率。最终给出决策指南,帮助开发者根据数据量、引用次数和复杂度选择最优方案,同时提供高级优化技巧和版本演进建议,助力SQL性能调优。
|
SQL 自然语言处理 数据库
DAIL-SQL: 发掘LLM的NL2SQL能力
最近,DAIL-SQL在魔搭创空间上线,并在NL2SQL任务上取得了新的SOTA。DAIL-SQL可以更好地利用LLM的NL2SQL能力,本文对其进行详细解读。
|
Oracle 关系型数据库 MySQL
一文了解PostgreSQL MVCC机制
一文了解PostgreSQL MVCC机制
1444 0
|
编解码 算法 计算机视觉
HSV
HSV
1162 4
|
存储 负载均衡 算法
TiKV简介
【2月更文挑战第27天】本章节旨在为读者提供一个关于TiKV存储引擎的初步认识,包括其基本概念、产生背景、主要特性以及在分布式存储领域中的应用。通过本章节的介绍,读者将能够对TiKV有一个整体的了解,为后续深入学习其存储原理和数据模型奠定基础。
1049 0
|
存储 关系型数据库 Go
深入理解 PostgreSQL 中的 MVCC(多版本并发控制)机制
深入理解 PostgreSQL 中的 MVCC(多版本并发控制)机制
969 0