Calcite Gremlin Adapter 图计算遇上SQL (一)

临近年终,想把自己这一年里的做的一些东西整理一下,也许复盘的不会非常好,但也会尽我所能。这个功能最终是否能成功合并,对于我而言也是未知的. 所以,走一步看一步吧

讨论线程:lists.apache.org/thread/s0cg...

我将会从业务价值, 实现方案,以及贡献细节三个维度进行复盘总结.

业务价值

在大型社交网络的关系计算当中,图数据库显得越来越重要,如果有这样一个人,已知他的点属性有公司,国家,才艺,人物等信息,并且也知道跟他有相同属性的人的信息, 这就可以构建一个图, 用来表征整个社交网络的关系.对其进行关联关系的计算

下面这个json代表着一张社交网络关系图的表示, tables代表点属性的集合,relationships代表边属性的集合.edgeLabel代表着某条边的特定标签(两种点属性连线的社交关系标识, 比如person与company代表在同一个公司工作的人)

json 复制代码
{
  "tables": [
    {
      "name": "company",
      "columns": [
        {"name": "name", "type": "string"}
      ]
    },
    {
      "name": "country",
      "columns": [
        {"name": "name", "type": "string"}
      ]
    },
    {
      "name": "planet",
      "columns": [
        {"name": "name", "type": "string"}
      ]
    },
    {
      "name": "person",
      "columns": [
        {"name": "name", "type": "string"},
        {"name": "age", "type": "integer"}
      ]
    },
    {
      "name": "spaceship",
      "columns": [
        {"name": "name", "type": "string"},
        {"name": "model", "type": "string"}
      ]
    },
    {
      "name": "satellite",
      "columns": [
        {"name": "name", "type": "string"}
      ]
    },
    {
      "name": "sensor",
      "columns": [
        {"name": "name", "type": "string"},
        {"name": "type", "type": "string"}
      ]
    },
    {
      "name": "sensorReading",
      "columns": [
        {"name": "tstamp", "type": "long_timestamp", "propertyName": "timestamp"},
        {"name": "dt", "type": "long_date", "propertyName": "date"},
        {"name": "value", "type": "double"}
      ]
    },
    {
      "name": "fliesTo",
      "columns":[
        {"name": "trips", "type": "integer"}
      ]
    },
    {
      "name": "orbits",
      "columns":[
        {"name": "launched", "type": "integer"}
      ]
    }
    ],
  "relationships": [
    {"outTable": "company", "inTable": "country", "edgeLabel": "baseIn"},
    {"outTable": "person", "inTable": "company", "edgeLabel": "worksFor"},
    {"outTable": "person", "inTable": "planets", "edgeLabel": "travelledTo"},
    {"outTable": "company", "inTable": "spaceship", "edgeLabel": "owns"},
    {"outTable": "person", "inTable": "spaceship", "edgeLabel": "pilots"},
    {"outTable": "sensor", "inTable": "sensorReading", "edgeLabel": "hasReading", "fkTable": "sensorReading"},
    {"outTable": "person", "inTable": "planet", "edgeLabel": "fliesTo"},
    {"outTable": "satellite", "inTable": "planet", "edgeLabel": "orbits"},
    {"outTable": "person", "inTable": "person", "edgeLabel": "friendsWith"}
  ]
}

熟悉图这种数据结构的同学应该都知道,在图中,对于点存在出度和入度,区分于两者差异的要素在于,是否有边指向/指出这个点. 在上述数据中 person (Edge Label: worksFor)-> company -> (Edge Label: owns) -> spaceship. 对于compay这个点而言,person就是入度点, spaceship就是出度点. worksFor/owns 分别是入出度边

讲完了图定义,为了接下来梳理calcite执行计划和图的遍历操作之间的关系, 介绍一下图遍历操作:

  • 以 "顶点"为基准

    • out(label): 根据指定的Edge Label来访问顶点的OUT方向邻接点(如果是0个Edge Label代表所有类型的边, 也可以为一个或者多个Edge Label, 代表任意给定Edge Label的边)
    • in(label): 根据指定的Edge Label来访问顶点的IN方向邻接点
    • both(label): 根据指定的Edge Label来访问顶点的双向邻接点
    • outE(label): 根据指定的Edge Label来访问顶点OUT方向邻接边
    • inE(label): 根据指定的Edge Label来访问顶点IN邻接边
    • both(label): 根据指定的Edge Label来访问双向邻接边
  • 以 "边" 为基准

    • outV: 访问边的出顶点,出顶点意思就是指的边的起始顶点
    • inV: 访问边的入顶点, 入顶点是指边的目标顶点,也就是箭头指向的顶点
    • bothhV: 访问边的双向顶点
    • otherV: 访问边的伙伴顶点, 也就是相对于基准点而言的另一端顶点

