开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十三):DML之INSERTIGNORE流程

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十三):DML之INSERTIGNORE流程

作者:潜璟


在上一篇源码阅读中,我们介绍了INSERT的执行流程。而INSERT IGNORE与INSERT不同,需要对插入值判断是否有Unique Key的冲突,并忽略有冲突的插入值。因此本文将进一步介绍PolarDB-X中INSERT IGNORE的执行流程,其根据插入的表是否有GSI也有所变化。  


一、下推执行


如果插入的表只有一张主表,没有GSI,那么只需要将INSERT IGNORE直接发送到对应的物理表上,由DN自行忽略存在冲突的值。在这种情况下,INSERT IGNORE的执行过程和INSERT基本上相同,读者可以参考之前的源码阅读文章。


二、逻辑执行


而在有GSI的情况下,就不能简单地将INSERT IGNORE分别下发到主表和GSI对应的物理分表上,否则有可能出现主表和GSI数据不一致的情况。举个例子:  


create table t1 (a int primary key, b int, global index g1(b) dbpartition by hash(b)) dbpartition by hash(a);
 insert ignore into t1 values (1,1),(1,2);

对于插入的两条记录,它们在主表上位于同一个物理表(a相同),但是在GSI上位于不同的物理表(b不相同),如果直接下发INSERT IGNORE的话,主表上只有(1,1)能够成功插入(主键冲突),而在GSI上(1,1)和(1,2)都能成功插入,于是GSI比主表多了一条数据。


针对这种情况,一种解决方案是根据插入值中的Unique Key,先到数据库中SELECT出有可能冲突的数据到CN,然后在CN判断冲突的值并删除。  


进行SELECT的时候,最简单的方式就是将所有的SELECT直接发送到主表上,但是主表上可能没有对应的Unique Key,这就导致SELECT的时候会进行全表扫描,影响性能。所以在优化器阶段,我们会根据Unique Key是在主表还是GSI上定义的来确定相应的SELECT需要发送到主表还是GSI,具体代码位置:


com.alibaba.polardbx.optimizer.core.planner.rule.OptimizeLogicalInsertRule#groupUkByTable  


protected Map>> 
groupUkByTable(LogicalInsertIgnore insertIgnore, 
                                                                                                                                      ExecutionContext 
 executionContext) { 
                                                // 找到每个 Unique Key 在主表和哪些 GSI 中存在           
                                     Map> ukAllTableMap = new HashMap<>();       
                                                           for (int i = 0; i < uniqueKeys.size(); i++) {             
                                                        List uniqueKey = uniqueKeys.get(i);            
                                                    for (Map.Entry>> e : writableTableUkMap.entrySet()) { 
                                                     String currentTableName = e.getKey().toUpperCase();
                                                          Map> currentUniqueKeys = 
e.getValue(); 
                                                                   boolean found = false; 
                                                             for (Set currentUniqueKey : 
currentUniqueKeys.values()) { 
                                                if (currentUniqueKey.size() != uniqueKey.size()) {
                                                    continue;          
                                                 }
                                                 boolean match = 
currentUniqueKey.containsAll(uniqueKey); 
                                                  if (match) { 
                                           found = true; 
                                           break; 
                                         } 
                                    } 
                                    if (found) { 
                                     ukAllTableMap.computeIfAbsent(i, k -> new 
ArrayList<>()).add(currentTableName); 
                                     } 
                                } 
                            } 
    // 确定是在哪一个表上进行 SELECT 
            for (Map.Entry> e : 
  ukAllTableMap.entrySet()) { 
                      List tableNames = e.getValue(); 
                 if (tableNames.contains(primaryTableName.toUpperCase())) { 
tableUkMap.computeIfAbsent(primaryTableName.toUpperCase(), k -> new ArrayList<>())
                               .add(uniqueKeys.get(e.getKey())); 
                 } else { 
                            final boolean onlyNonPublicGsi = 
                                     tableNames.stream().noneMatch(tn -> 
GlobalIndexMeta.isPublished(executionContext, sm.getTable(tn))); 
                                boolean found = false; 
                            for (String tableName : tableNames) { 
                            if (!onlyNonPublicGsi && 
GlobalIndexMeta.isPublished(executionContext, 
sm.getTable(tableName))) {
                                                                             tableUkMap.computeIfAbsent(tableName, k -> new
ArrayList<>()).add(uniqueKeys.get(e.getKey()));
                                               found = true; 
                                               break; 
                                       } else if (onlyNonPublicGsi && 
GlobalIndexMeta.canWrite(executionContext, sm.getTable(tableName)))
{
                                                                            tableUkMap.computeIfAbsent(tableName, k -> new 
ArrayList<>()).add(uniqueKeys.get(e.getKey())); 
                                                 found = true; 
                                                 break; 
                                              }
                                         }
                                   }
                                } 
        return tableUkMap; 
    }


