观测云查询语言DQL设计思路大解密

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 一文了解DQL

DQL 是专为 观测云(DataFlux)开发的语言,语法简单,方便使用,可在 DataFlux Studio 进行数据查询,也可通过 客户端命令行 进行数据查询。

在 DataFlux 中,我们用了多个不同的存储引擎(目前主要是 InfluxDB 以及 ElasticSearch),在这种混合存储的场景下,将查询语言统一起来,是非常有意义的:

  • DataFlux 是重查询产品,所有的可观测数据,都是通过查询来获取的
  • 在具体的可观测场景下,某个简单的图表,可能底层涉及多个不同的存储引擎查找,如果分别查找,会导致网络 IO 剧增,同时也殃及页面响应
  • 在巨量观测数据面前,为了防止意外的巨量数据查找,需在查询语句上做保护
  • 不同的存储引擎,其查询语法全然不同,无形中给开发人员带来了额外的工作量
  • 如果接入其它存储引擎,前端开发又得学一遍查询语法,历史页面也需要大量改造

基于这样一个情况,提供统一的查询语言,迫在眉睫。

对于新设计一门语言,这种事情是工程师所喜闻乐见的。对于新语言的开发,命名首当其冲,对 DataFlux 而言,顺其自然,就是「DQL」。

DQL  要做哪些事情

在上面,我们提到统一查询语言的意义,从中我们也能看到 DQL 要做的一些事情,但只是泛泛而谈。这里,我们要大致列举下 DQL 的能力范围:

所有DataFlux中的观测数据,都能通过DQL查找


DQL查询的返回结构是一致的

不管后端是 InflxuDB 还是 ElasticSearch 还是其它即将引入的存储引擎,它们各自的查询结果返回,结构虽各不相同,但 DQL 需统一好给前端(这里的「前端」包括但不限于 浏览器/命令行等)

DQL需支持参数注入

对浏览器端而言,某些查询条件是不便于直接写入 DQL 的,而 DQL 语句是通过类似 JSON API 发给后端,在这个 JSON 中,需提供各种不同的参数注入,以调整最终的 DQL 查询行为,如额外的分组(group by)参数,查询的时间范围等,因为这些参数,实际上都是可以在 UI 调整的,而表格里面的 DQL 是固定死的,不便于跟 UI 随动,只能通过查询注入

DQL语法要相对简单且高度可扩展

对 DQL 而言,其主要职责是查询(不排除后面提供更新语法),跟 SQL 相比,它只需要提供 SELECT 即可,目前是没有 INSERT/DELETE/UPDATE 功能,从这个角度而言,DQL 就简化了不少。从另一个角度而言,因为底层的存储引擎可能会有多个,对查询功能而言,在语法上,不能对 DQL 做过多限制

DQL针对不同的数据做查询限制

因为不同的数据(日志、时序、对象、APM等),其存储策略不同,查询策略也会有所差异,DQL 需分别对待。

确定了这些,接下来我们确定一下语法选择。

语法选择

我们最为熟悉的查询语言莫过于 SQL,现如今已经成了行业标准,大家基本都能看懂 MySQL/PostgreSQL/SQLServer/Oracle 几家的 SQL 语句,大同小异,稍有不同。查询结构大致如下:

SELECT column_name(s)     -- 要查什么
FROM table_name           -- 从哪查
WHERE condition           -- 过滤条件是什么
GROUP BY column_name(s)   -- 结果怎么分组
ORDER BY column_name(s);  -- 结果怎么排序


先不论采用何种语法,这些基本要素,DQL 都必须满足。对于目前 DataFlux 使用的查询引擎,以 InflxuDB 为例,其基本查询结构为:

SELECT <field_key>[,<field_key>,<tag_key>]
  FROM <measurement_name>[,<measurement_name>]
  WHERE <conditional_expression>
  GROUP BY [* | <tag_key>[,<tag_key]]
  ORDER BY time [desc|asc] 

而 ElasticSearch 的查询则很庞大(因为它足够灵活),这里以一个简答的查询为例,在 InflxuDB 中查询一条最近的 CPU 数据,其查询大概如下:

SELECT * FROM "cpu" WHERE "host" = '张三的电脑' ORDER BY "time" DESC LIMIT 1


同等的 ElasticSearch 的查询语句则大相庭径,对于这么复杂的查询,如果没有专门的 IDE,是很难写正确的:

