h2database源码解析-查询优化器原理

2023-11-06

一、成本计算规则

h2的查询优化器基于成本的,因此在执行查询前,会基于成本计算使用哪个索引,如果涉及多表关联,还会计算不同表关联顺序的成本,最终基于最小成本得出执行计划。
单表查询时,遍历表的各个索引,索引遍历的顺序是先主键索引,然后二级索引。h2先假设使用该索引,然后基于该索引计算成本,索引不同,成本的计算规则也不同。
单主键索引的成本计算规则是最简单的,规则为:10*(总记录数+1000) 。但是如果是二级索引或者是主键有多个字段,那计算规则就比较复杂了。在Index.getCostRangeIndex()里面具体描述了成本计算规则,只要是基于索引计算成本(元数据表除外),那么都会调用下面这个方法,该方法的返回值乘以10,就是基于该索引进行查询所需成本:

	//masks:数组中的元素与表的字段一一对应,比如数组第一个元素对应表的第一个字段,依以此类推,数组中的元素值表示该字段在查询语句中的比较类型,比如等值比较,元素值为1,比如大于等于,元素值为2,如果该字段在多个查询条件里面出现,比如既有大于等于也有等于操作,那么元素值为3(计算方式是1|2)
	//rowCount:表示表里面行记录总数
	//filters:每个TableFilter代表一个表
	//filter:如果多表关联查询,该参数表示正在处理filters[filter]
	//sortOrder:是否有order by子句
	//allColumnsSet:记录了本次查询涉及到的所有表的所有字段
	//isScanIndex:当前是否是表扫描,主键索引属于表扫描索引,二级索引不属于
    protected final long getCostRangeIndex(int[] masks, long rowCount, TableFilter[] filters, int filter,
            SortOrder sortOrder, boolean isScanIndex, AllColumnsForPlan allColumnsSet) {
        //成本是按行数+此偏移量计算的,以避免在表中不包含记录时使用错误索引
        rowCount += Constants.COST_ROW_OFFSET;
        int totalSelectivity = 0;
        long rowsCost = rowCount;
        if (masks != null) {
            int i = 0, len = columns.length;
            boolean tryAdditional = false;
            //从该索引的第一个字段开始遍历
            while (i < len) {
                Column column = columns[i++];//按顺序获取索引的每个字段
                int index = column.getColumnId();//获取字段在表里面的顺序
                int mask = masks[index];//获取该字段在查询语句里面的比较类型
                if ((mask & IndexCondition.EQUALITY) == IndexCondition.EQUALITY) {
                    //使用到了索引中的一个字段,并且是等值操作
                    if (i > 0 && i == uniqueColumnColumn) {
                    	//该索引是唯一索引,且遍历完索引的所有字段后发现查询语句中对该索引字段都是使用了等值操作
                    	//可见使用唯一索引的成本非常低
                        rowsCost = 3;
                        break;
                    }
                    //下面计算公式依赖于该字段的区分度(getSelectivity),区分度可以根据表优化或者重建重新计算
                    totalSelectivity = 100 - ((100 - totalSelectivity) *
                            (100 - column.getSelectivity()) / 100);
                    long distinctRows = rowCount * totalSelectivity / 100;
                    if (distinctRows <= 0) {
                        distinctRows = 1;
                    }
                    rowsCost = 2 + Math.max(rowCount / distinctRows, 1);
                } else if ((mask & IndexCondition.RANGE) == IndexCondition.RANGE) {
                	//当前字段是范围比较操作,比如between
                    rowsCost = 2 + rowsCost / 4;
                    tryAdditional = true;//tryAdditional表示不是等值操作
                    break;
                } else if ((mask & IndexCondition.START) == IndexCondition.START) {
                	//大于等于操作
                    rowsCost = 2 + rowsCost / 3;
                    tryAdditional = true;
                    break;
                } else if ((mask & IndexCondition.END) == IndexCondition.END) {
                	//小于等于操作
                    rowsCost = rowsCost / 3;
                    tryAdditional = true;
                    break;
                } else {
                    if (mask == 0) {
                        // Adjust counter of used columns (i)
                        i--;
                    }
                    break;
                }
            }
            //在上面遍历索引字段的过程中,一旦发现非等值比较或者该字段不再查询语句里面,便结束字段遍历
            if (tryAdditional) {//如果为true,表示查询语句里面有非等值比较查询
                while (i < len && masks[columns[i].getColumnId()] != 0) {
                	//如果索引中某个字段使用了非等值操作,那么后续的字段不能像等值操作一样通过该索引高效过滤数据,但是依然可以使用索引过滤数据,因此下面使用rowsCost--
                    i++;
                    rowsCost--;
                }
            }
            // 增加额外未使用列的索引成本
            // len表示当前索引中字段的个数
            rowsCost += len - i;
        }
        //如果ORDER BY子句与此索引的排序相一致,那么它的成本将响应降低
        long sortingCost = 0;
        if (sortOrder != null) {
            sortingCost = 100 + rowCount / 10;
        }
        //下面的if分支判断为true时表示当前SQL语句有order by子句,且不是使用主键索引查询
        if (sortOrder != null && !isScanIndex) {
        	//sortOrderMatches表示order by子句的排序规则是否与索引匹配
            boolean sortOrderMatches = true;
            int coveringCount = 0;
            //sortTypes表示order by子句里面各个字段的排序规则,下文有详细介绍
            int[] sortTypes = sortOrder.getSortTypesWithNullOrdering();
            TableFilter tableFilter = filters == null ? null : filters[filter];
            //从order by子句的第一个字段开始遍历
            for (int i = 0, len = sortTypes.length; i < len; i++) {
                if (i >= indexColumns.length) {
                	//这里表示当前排序的字段多于索引中的字段,在这种情况下,我们依然可以使用该索引,只是相对于包含更多排序字段的索引,coveringCount值小而已,进而影响其成本
                    break;
                }
                //获得对应的数据库表字段
                Column col = sortOrder.getColumn(i, tableFilter);
                //如果col为null,表示该排序字段不是当前表的字段
                if (col == null) {
                    sortOrderMatches = false;
                    break;
                }
                IndexColumn indexCol = indexColumns[i];
                //当前正在遍历order by子句的第i个字段,将该字段与索引中的第i个字段比较,如果字段一致,那么意味着可以使用索引排序,否则不能使用
                if (!col.equals(indexCol.column)) {
                    sortOrderMatches = false;
                    break;
                }
                int sortType = sortTypes[i];
                //如果order by子句的第i个字段与索引的第i个字段相同,那么比较排序规则,如果也一致,那么可以使用索引排序,否则不能使用
                if (sortType != indexCol.sortType) {
                    sortOrderMatches = false;
                    break;
                }
                //如果排序字段与索引字段相同,而且排序规则也一致,增大coveringCount,coveringCount越大,成本越小

                coveringCount++;
            }
            if (sortOrderMatches) {
                //coveringCount确保当我们有两个或多个覆盖索引时,我们选择覆盖更多的索引
                sortingCost = 100 - coveringCount;
            }
        }
		//如果当前的SQL语句,使用主键查询和使用某个二级索引查询的成本相同,而且二级索引可以满足查询需要,那么使用索引查询即可
		//如果needsToReadFromScanIndex=true,表示查询除了使用二级索引外还需要回表查询
        boolean needsToReadFromScanIndex;
        if (!isScanIndex && allColumnsSet != null) {
            needsToReadFromScanIndex = false;
            //获取SQL语句中使用到该表的所有字段
            ArrayList<Column> foundCols = allColumnsSet.get(getTable());
            if (foundCols != null) {
            	//获取main index column,在h2里面main index比较复杂,
            	//不过可以简单理解为,如果当前主键索引只有一个字段,那么getMainIndexColumn()返回的就是该字段在表中的字段位置,
            	//如果有多个字段或者没有索引,那么该方法返回的是一个常量SearchRow.ROWID_INDEX
                int main = table.getMainIndexColumn();
                //遍历SQL语句里面涉及到该表的所有字段,如果这些字段在当前索引中都有,那么表示可以使用该索引查询,不用回表
                loop: for (Column c : foundCols) {
                    int id = c.getColumnId();
                    //下面判断了id == SearchRow.ROWID_INDEX,因为二级索引里面默认有主键列
                    if (id == SearchRow.ROWID_INDEX || id == main) {
                        continue;
                    }
                    //columns表示当前索引中涉及到表字段
                    for (Column c2 : columns) {
                        if (c == c2) {
                            continue loop;
                        }
                    }
                    //SQL中有一个字段不在索引里面,那么除了使用二级索引查询外,还需要回表查询
                    needsToReadFromScanIndex = true;
                    break;
                }
            }
        } else {
            needsToReadFromScanIndex = true;
        }
        long rc;
        //下面是计算最终的成本
        if (isScanIndex) {
        	//如果当前是使用主键查询
            rc = rowsCost + sortingCost + 20;
        } else if (needsToReadFromScanIndex) {
        	//如果需要回表查询,那么还要计算上使用主键查询的成本
            rc = rowsCost + rowsCost + sortingCost + 20;
        } else {
			//下面的计算公式使用了索引列的个数,这是因为当我们选择覆盖索引时,
			//我们选择列数最少的覆盖索引(索引中的列数越多,成本越高),因为较小的索引占用的数据空间更小。
            rc = rowsCost + sortingCost + columns.length;
        }
        return rc;
    }