Gremlin 是一种图数据库的普遍查询语言,最早在thinkerpop框架中提出。thinkerpop作为一种兼容不同图数据库的抽象层,只要保证图数据库都兼容Gremlin语法,就能适配.

现在来看一看主角之一Gremlin语法长什么样子

less 复制代码
g.V().hasLabel("stringtype").group().unfold().select(Column.values).order().by(__.unfold().id()).project("stringtype").by(__.project("key").by(__.unfold().choose(__.has("key"),__.values("key"),__.constant("\$%#NULL#%\$"))))

"怎么样,是不是觉得这根本就不像平常我们用的MySQL, clickhouse这种SQL92,SQL95标准的SQL?"

首先,如果这种语法,要拿去给客户用,客户是只能接受傻瓜式小白操作的,你肯定不能让客户自己学着写这种SQL,一边像开发人员查Gremlin的语法文档吧? 那自然是不行的,理解成本太高了. 那么你又会有个疑问,作为图计算的普遍查询语言,官方难道就没有符合SQL92这种语法的DSL可以用? 很遗憾,我几乎翻遍了全网,没在一个角落里发现有这种支持. 在大厂的实践中,对于图数据库的运用有两种解法,一种是像小红书这样,自己自建RedTao(mysql存边集和点集,redis负责基于散列表,负责路由查询负载的均衡). 一种是像蚂蚁开源的TuGraph-anaylsis那样, 基于现有的图数据库,实现一层专门的图计算层,沉淀不同的模型.

其次, 如果像小红书这样建设,需要建设额外的缓存集群,这也就意味着缓存冲突和通过代理后的写后一致性需要解决,同时在170w边, 50w点这种场景下对内存的申请和释放要求非常高, 需要额外的基架团队来维护。并且这种方法没办法接入业内通用的数据库(开源的JanusGraph,华为云的Graphbase),公司是政府行业业务没有办法,也没有精力维护一套这样的集群. 如果像蚂蚁这样建设图计算引擎,就必须强依赖于蚂蚁的生态(专门用他们的图数据库TuGraph-DB)。 既不想跟厂商强绑定,也不想自己去自研缓存集群, 但还需要兼容各种数据库。那就只能看看能不能用正常的SQL92标准做图计算OLAP了

实现方案

习惯用mysql的同学会发现一个问题 用explain分析sql的时候,生成的执行计划会告诉你该查询的类型,命中的索引,扫描的表,过滤了多少行数据.这些其实都是查询器在通过语法解析传下来的信息,根据数据量对整个表的扫描方式做分配.也就是说查询层只负责查询计划的生成,具体的读io是通过下推到磁盘自己去实现的

vbnet 复制代码
mysql> EXPLAIN SELECT t1.a, t1.a IN (SELECT t2.a FROM t2) FROM t1\G 
*************************** 1. row *************************** 
id: 1 
select_type: PRIMARY 
table: t1 
type: index 
possible_keys: NULL 
key: PRIMARY 
key_len: 4 
ref: NULL 
rows: 4 
filtered: 100.00 
Extra: Using index 
*************************** 2. row *************************** 
id: 2 
select_type: SUBQUERY 
table: t2 
type: index 
possible_keys: a 
key: a 
key_len: 5 
ref: NULL 
rows: 3 
filtered: 100.00 
Extra: Using index 2 rows in set, 1 warning (0.00 sec)

上述提到的图遍历操作本质上是找出符合条件的EdgeLabel对需要关联的点与边进行统计,跟MySQL表的扫描方式不谋而合. 这里稍微介绍一下第二位主角calcite, calcite是一个查询优化器项目,他本身可以通过适配各种数据库的schema,对不同数据源的查询进行兼容,也就是adapter功能.

我们要做的事情 就是根据SQL92标准的语句,生成符合Gremlin语法的执行计划

sql 复制代码
SELECT "key" FROM stringtype

翻译成如下执行计划

less 复制代码
g.V().hasLabel("stringtype").group().unfold().select(Column.values).order().by(__.unfold().id()).project("stringtype").by(__.project("key").by(__.unfold().choose(__.has("key"),__.values("key"),__.constant("\$%#NULL#%\$"))))