而到了执行阶段,我们在LogicalInsertIgnoreHandler中处理INSERT IGNORE。我们首先会进入getDuplicatedValues函数,其通过下发SELECT的方式查找表中已有的冲突的Unique Key的记录。我们将下发的SELECT语句中选择的列设置为(value_index, uk_index, pk)。其中value_index和uk_index均为的常量。  


举个例子,假设有表:


CREATE TABLE `t` ( 
    `id` int(11) NOT NULL, 
    `a` int(11) NOT NULL, 
    `b` int(11) NOT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE GLOBAL KEY `g_i_a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`) 
) DBPARTITION BY HASH(`id`)

以及一条INSERT IGNORE语句:


INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);


假设在PolarDB-X中执行时,其会将Unique Key编号为:


0: id 
1: g_i_a


INSERT IGNORE语句中插入的每个值分别编号为:


0: (1,2,3) 
1: (2,3,4) 
2: (3,4,5)


那么对于(2,3,4)的UNIQUE KEY构造的GSI上的SELECT即为:


# 查询 GSI 
SELECT 1 as `value_index`, 1 as `uk_index`, `id` 
FROM `g_i_a_xxxx` 
WHERE `a` in 3;


假设表中已经存在(5,3,6),那么这条SELECT的返回结果即为(1,1,5)。此外,由于不同的Unique Key的SELECT返回格式是相同的,所以我们会将同一个物理库上不同的SELECT查询UNION起来发送,以一次性得到多个结果,减少CN和DN之间的交互次数。只要某个Unique Key有重复值,我们就能根据value_index和uk_index确定是插入值的哪一行的哪个Unique Key是重复的。


当得到所有的返回结果之后,我们对数据进行去重。我们将上一步得到的冲突的的值放入一个SET中,然后顺序扫描所有的每一行插入值,如果发现有重复的就跳过该行,否则就将该行也加入到SET中(因为插入值之间也有可能存在相互冲突)。去重完毕之后,我们就得到了所有不存在冲突的值,将这些值插入到表中之后就完成了一条INSERT IGNORE的执行。


逻辑执行的执行流程:


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute
protected int doExecute(LogicalInsert insert, ExecutionContext executionContext, 
                            LogicalInsert.HandlerParams handlerParams) { 
        // ... 
        try { 
            Map>> ukGroupByTable = 
insertIgnore.getUkGroupByTable(); 
            List> deduplicated; 
            List> duplicateValues; 
            // 获取表中已有的 Unique Key 冲突值 
            duplicateValues = getDuplicatedValues(insertIgnore, 
LockMode.SHARED_LOCK, executionContext, ukGroupByTable,
                (rowCount) -> memoryAllocator.allocateReservedMemory( 
                    MemoryEstimator.calcSelectValuesMemCost(rowCount, 
selectRowType)), selectRowType, true,
                handlerParams); 
            final List> batchParameters
                executionContext.getParams().getBatchParameters(); 
            // 根据上一步得到的结果,去掉 INSERT IGNORE 中的冲突值 
            deduplicated = 
getDeduplicatedParams(insertIgnore.getUkColumnMetas(), 
insertIgnore.getBeforeUkMapping(),
                insertIgnore.getAfterUkMapping(),
RelUtils.getRelInput(insertIgnore), duplicateValues,
                batchParameters, executionContext); 
           if (!deduplicated.isEmpty()) { 
                insertEc.setParams(new Parameters(deduplicated)); 
rams(new Parameters(deduplicated)); 
            } else { 
                // All duplicated 
                return affectRows; 
            } 
            // 执行 INSERT 
            try { 
                if (gsiConcurrentWrite) { 
                    affectRows = concurrentExecute(insertIgnore,
insertEc);
                } else { 
                    affectRows = sequentialExecute(insertIgnore, 
insertEc);
                } else { 
                    affectRows = sequentialExecute(insertIgnore,
insertEc);
                } 
            } catch (Throwable e) { 
                handleException(executionContext, e, 
GeneralUtil.isNotEmpty(insertIgnore.getGsiInsertWriters()));
            } 
        } finally { 
            selectValuesPool.destroy(); 
        } 
        return affectRows; 
    }


三、RETURNING优化