{
  "query": {
    "bool": {
      "must": [
      {
        "bool": {
          "should": [
          {
            "term": {
              "class": {
                "value": "cpu"
              }
            }
          }
          ]
        }
      },
      {
        "term": {
          "host": {
            "value": "张三的电脑"
          }
        }
      }
    }
    "size": 1,
    "sort": [
        {
            "last_update_time": {
                "missing": "_last",
                "order": "desc",
                "unmapped_type": "string"
            }
        }
    ]
  }
}

综合两种查询风格,我们可以看到:

  • InflxuDB 的查询风格,跟 SQL 基本一致,这也很容易理解,毕竟大家都很熟悉,写起来差不多
  • ElasticSearch 的查询很臃肿,但极为灵活,对 ElasticSearch 本身的特性而言,这是正确的设计。虽然 ElasticSearch 也支持 SQL 形式的查询,但其功能(相对)没有 JSON 格式强大
  • 对 DQL 而言,这两种风格,似乎都不太合适
  • 类 SQL 的语法肯定能满足查询需求,但其关键字太多(SELECT/FROM),写起来繁琐,另外容易让人联想到 insert/update 等语法,而这些在 DQL 设计之初就决定不予支持
  • JSON 语法没有必要,DQL 没有这么灵活的查询需求(主要还是太难写了)

为此,我们看了下其它的查询语言,比如 PromQL:

http_requests_total{job="apiserver", handler="/api/comments"}

这里的查询语义为:查询指标 http_requests_total,以 job="apiserver" AND handler="/api/comments" 为过滤条件。注意,这里省去了 SELECT/FROM 这样的语法,直接通过出现的位置来「暗示」其语义,翻译成 SQL 就是:

SELECT * FROM http_requests_total WHERE `job`="apiserver" AND `handler`="/api/comments";

在我们看来,PromQL 的语法,正是 DQL 喜欢的味道,它们要做的事情,其实异曲同工:只专注查询。

确定了语法选型,接下来的事情,就是如何实现这些语法了,语法的实现,有几种常见的思路:


  • 直接裸解析,暴力如 TCL 这种 C 编译器,就是这种。当然 InflxuDB 的查询语言处理,也是手写的
  • 通过专门的语法生成工具,如 ANTLR 或者 yacc/lex(bison/flex)

我们看了下 PromQL 的实现,决定采用 yacc,相比 ANTLR:

  • Golang 中内置了 yacc 实现(PromQL 就是用 golang 实现的),跟我们的技术栈契合很好
  • yacc 的性能(内存消耗)相对更好(之前我们通过 ALTLR 做过 InfluxQL 的翻译,性能不太理想)
  • 最主要的是,我们的工程师相对更熟悉 yacc

DQL  为什么是现在这个样子

最终,DQL 的语法结构大概如下:

namespace::data-source:(target-clause)
  {where-clause}
  [time-expr]
  by-clause
  order-by-clause
  limit-clause

从基本的语法结构中,可以看出,我们倾向于采用一些特殊符号来「暗示」高频语义,而非用确定的单词(如SELECT/FROM 等),因为它们极为常用,简化其输入是我们优先考虑的。但对于相对低频的语义,我们还是选用了英文单词,但还是一个原则:减少输入,如将 GROUP BY,简化成了 BY,但 ORDER BY 我们保持原样,因为它相对不常用。

各个语法结构说明如下:

语法结构

namespace  :查询的数据类型,类似于MySQL中的一个数据库

这里我们借鉴了 C++ 中 namespace 语法,如 std::string str1 = "hello",对DQL 而言,就是形如 object::HOST、metric::cpu、logging::nginx,看起来语义很契合。

在 DataFlux 中,截止目前,已经有如下几种数据类型,故需要在语法层面,对查询的数据做命名空间划分,如:

  1. 时序(metirc/M)
  2. 对象(object/O)
  3. 日志(logging/L)
  4. 事件(event/E)
  5. 安全(security/S)
  6. RUM(rum/R)
  7. APM(tracing/T)
  8. 自定义对象(custom_object/CO)
  9. ...

为便于输入,DQL 对各个命名空间,都做了别名。对于最常用的时序数据(M),甚至可以略去别名,默认就是 M 这个命名空间,进一步简化了 DQL 的编写。

data-source    :基本查询范围,类似于数据库

以对象为例,这里填写的是对象分类名(class),以时序为例,这里填写的是指标集名称,以日志为例,这里填写的是来源(source),以此类推。

target-clause    :查询的字段列表,类似于表字段

如查询 CPU 指标集的两个字段:M::cpu:(usage_guest, usage_idle) LIMIT 1,表示在时序命名空间(M)中查找指标集为 cpu 的两个指标(usage_guest, usage_idle),且只查询一条。

where-clause  :以{}来表示过滤条件

如查询主机 CPU 空闲率大于 90% 的机器:

cpu:(host) { usage_idle>90 }

注意,这里的过滤条件可以有多个,按照列表语义来处理,以 , 分割,它们之间是 AND 的关系:

# 如下三个语义是等价的

cpu:(host) { conditon1, condition2 }
cpu:(host) { conditon1 AND condition2 }
cpu:(host) { conditon1 && condition2 }

既然有 AND 关系,那就有 OR 关系:

# 如下两个语义是等价的

cpu:(host) { conditon1 || condition2 }
cpu:(host) { conditon1 OR condition2 }

还可以用括号表示条件之间的各种组合:

cpu:(host) { conditon0 AND (conditon1 || condition2) }

time-expr    :时间过滤条件,其表达形式为[start:end:by-interval]

初步看来,这里似乎有一点冗余,比如 time-expr 本质上是一个 where-clause 和 by-clause 的合体,即既指定查询的时间范围,又指定时间范围的分组。之所以将这个语法单独拧出来,主要还是因为,在 DataFlux 的查询中,基于时间的查找以及分组,使用频率极高,几乎所有的查询都有涉及,为了将它们从 where-clause 和 by-clause 中「解放」出来,就单独设计了这个语法单元,我们直接可以在这里实现时间范围过滤以及分组,属于一种「快捷方式」。

另外,这里的 start、end 支持多种时间类型的表示,如:

  • [10h:5m:1m] 表示 10 小时以前至 5 分钟以前的时间范围,将查询到的数据,按照 1 分钟的间隔进行分组。在终端手编写 DQL 时,这样指定时间范围非常方便
  • [1626401634:1626402634:1m] 表示两个 UNIX 时间戳时间范围,也是按照 1 分钟的间隔分组。这样做更便于大多数编程语言的处理
  • [2019-01-01 12:13:14:5m:1w:1d] 表示自 2019-01-01 12:13:14 至一周(1w)前的时间范围,将查询到的数据,按照一天(1d)的间隔分组。这里支持日期格式,主要便于通过 Web 前端的时间控件来指定时间

by-clause  :分组语法(同 SQL 中的 GROUP BY)

oder-by-clause :排序语法

limit-clause    :限制返回数量间

DQL  如何解决一些具体问题

如何控制查询的数据安全?

DataFlux 是一个准 SAAS 平台,简而言之,就是多租户平台。在这种情况下,不能因为某个意外的查询,影响其他租户的数据体验。基于此,需要对不同的租户,有独立的查询空间:

  • 每个独立的工作空间,底层的存储是逻辑隔离的,可以简单理解为,不同租户的数据,是存储在不同的数据库上。而单个 DQL 查询,只能在单个「数据库」上执行查找,不存在「串库」的情况
  • 由于观测数据量极大,极有可能是在指定数据查询的时间范围时,手抖了一下,造成底层存储的巨量 IO 查询,进而影响整个集群的租户。为此,在 DQL 的 HTTP 查询接口上支持时间范围的指定(默认 15 分钟),即使没有指定,DQL 本身也会检查查询的时间范围是否超过系统的设定,这在很大程度上保护的底层的存储系统

是如何辅助 DataFlux 前端开发的

前面提到,DQL 的 HTTP 接口是支持注入的,为便于前端实现各种复杂的数据观测场景,DQL 额外支持如下这些查询参数注入:

  • 返回的最大点数控制:在一些密集绘图的前端页面上,巨量的数据返回,可能导致前端页面卡死甚至奔溃。有了最大点数控制,就能杜绝这种情况
  • 过滤条件注入:这个跟时间范围的注入类似,主要应用在数据权限控制上
  • 排序字段注入:在一些特定的数据页面上,需要对返回的数据,按照指定的字段来排序,比如,返回的主机列表上,虽然默认按照主机名来排序(这个默认的 DQL 写好了),单用户可以选择按照 CPU 或内存使用率来排序,此时就可以在 HTTP 请求参数上额外指定排序字段,覆盖默认 DQL 上的 ORDER BY 字段
  • 禁止多字段返回:在一些 UI 效果上,多列返回是无法绘图的,但为了避免 DQL 真的返回了多列数据,可以对应的 UI 效果上,通过 HTTP 接口禁用多列查询,这样依赖, DQL 解析阶段就能检测到错误,非常便于日常的开发以及调试
  • 额外的其它一些注入,主要也是便于实现数据展示效果,比如深度分页、查询的高亮显示等等

「即时」的数据查询效果

DataFlux 中的数据,大部分都是通过 DataKit 上传的,为此,我们在 DataKit 中内置的 DQL 查询终端。数据采集完后,稍后片刻(考虑到多级缓存、网络传输延迟等因素)即可通过 DQL 查询到刚刚上传的数据,而不用打开 DataFlux 来查看数据。另外,某些情况下,特别是在开发阶段,DataFlux 前端可通过这个命令行终端,来排查一些数据问题

灵活的数据处理

主要体现在如下方面:

  • 方便不同的数据接入:如果有新的数据分类需要接入,扩充一个 namespace 即可。如果有新的存储引擎接入,只需要增加一套对应存储引擎的查询翻译即可
  • 可以对查询到的数据,进行灵活的额外处理。假定 InfluxDB 不支持某个数学处理函数,DQL 查询到数据后,可通过 Golang/Python 等,自定义实现即可。另外,还能跨服务实现数据的多级计算,比如将 DQL 查询到的数据,送给 Function 处理

目前,DataFlux 中绝大多数的数据查询,都是通过 DQL 来实现的,历经近一年的开发迭代,DQL 日趋稳定,功能也日渐强大。随着 DataFlux 业务的不断发展,DQL 也将面临着更大的挑战。


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
相关文章
|
4天前
|
存储 NoSQL 关系型数据库
《数据密集型应用系统设计》 - 数据模型和查询语言
《数据密集型应用系统设计》 - 数据模型和查询语言
18 0
|
4天前
|
存储 SQL 关系型数据库
掌握高性能SQL的34个秘诀🚀多维度优化与全方位指南
掌握高性能SQL的34个秘诀🚀多维度优化与全方位指南
|
4天前
|
存储 监控 负载均衡
InfluxDB最佳实践:数据模型设计与查询优化
【4月更文挑战第30天】本文探讨了InfluxDB的最佳实践,重点在于数据模型设计和查询优化。合理选择字段类型,根据业务逻辑划分Measurement,利用Tags进行索引优化,以及适时数据归档和清理,能有效提升性能。查询优化包括使用索引、精简查询语句、应用聚合函数及限制返回结果。分布式查询和分片适用于大规模数据集,以实现并行查询和负载均衡。这些策略旨在帮助用户优化InfluxDB的性能,进行高效时序数据分析。
|
4天前
|
Cloud Native 关系型数据库 MySQL
云原生数据仓库产品使用合集之在ADB中,如何将源数据的多表(数据结构一致)汇总到一张表
阿里云AnalyticDB提供了全面的数据导入、查询分析、数据管理、运维监控等功能,并通过扩展功能支持与AI平台集成、跨地域复制与联邦查询等高级应用场景,为企业构建实时、高效、可扩展的数据仓库解决方案。以下是对AnalyticDB产品使用合集的概述,包括数据导入、查询分析、数据管理、运维监控、扩展功能等方面。
|
4天前
|
数据库
第四章数据查询基础
第四章数据查询基础
10 0
|
4天前
|
SQL 监控 测试技术
SQL语法优化与最佳实践
【2月更文挑战第28天】本章将深入探讨SQL语法优化的重要性以及具体的优化策略和最佳实践。通过掌握和理解这些优化技巧,读者将能够编写出更高效、更稳定的SQL查询,提升数据库性能,降低系统资源消耗。
|
小程序 数据库 开发者
小程序云开发联表数据查询以及云函数中的应用
1、联表查询 (1)lookup联接两个表格 (2)使用match进行条件查询 (3)直接返回学生成绩平均值 (4)只显示teacher和score这两个值 2、在云函数中的应用 (1)在云数据库中添加数据 (2)创建云函数并初始化数据库 (3)编辑云函数入口函数 (4)上传部署云函数
866 0
小程序云开发联表数据查询以及云函数中的应用
|
6月前
|
SQL 数据挖掘 关系型数据库
数据分析法宝,一个SQL语句查询多个异构数据源
NineData DSQL 是针对多个同异构数据库系统进行跨库查询的功能,当前支持对表和视图进行 SELECT 操作。您可以在一个查询中访问多个数据库,获取分散在各个数据库中的有用信息,并且将这些信息聚合为一份查询结果返回,轻松实现跨多个库、多个数据源,乃至跨多个异构数据源的数据查询。
481 0
数据分析法宝,一个SQL语句查询多个异构数据源
|
存储 算法 数据挖掘
火山引擎:ClickHouse增强计划之“多表关联查询”
火山引擎:ClickHouse增强计划之“多表关联查询”
|
SQL 存储 关系型数据库
MySQL基础教程7——DQL—基础数据查询
使用select 字段名 from 表名;多个字段名之间用,隔开,如果要查询表中所有字段可以用*代替字段名。