实现一个adapter需要做如下几件事情

  • 将节点中的select/group by/having/order by解析成Gremlin语法的遍历方式,完成扫描方式的生成
  • 在根据语法节点生成扫描方式后,根据指定的label生成project遍历步骤
  • 将执行计划当中的filter,scan重写,下推
  • 完成多表join时,表分片数据是否命中的预先判定以及符合扫描方式的条件下,是否能返回预期数据

在讨论线程的项目当中 有一个GremlinSqlBasicSelectTest的单元测试,有兴趣可以跑跑看

实现细节

以下代码是一个SQL92查询翻译成执行计划的主逻辑 (单表查询 不包含多表join)

scss 复制代码
public class GremlinSqlSelectSingle extends GremlinSqlSelect {
    private static final Logger LOGGER = LoggerFactory.getLogger(GremlinSqlSelectSingle.class);
    private final SqlSelect sqlSelect;
    //数据扫描的元信息(GremlinSchema的格式,包含点集和边集。命中的条件列以及表信息,是否发生聚合操作)
    private final SqlMetadata sqlMetadata;
    //图的遍历数据源,对应着执行计划开头的g
    private final GraphTraversalSource g;
    //calcite最顶级的语法节点 代表第一层级(非子查询的源表名)
    private final SqlBasicCall sqlBasicCall;

    public GremlinSqlSelectSingle(final SqlSelect sqlSelect,
                                  final SqlBasicCall sqlBasicCall,
                                  final SqlMetadata sqlMetadata, final GraphTraversalSource g) {
        super(sqlSelect, sqlMetadata);
        this.sqlSelect = sqlSelect;
        this.sqlMetadata = sqlMetadata;
        this.g = g;
        this.sqlBasicCall = sqlBasicCall;
    }

    @Override
    protected void runTraversalExecutor(final GraphTraversal<?, ?> graphTraversal,
                                        final SqlGremlinQueryResult sqlGremlinQueryResult) throws SQLException {
        // Launch thread to continue grabbing results.
        final ExecutorService executor = Executors.newSingleThreadExecutor(
                new ThreadFactoryBuilder().setNameFormat("Data-Insert-Thread-%d").setDaemon(true).build());
        final List<List<String>> columns = new ArrayList<>(sqlMetadata.getColumnOutputListMap().values());
        if (columns.size() != 1) {
            throw new SQLException("Error: Single select has multi-table return.");
        }
        executor.execute(new Pagination(new SimpleDataReader(
                sqlMetadata.getRenameFromActual(sqlMetadata.getTables().iterator().next().getLabel()), columns.get(0)),
                graphTraversal, sqlGremlinQueryResult));
        executor.shutdown();
    }

    @Override
    public GraphTraversal<?, ?> generateTraversal() throws SQLException {
        if (sqlSelect.getSelectList() == null) {
            throw new SQLException("Error: GremlinSqlSelect expects select list component.");
        }

        final GremlinSqlOperator gremlinSqlOperator =
                GremlinSqlFactory.createOperator(sqlBasicCall.getOperator(), sqlBasicCall.getOperandList());
        if (!(gremlinSqlOperator instanceof GremlinSqlAsOperator)) {
            throw new SQLException("Unexpected format for FROM.");
        }
        final List<GremlinSqlNode> gremlinSqlOperands = GremlinSqlFactory.createNodeList(sqlBasicCall.getOperandList());
        final List<GremlinSqlIdentifier> gremlinSqlIdentifiers = new ArrayList<>();
        for (final GremlinSqlNode gremlinSqlOperand : gremlinSqlOperands) {
            if (!(gremlinSqlOperand instanceof GremlinSqlIdentifier)) {
                throw new SQLException("Unexpected format for FROM.");
            }
            gremlinSqlIdentifiers.add((GremlinSqlIdentifier) gremlinSqlOperand);
        }

        final GraphTraversal<?, ?> graphTraversal =
                SqlTraversalEngine.generateInitialSql(gremlinSqlIdentifiers, sqlMetadata, g);
        final String label = sqlMetadata.getActualTableName(gremlinSqlIdentifiers.get(0).getName(1));
        //生成groupby扫描方式
        applyGroupBy(graphTraversal, label);
        //生成select扫描方式
        applySelectValues(graphTraversal);
        //生成Order by 扫描方式
        applyOrderBy(graphTraversal, label);
        //生成Having扫描方式
        applyHaving(graphTraversal);
         //生成where扫描方式
        applyWhere(graphTraversal);
        //生成聚合扫描
        SqlTraversalEngine.applyAggregateFold(sqlMetadata, graphTraversal);
        //添加需要下推的投影project
        SqlTraversalEngine.addProjection(gremlinSqlIdentifiers, sqlMetadata, graphTraversal);
        final String projectLabel = gremlinSqlIdentifiers.get(1).getName(0);
        //下推对应的列
        applyColumnRetrieval(graphTraversal, projectLabel,
                GremlinSqlFactory.createNodeList(sqlSelect.getSelectList().getList()));

        if (sqlMetadata.getRenamedColumns() == null) {
            throw new SQLException("Error: Column rename list is empty.");
        }
        if (sqlMetadata.getTables().size() != 1) {
            throw new SQLException("Error: Expected one table for traversal execution.");
        }
        return graphTraversal;
    }