上一节提到的INSERT IGNORE的逻辑执行方式,虽然保证了数据的正确性,但是也使得一条INSERT IGNORE语句至少需要CN和DN的两次交互才能完成(第一次SELECT,第二次INSERT),影响了INSERT IGNORE的执行性能。  


目前的DN已经支持了AliSQL的RETURNING优化,其可以在DN的INSERT IGNORE执行完毕之后返回成功插入的值。利用这一功能,PolarDB-X对INSERT IGNORE进行了进一步的优化:直接将INSERT IGNORE下发,如果在主表和GSI上全部成功返回,那么就说明插入值中没有冲突,于是就成功完成该条INSERT IGNORE的执行;否则就将多插入的值删除。


执行时,CN首先会根据上文中的语法下发带有RETURNING的物理INSERT IGNORE语句到DN,比如:


call dbms_trans.returning("a", "insert into t1_xxxx values(1,1)");


其中返回列是主键,用来标识插入的一批数据中哪些被成功插入了;t1_xxxx是逻辑表t1的一个物理分表。当主表和GSI上的所有INSERT IGNORE执行完毕之后,我们计算主表和GSI中成功插入值的交集作为最后的结果,然后删除多插入的值。这部分代码在


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#getRowsToBeRemoved


private Map>> getRowsToBeRemoved(String tableName, 
                                                                                                                    Map
List>> tableInsertedValues, 
                                                                                                                   List 
beforePkMapping,
List pkColumnMetas) { 
         final Map> tableInsertedPks = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        final Map>>> 
tablePkRows =
            new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 
        tableInsertedValues.forEach((tn, insertedValues) -> { 
            final Set insertedPks = new TreeSet<>(); 
            final List>> pkRows = new 
ArrayList<>();
            for (List inserted : insertedValues) { 
                final Object[] groupKeys =
beforePkMapping.stream().map(inserted::get).toArray();
                final GroupKey pk = new GroupKey(groupKeys,
pkColumnMetas);
                insertedPks.add(pk); 
                pkRows.add(Pair.of(pk, inserted)); 
            } 
            tableInsertedPks.put(tn, insertedPks); 
            tablePkRows.put(tn, pkRows); 
        }); 
        // Get intersect of inserted values 
        final Set distinctPks = new TreeSet<>();
        for (GroupKey pk : tableInsertedPks.get(tableName)) { 
            if (tableInsertedPks.values().stream().allMatch(pks -> 
pks.contains(pk))) { 
                distinctPks.add(pk); 
            }
         } 
        // Remove values which not exists in at least one insert results 
        final Map>> tableDeletePks = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER); 
        tablePkRows.forEach((tn, pkRows) -> { 
            final List> deletePks = new ArrayList<>(); 
            pkRows.forEach(pkRow -> { 
                if (!distinctPks.contains(pkRow.getKey())) { 
                    deletePks.add(pkRow.getValue()); 
                }
             }); 
            if (!deletePks.isEmpty()) {
                 tableDeletePks.put(tn, deletePks); 
            }
         }); 
        return tableDeletePks;
     }

与上一节的逻辑执行的“悲观执行”相比,使用RETURNING优化的INSERT IGNORE相当于“乐观执行”,如果插入的值本身没有冲突,那么一条INSERT IGNORE语句CN和DN间只需要一次交互即可;而在有冲突的情况下,我们需要下发DELETE语句将主表或GSI中多插入的值删除,于是CN和DN间需要两次交互。可以看出,即便是有冲突的情况,CN和DN间的交互次数也不会超过上一节的逻辑执行。因此在无法直接下推的情况下,INSERT IGNORE的执行策略是默认使用RETURNING优化执行。


当然RETURNING优化的使用也有一些限制,比如插入的Value有重复主键时就不能使用,因为这种情况下无法判断具体是哪一行被成功插入,哪一行需要删除;具体可以阅读代码中的条件判断。当不能使用RETURNING优化时,系统会自动选择上一节中的逻辑执行方式执行该条INSERT IGNORE语句以保证数据的正确性。


使用RETURNING优化的执行流程:


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute


protected int doExecute(LogicalInsert insert, ExecutionContext executionContext, 
                            LogicalInsert.HandlerParams handlerParams) { 
         // ... 
        // 判断能否使用 RETURNING 优化
         boolean canUseReturning = 
executorContext.getStorageInfoManager().supportsReturning() && executionContext.getParamManager() 
                .getBoolean(ConnectionParams.DML_USE_RETURNING) && 
allDnUseXDataSource && gsiCanUseReturning
                 && !isBroadcast 
&& !ComplexTaskPlanUtils.canWrite(tableMeta); 
        if (canUseReturning) {
             canUseReturning = noDuplicateValues(insertIgnore, insertEc);
         } 
if (canUseReturning) {
            // 执行 INSERT IGNORE 并获得返回结果 
            final List allPhyPlan = 
                new ArrayList<>(replaceSeqAndBuildPhyPlan(insertIgnore, 
insertEc, handlerParams)); 
getPhysicalPlanForGsi(insertIgnore.getGsiInsertIgnoreWriters(),
 insertEc, allPhyPlan); 
            final Map>> tableInsertedValues =
                 executeAndGetReturning(executionContext, allPhyPlan,
 insertIgnore, insertEc, memoryAllocator,
                     selectRowType); 
            // ... 
           // 生成 DELETE
             final boolean removeAllInserted =
                 targetTableNames.stream().anyMatch(tn
 -> !tableInsertedValues.containsKey(tn)); 
            if (removeAllInserted) {
                 affectedRows -=
                     removeInserted(insertIgnore, schemaName, tableName,
 isBroadcast, insertEc, tableInsertedValues);
                 if (returnIgnored) {
                    ignoredRows = totalRows; 
                }
             } else {
                 final List beforePkMapping =
 insertIgnore.getBeforePkMapping();
                 final List pkColumnMetas =
 insertIgnore.getPkColumnMetas(); 
                // 计算所有插入值的交集
                 final Map>> tableDeletePks =
                     getRowsToBeRemoved(tableName, tableInsertedValues,
 beforePkMapping, pkColumnMetas); 
                affectedRows -=
                     removeInserted(insertIgnore, schemaName, tableName,
 isBroadcast, insertEc, tableDeletePks);
                 if (returnIgnored) {
                     ignoredRows += 
Optional.ofNullable(tableDeletePks.get(insertIgnore.getLogicalTable
Name())).map(List::size)
                             .orElse(0);
                 }
              }
              handlerParams.optimizedWithReturning = true;
              if (returnIgnored) {
                 return ignoredRows;
             } else {
                 return affectedRows;
             }
         } else {             
handlerParams.optimizedWithReturning = false;
         } 
      // ...
      }

最后以一个例子来展现RETURNING优化的执行流程与逻辑执行的不同。通过/*+TDDL:CMD_EXTRA(DML_USE_RETURNING=TRUE)*/这条HINT,用户可以手动控制是否使RETURNING优化。


首先建表并插入一条数据:


CREATE TABLE `t` (
     `id` int(11) NOT NULL,
     `a` int(11) NOT NULL,
     `b` int(11) NOT NULL,
     PRIMARY KEY (`id`),
     UNIQUE GLOBAL KEY `g_i_a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`)
 ) DBPARTITION BY HASH(`id`);
  INSERT INTO t VALUES (1,3,3);


再执行一条INSERT IGNORE:


INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);


其中(1,2,3)与(1,3,3)主键冲突,(2,3,4)与(1,3,3)对于Unique Key g_i_a冲突。如果是RETURNING优化:


         

                                                       截屏2022-08-08 10.16.25


可以看到PolarDB-X先进行了INSERT IGNORE,再将多插入的数据删除:(1,2,3)在主表上冲突在UGSI上成功插入,(2,3,4)在UGSI上冲突在主表上成功插入,因此分别下发对应的DELETE到UGSI和主表上。


如果关闭RETURNING优化,逻辑执行:


         

                                                    截屏2022-08-08 10.18.07


可以看到PolarDB-X先进行了SELECT,再将没有冲突的数据(3,4,5)插入。


四、小结


本文介绍了PolarDB-X中INSERT IGNORE的执行流程。除了INSERT IGNORE之外,还有一些DML语句在执行时也需要进行重复值的判断,比如REPLACE、INSERT ON DUPLICATE KEY UPDATE等,这些语句在有GSI的情况下均采用了逻辑执行的方式,即先进行SELECT再进行判重、更新等操作,感兴趣的读者可以自行阅读相关代码。