order by子句里面可以指定字段的排序规则,是ASC还是DESC,对于可以为NULL的字段,还有两种排序规则,分别是无论升序还是降序NULL值始终在最前以及无论升序还是降序NULL值始终在最后的排序规则,在h2中使用常量ASCENDING(0)、DESCENDING(1)、NULLS_FIRST(2)、NULLS_LAST(4)表示上述四种排序规则,不过后面两种不能在SQL语句中指定,h2会基于系统默认值设定,默认情况下,升序是NULL值始终在前,降序是NULL值始终在最后。所以默认情况下,对于order by id ,name desc子句,上面代码里面数组变量sortTypes[0]=ASCENDING | NULLS_FIRST=2,sortTypes[1]=DESCENDING | NULLS_LAST=5。

在h2中,全表扫描、回表查询指的都是使用主键查询。h2的表都是使用主键维护的表。

getCostRangeIndex()方法是计算执行计划成本的最核心方法,它的返回值表示的就是当前表使用该索引查询的成本。通过本方法,对表查询时就可以判断出使用哪个索引查询成本最低。上面的方法代码量并不多,但是涉及的内容比较多,通过上面的方法可以看到什么样的索引是“好”索引。下面总结一下“好”索引的特点:

  1. 索引能覆盖SQL语句中的所有字段,这种索引也叫作覆盖索引;
  2. 索引遵循最左前缀原则,因此尽量不要由对索引中间字段或者开头字段进行范围查询,这会导致后面的字段无法使用索引;
  3. 排序的字段及顺序对索引的使用也有影响;
  4. 索引是唯一索引,而且查询语句对该索引都是等值查询,这种索引成本最低。