    public String getStringTraversal() throws SQLException {
        return GroovyTranslator.of("g").translate(generateTraversal().asAdmin().getBytecode());
    }

    private void applySelectValues(final GraphTraversal<?, ?> graphTraversal) {
        graphTraversal.select(Column.values);
    }
    
    //生成聚合扫描计划比较麻烦 比如select count(1) from inttype groupby workFor,这样相当于要将所有
    //符合Edge Label的点集合分批聚合.也就是一个人的关联次数,如果想要从各个维度统计 ,就需要借助union all
    //的方式将点属性分别聚合后得到的结果进行合并,之所以这样做 是因为图的遍历方式存在方向,所以需要先满足之
    //前点集合集合的条件下,才能进行下一个点集合的聚合,可以试想有一个线性的列表,保存不断加入的聚合键,进
    //行聚合。比如第一次根据company聚合出5000条数据,第二次加入country属性,此时聚合键列表有两个属性,聚
    //合出500条数据,分阶段聚合每一次需要遍历操作带来的磁盘压力就会减少许多
    protected void applyGroupBy(final GraphTraversal<?, ?> graphTraversal, final String table) throws SQLException {
        if ((sqlSelect.getGroup() == null) || (sqlSelect.getGroup().getList().isEmpty())) {
            // If we group bys but we have aggregates, we need to shove things into groups by ourselves.-
            graphTraversal.group().unfold();
        } else {
            final List<GremlinSqlNode> gremlinSqlNodes = new ArrayList<>();
            for (final SqlNode sqlNode : sqlSelect.getGroup().getList()) {
                gremlinSqlNodes.add(GremlinSqlFactory.createNodeCheckType(sqlNode, GremlinSqlIdentifier.class));
            }
            graphTraversal.group();
            final List<GraphTraversal> byUnion = new ArrayList<>();
            for (final GremlinSqlNode gremlinSqlNode : gremlinSqlNodes) {
                final GraphTraversal graphTraversal1 = __.__();
                toAppendToByGraphTraversal(gremlinSqlNode, table, graphTraversal1);
                byUnion.add(graphTraversal1);
            }
            graphTraversal.by(__.union(byUnion.toArray(new GraphTraversal[0])).fold()).unfold();
        }
    }

    protected void applyOrderBy(final GraphTraversal<?, ?> graphTraversal, final String table) throws SQLException {
        graphTraversal.order();
        if (sqlSelect.getOrderList() == null || sqlSelect.getOrderList().getList().isEmpty()) {
            graphTraversal.by(__.unfold().id());
            return;
        }
        final List<GremlinSqlNode> gremlinSqlIdentifiers = new ArrayList<>();
        for (final SqlNode sqlNode : sqlSelect.getOrderList().getList()) {
            gremlinSqlIdentifiers.add(GremlinSqlFactory.createNode(sqlNode));
        }
        for (final GremlinSqlNode gremlinSqlNode : gremlinSqlIdentifiers) {
            appendByGraphTraversal(gremlinSqlNode, table, graphTraversal);
        }
    }

   //分阶段聚合操作添加到图遍历数据源当中的过程
    private void toAppendToByGraphTraversal(final GremlinSqlNode gremlinSqlNode, final String table,
                                            final GraphTraversal graphTraversal)
            throws SQLException {
        if (gremlinSqlNode instanceof GremlinSqlIdentifier) {
            //从元信息中拿到该查询需要下推的列,值得注意的是由于我们选择根据点属性分阶段聚合,出于性能考虑, 所以根据边属性的聚合就不再实现了
            final String column = sqlMetadata
                    .getActualColumnName(sqlMetadata.getGremlinTable(table),
                            ((GremlinSqlIdentifier) gremlinSqlNode).getColumn());
            if (column.endsWith(GremlinTableBase.IN_ID) || column.endsWith(GremlinTableBase.OUT_ID)) {
                // TODO: Grouping edges that are not the edge that the vertex are connected - needs to be implemented.
                throw new SQLException("Error, cannot group by edges.");
            } else {
                graphTraversal.values(sqlMetadata.getActualColumnName(sqlMetadata.getGremlinTable(table), column));
            }
        } else if (gremlinSqlNode instanceof GremlinSqlBasicCall) {
            final GremlinSqlBasicCall gremlinSqlBasicCall = (GremlinSqlBasicCall) gremlinSqlNode;
            gremlinSqlBasicCall.generateTraversal(graphTraversal);
        }
    }

//为select/group by/having/order by操作各自生成符合Gremlin语法的执行计划后, 通过解析对应语法解析起到 //编排的作用, 比如group操作生成了 g.group(),select操作生成了g.V().by(__.project("key") ,会编排成如下Gremlin语法的执行计划 g.V().project("stringtype").g.V().hasLabel("stringtype").group().unfold().select(Column.values).order().by(__.unfold().id()).project("stringtype").by(__.project("key").by(__.unfold().choose(__.has("key"),__.values("key"),__.constant("\$%#NULL#%\$"))))