相关实践学习
体验PolarDB-X透明分布式
PolarDB-X具备从单机到分布式的平滑演进能力,支持通过DDL将一张大表动态调整为分布式的分区表,结合分布式事务、以及兼容MySQL Binlog的数据回流,可完成单机到分布式的快速改造。本体验从PolarDB-X的Auto Partition,以及兼容MySQL Binlog的CDC能力两个方面演示透明分布式能力。
相关文章
|
2月前
|
关系型数据库 分布式数据库 数据库
|
2月前
|
存储 关系型数据库 分布式数据库
喜报|阿里云PolarDB数据库(分布式版)荣获国内首台(套)产品奖项
阿里云PolarDB数据库管理软件(分布式版)荣获「2024年度国内首版次软件」称号,并跻身《2024年度浙江省首台(套)推广应用典型案例》。
|
4月前
|
关系型数据库 数据库 RDS
【瑶池数据库训练营及解决方案本周精选(探索PolarDB,参与RDS迁移、连接训练营)】(5.30-6.8)
本周精选聚焦数据库迁移训练营、快速连接云数据库RDS训练营及智能多模态搜索解决方案。为用户提供模拟教程与实战演练,学习RDS MySQL实例连接与数据管理技能,助力企业智能化发展。每周解锁数据库实战新场景,抓紧时间,精彩不容错过!
|
3月前
|
关系型数据库 分布式数据库 数据库
再获殊荣,阿里云PolarDB数据库蝉联SIGMOD最佳论文奖
内存池化技术新突破,阿里云PolarDB蝉联SIGMOD最佳论文奖
|
4月前
|
Cloud Native 关系型数据库 分布式数据库
阿里云PolarDB与沃趣科技携手打造一体化数据库解决方案,助推国产数据库生态发展
阿里云瑶池数据库与沃趣科技将继续深化合作,共同推动国产数据库技术的持续创新与广泛应用,为行业生态的繁荣注入更强劲的技术动力。
阿里云PolarDB与沃趣科技携手打造一体化数据库解决方案,助推国产数据库生态发展
|
2月前
|
关系型数据库 分布式数据库 数据库
阿里云PolarDB数据库蝉联SIGMOD最佳论文奖
阿里云PolarDB凭借全球首创基于CXL Switch的分布式内存池技术,在SIGMOD 2025上荣获工业赛道“最佳论文奖”,连续两年蝉联该顶会最高奖项。其创新架构PolarCXLMem打破传统RDMA技术瓶颈,性能提升2.1倍,并已落地应用于内存池化场景,推动大模型推理与多模态存储发展,展现CXL Switch在高速互联中的巨大潜力。
阿里云PolarDB数据库蝉联SIGMOD最佳论文奖
|
3月前
|
存储 监控 关系型数据库
突破IO瓶颈:PolarDB分布式并行查询(Parallel Query)深度调优手册
在海量数据处理中,I/O瓶颈严重制约数据库性能。本文基于PolarDB MySQL 8.0.32版本,深入解析分布式并行查询技术如何提升CPU利用率至86.7%、IO吞吐达8.5GB/s,并结合20+实战案例,系统讲解并行架构、执行计划优化、资源调优与故障排查方法,助力实现高性能数据分析。
103 6
|
3月前
|
Cloud Native 关系型数据库 分布式数据库
客户说|知乎基于阿里云PolarDB,实现最大数据库集群云原生升级
近日,知乎最大的风控业务数据库集群,基于阿里云瑶池数据库完成了云原生技术架构的升级。此次升级不仅显著提升了系统的高可用性和性能上限,还大幅降低了底层资源成本。
|
4月前
|
存储 Cloud Native 关系型数据库
PolarDB开源:云原生数据库的架构革命
本文围绕开源核心价值、社区运营实践和技术演进路线展开。首先解读存算分离架构的三大突破,包括基于RDMA的分布式存储、计算节点扩展及存储池扩容机制,并强调与MySQL的高兼容性。其次分享阿里巴巴开源治理模式,涵盖技术决策、版本发布和贡献者成长体系,同时展示企业应用案例。最后展望技术路线图,如3.0版本的多写多读架构、智能调优引擎等特性,以及开发者生态建设举措,推荐使用PolarDB-Operator实现高效部署。
234 3
|
4月前
|
SQL 关系型数据库 分布式数据库
PolarDB开源数据库入门教程
PolarDB是阿里云推出的云原生数据库,基于PostgreSQL、MySQL和Oracle引擎构建,具备高性能、高扩展性和高可用性。其开源版采用计算与存储分离架构,支持快速弹性扩展和100%兼容PostgreSQL/MySQL。本文介绍了PolarDB的安装方法(Docker部署或源码编译)、基本使用(连接数据库、创建表等)及高级特性(计算节点扩展、存储自动扩容、并行查询等)。同时提供了性能优化建议和监控维护方法,帮助用户在生产环境中高效使用PolarDB。
1394 21

相关产品

  • 云原生数据库 PolarDB