不过,h2的优化器还有有局限性的,比如计算成本时,没有考虑该表或者索引是否已经在缓存中了。
下面分几种场景介绍h2如何使用getCostRangeIndex()方法确定执行计划。

二、单表查询

单表查询是最简单的,首先使用主键索引计算成本,然后遍历各个二级索引计算成本,从这里面选择成本最小的索引,之后便使用该表进行查询。

三、多表关联查询

当关联表的个数小于等于7个时,h2会计算出这些表的所有排列,对每个排列从第一个表开始遍历,根据查询条件,通过getCostRangeIndex()方法确定每个表使用哪个索引的成本最低,并得到最低成本,然后使用如下伪代码计算出该排列的总成本:

	//item.cost是对每个表进行查询的最小成本,
	double cost = 1;
	//遍历每个表,tables表示一个排列
	//tables[i].cost表示表的最小成本
	for (int i = 0; i < tables.length; i++) {
		cost += cost * tables[i].cost;
	}

这样便可以从这些排列里面找到成本最低的排列。之后h2使用该排列中表的顺序依次操作每个表。
不过h2也有一个提前终止条件,可以提前终止对排列的遍历,代码如下:

    private boolean canStop(int x) {
        return (x & 127) == 0
                // don't calculate for simple queries (no rows or so)
                && cost >= 0
                // 100 microseconds * cost
                && System.nanoTime() - startNs > cost * 100_000L;
    }