    private void appendByGraphTraversal(final GremlinSqlNode gremlinSqlNode, final String table,
                                        final GraphTraversal graphTraversal)
            throws SQLException {
        final GraphTraversal graphTraversal1 = __.unfold();
        //只有父表,没有子查询
        if (gremlinSqlNode instanceof GremlinSqlIdentifier) {
            final String column = sqlMetadata
                    .getActualColumnName(sqlMetadata.getGremlinTable(table),
                            ((GremlinSqlIdentifier) gremlinSqlNode).getColumn());
            if (column.endsWith(GremlinTableBase.IN_ID) || column.endsWith(GremlinTableBase.OUT_ID)) {
                // TODO: Grouping edges that are not the edge that the vertex are connected - needs to be implemented.
                throw new SQLException("Error, cannot group by edges.");
            } else {
   //根据不同语法解析生成的执行计划开始编排             graphTraversal1.values(sqlMetadata.getActualColumnName(sqlMetadata.getGremlinTable(table), column));
            }
            graphTraversal.by(graphTraversal1);
        } 
        //如果存在子查询
        else if (gremlinSqlNode instanceof GremlinSqlBasicCall) {
            
            final GremlinSqlBasicCall gremlinSqlBasicCall = (GremlinSqlBasicCall) gremlinSqlNode;
            gremlinSqlBasicCall.generateTraversal(graphTraversal1);
            //是否存在降序排序的情况
            if (gremlinSqlBasicCall.getGremlinSqlOperator() instanceof GremlinSqlPostFixOperator) {
                final GremlinSqlPostFixOperator gremlinSqlPostFixOperator =
                        (GremlinSqlPostFixOperator) gremlinSqlBasicCall.getGremlinSqlOperator();
                graphTraversal.by(graphTraversal1, gremlinSqlPostFixOperator.getOrder());
            } else {
                graphTraversal.by(graphTraversal1);
            }
        }
    }

    protected void applyHaving(final GraphTraversal<?, ?> graphTraversal) throws SQLException {
      .......
    }

    protected void applyWhere(final GraphTraversal<?, ?> graphTraversal) throws SQLException {
       ..........
    }
}

由于本提案仍在讨论当中,最近会逐步记录拆分细节并整理到邮件列表当中,希望能给自己一个好的结果

祝宫园薰保佑我

相关推荐
中科岩创2 分钟前
中科岩创边坡自动化监测解决方案
大数据·网络·物联网
DolphinScheduler社区1 小时前
作业帮基于 Apache DolphinScheduler 3_0_0 的缺陷修复与优化
大数据
SeaTunnel1 小时前
京东科技基于 Apache SeaTunnel 复杂场景适配 #数据集成
大数据
喝醉酒的小白2 小时前
Elasticsearch 配置文件
大数据·elasticsearch·搜索引擎
一只敲代码的猪3 小时前
Llama 3 模型系列解析(一)
大数据·python·llama
智慧化智能化数字化方案3 小时前
深入解读数据资产化实践指南(2024年)
大数据·人工智能·数据资产管理·数据资产入表·数据资产化实践指南
哦哦~9213 小时前
深度学习驱动的油气开发技术与应用
大数据·人工智能·深度学习·学习
Anna_Tong4 小时前
云原生大数据计算服务 MaxCompute 是什么?
大数据·阿里云·云原生·maxcompute·odps
drebander4 小时前
SQL 实战-巧用 CASE WHEN 实现条件分组与统计
大数据·数据库·sql
18号房客4 小时前
高级sql技巧进阶教程
大数据·数据库·数据仓库·sql·mysql·时序数据库·数据库架构