入参x是一个计数器,表示已经遍历了多少个排列了,每遍历一个排列,x会增加1,x从0开始计数。当canStop()返回true时,那么后续的排列便不再遍历。这会导致最优的排列可能没有遍历到。

h2提供了Permutations类用于生成下一个排列,该算法是由Alan Tucker实现的。

如果关联表的个数大于7个,h2是无法遍历所有排列的。h2使用随机+贪心算法来搜索较好的排列。与暴力搜索不同,随机+贪心不一定能找到最好的排列顺序,但是至少可以找到次优的排列。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

h2database源码解析-查询优化器原理 的相关文章

随机推荐

  • 有符号数的四舍五入(round)(verilog实现)

    有符号数的表达方式见上篇 有符号小数的表示 扩展和计算 weixin 42330305的博客 CSDN博客 对于有符号数 正数和负数的四舍五入有些许不同 需要区别对待 一 正数 对于正数来说 如果被截掉的数的最高位为1 则结果为保留的数 1
  • 【分析方法】A/B test

    A B测试是什么 怎么做 有什么作用呢 本篇文章为大家分享了几种应用场景及案例 告诉大家如何在团队中有效推进A B测试 一 A B测试 为了检测某些用户到底属于哪一类别 我们定制了A类用户喜欢的产品和B类用户喜欢的产品 统计并对比了不同方案
  • 给jupyter添加多个python版本的kernel

    两种方法 1 想添加的python版本已存在 1 通过ipykernel为jupyter添加python环境 activate env name python m ipykernel install name env name 2 关闭py
  • Windows安装Anaconda,创建pytorch环境,pycharm配置环境

    目录 1 简介 2 安装Anaconda 3 创建一个独立的环境 4 安装依赖的库 5 安装pytorch 6 pycharm中使用conda环境 7 到这里安装就结束了 希望对您有所帮助 如有什么错误请指正 1 简介 安装Anaconda
  • C++:重载

    一 重载 重载 重载函数是函数的一种特殊情况 为方便使用 C 允许在同一范围中声明几个功能类似的同名函数 但是这些同名函数的形式参数 指参数的个数 类型或者顺序 必须不同 也就是说用同一个函数完成不同的功能 重载函数常用来实现功能类似而所处
  • git fatal: unable to access  Failed to connect to localhost port 1080: Connection refused

    git 拉取 更新子模块失败 提示失败 Submodule libXesBase https git xxxxx com xesoa libXesBase git registered for path libXesBase Cloning
  • 整合google,51ditu和mapbar的地图API

    http blog 163 com goodluck lq 126 blog static 63285386201001994058213
  • Java中的异常处理机制的简单原理和应用。

    异常是指java程序运行时 非编译 所发生的非正常情况或错误 与现实生活中的事件很相似 现实生活中的事件可以包含事件发生的时间 地点 人物 情节等信息 可以用一个对象来表示 Java使用面向对象的方式来处理异常 它把程序中发生的每个异常也都
  • 基于STM32F103单片机的车牌识别图像处理识别系统 原理图PCB程序设计

    硬件电路的设计 末尾附文件 系统硬件系统分析设计 1 STM32单片机核心电路设计 STM32系列处理器是意法半导体ST公司生产的一种基于ARM 7架构的32位 支持实时仿真和跟踪的微控制器 选择此款控制芯片是因为本系统设计并非追求成本的最
  • React通过axios拿到数据后,使用hooks,通过map函数对列表进行渲染

    导入hooks 导入你封装的http模块 引入样式 import React useEffect useState from react import http from API import index scss 默认导出一个函数组价 并
  • C#学习记录——.NET的三层架构

    每一个不曾起舞的日子 都是对生命的辜负 尼采 每一个不读书的的日子 都是对时光的辜负 今天学习 零基础学C 3 0 NET的三层架构 为了实现大型应用系统后续功能的扩展性和程序的灵活性 NET编程语言借鉴了JAVA的MVC思想 产生了三层架
  • MySQL - 第9节 - MySQL内外连接

    目录 1 内连接 2 外连接 2 1 左外连接 2 2 右外连接 3 简单案例 1 内连接 表的连接分为内连接和外连接 内连接实际上就是利用where 子句对两种表形成的笛卡儿积进行筛选 我们前面学习的查询都是内连接 也是在开发过程中使用的
  • Markdown语法--Obsidian笔记

    Markdown 语法 笔记 文章目录 Markdown 语法 笔记 语法分类 文字层级类 1 标题 2 段落 3 区块引用 4 代码区块 5 列表 6 待办事项 文字格式类 1 样式 2 表格 链接引用类 1 链接 2 图片 3 脚注 4
  • Dubbo与Spring Cloud的区别

    这是个老生常谈的问题 每个技术团队在业务转型微服务化架构的时候都会纠结过这个选型问题 首先 dubbo 之前确实在 2012 年的时候发布了最后一个版本 2 5 3 并且停止维护更新 在2017年的时候又 起死回生 官方宣布重启更新 并重点
  • 2021图像检索综述

    论文地址 Deep Image Retrieval A Survey 本文是2021年最新的关于图像检索的综述 介绍了基于内容的图像检索 content based image retrieval CBIR 在深度学习技术上的进展 目录 0
  • Traceback (most recent call last): File “D:/python_workspace/hello.py“, line 3, in <module>

    错误背景 python的初学者 在学习多行语句 错误信息如下 错误原因 变量有字符串类型 有整型类型 有浮点型 在java 里面 String标识字符串类型 Int标识整型 在python里面 a yy1 就是字符串类型 a 1就是数字类型
  • 29_content 阶段的concat 模块

    文章目录 提升性能 content 阶段的 caoncat 模块 concat 模块的指令 示例配置 提升性能 content 阶段的 caoncat 模块 功能 当页面需要访问多个小文件时 把它们内容合并到一次http 响应中返回 提升性
  • 数组排序的方法?

    1 sort排序 let arr 1 2 3 4 5 6 7 8 9 0 9 8 7 6 3 4 5 5 var res console log arr 排序前 1 2 3 4 5 6 7 8 9 0 9 8 7 6 3 4 5 5 arr
  • SSD目标检测算法原理(上)

    目录 一 目标检测概述 1 1 项目演示介绍 1 2 图片识别背景 1 3 目标检测定义 二 目标检测算法原理 2 1 任务描述 2 2 目标检测算法必备基础 2 3目标检测算法模型输出 目标检测 overfeat模型 R CNN模型 候选
  • h2database源码解析-查询优化器原理

    目录 一 成本计算规则 二 单表查询 三 多表关联查询 一 成本计算规则 h2的查询优化器基于成本的 因此在执行查询前 会基于成本计算使用哪个索引 如果涉及多表关联 还会计算不同表关联顺序的成本 最终基于最小成本得出执行计划 单表查询时 遍