NAME

LZSQL Reference Manual - LZSQL 参考手册

LZSQL 概览

LZSQL 是专门为量子数据中间层设计的一种小语言,用于简化中间层的数据计算逻辑的编写以及抽象掉对分布式后端的访问细节。

LZSQL 本质上是一种过程式语言,支持用户变量、赋值语句等常见的语言结构。同时,它又融合了 SQL 的语言成分,从而拥有了自然表达复杂的关系型数据库查询和操作的能力。

LZSQL 是一种强类型的语言,它会对用户定义的变量进行严格的类型检查。其语言编译器的类型检查器(typechecker)有助于发现更多的因类型不匹配而导致的程序 bug 以及安全性方面的漏洞。

LZSQL 语言避免了程序员自己执行繁琐和易错的动态 SQL 的拼串操作,同时从语言级别上确保内联在 SQL 串中的各个变量都能被恰当地转义(quoting),以阻止 SQL 注入的发生。

在 LZSQL 中,SQL 是其一等对象(first-class object),而不像许多其他语言中被表示为用户字符串的形式。所以,SQL 语言和 LZSQL 共享着相当大的一个子集。换句话说,LZSQL 也可以视为 SQL 的一个常用子集的“超集”。

LZSQL 程序员可以直接在 LZSQL 的类 SQL 语句中内联各种类型的 LZSQL 变量,实现动态查询的自然表达。LZSQL 会根据 LZSQL 变量的具体类型检查这些变量是否在 SQL 语句中应用于正确的上下文中,同时也会依据 LZSQL 变量的类型进行运行时的值域检查,以及选择适当的 SQL 转义规则。

在 LZSQL 中,SQL 语句既可以运行在指定的某一个后端上,比如某一个 MySQL 节点,也可以直接运行在 LZSQL 程序的运行时环境中(即本地)。

运行在远方 MySQL 节点上的 SQL 查询当涉及到量子后端规定的按月分表等分表逻辑时,可以享受到 LZSQL 编译器内置的语言级别的支持。LZSQL 程序员无须了解分表的具体细节,也不用自己去处理跨表查询的各种优化细节。LZSQL 会自动针对量子 MySQL 表的按月分表等逻辑,对用户的 SQL 语句进行各种高级优化,而这些优化是 MySQL 服务器本身不能或者不便进行的。关于此类 LZSQL 远方查询的详细讨论,请参见 "远方 SQL 查询"一节。

运行在本地的 SQL 查询是由"LZSQL 运行时库"内置的纯内存的 SQL 引擎完成计算的。这种 SQL 查询一般用于对来自多个数据源的结果集进行复杂的关系型汇总计算,也可用于某些功能较弱的数据源的结果集的后续尾处理。例如,在许多量子基础报表的接口中,经常需要对来自 MySQL 报表数据库和来自 st 实时引擎的数据进行融合操作。另一个例子是,st 实时引擎本身并不支持任何 SQL 性质的自定义查询,也没有像样的数据制筛选和处理界面,所以经常需要通过 LZSQL 本地查询,对 st 实时引擎返回的结果集进行进一步的汇总计算和数据筛选操作。

LZSQL 支持用户内联任意的 Lua 代码,从而允许用户自由插入自己的 Lua 处理逻辑,弥补 LZSQL 语言本身在过程式处理功能上的不足。

中间层开发人员通过 LZSQL 语言编写数据接口的计算逻辑,然后通过调用 LZSQL 编译器,将之编译为最终运行在 ngx_openresty 平台之上的 Lua 代码。

编写 LZSQL 程序

可以使用任意的纯文本编辑器来编写 LZSQL 程序,比如 vi 或者 emacs,甚至 Windows 中的 notepad.

LZSQL 源文件名一般以 .lzsql 作为扩展名。一个 .lzsql 源文件对应一个 LZSQL 编译单元(compilation unit)。若干个 LZSQL 编译单元最终再链接为一个完整的 LZSQL 程序。

LZSQL 代码通过 LZSQL 编译器编译为 Lua 代码。从生成的 Lua 代码上看,一个 .lzsql 源文件,也就是一个 LZSQL 编译单元,会被 LZSQL 编译器编译为一个 Lua 函数的定义,其 Lua 代码会被放置在一个对应的 .oul 文件中。最终,当 LZSQL 编译单元链接为一个 LZSQL 程序的过程,其实就是把这些 LZSQL 编译单元对应的 Lua 函数的定义合并为一个完整的 Lua 模块(或者说 Lua 包)的过程。所以,从输出文件上看,最终链接的过程,就是合并 .oul 文件为一个大的 .lua 文件的过程。一个 LZSQL 程序最终就生成一个 Lua 模块。

一个 LZSQL 程序的大致结构一般是,开头先定义若干程序里用到的 LZSQL 标量变量,然后使用 LZSQL 赋值语言从各种数据源获取结果集数据,并赋值给用户自己定义的 LZSQL 矢量变量,中间可能内联一些 Lua 代码块,对用户自定义的 LZSQL 变量进行值修改或者值读取,最后通过 LZSQL 返回语句将最终的结果集结果作为整个 LZSQL 程序的结果返回。

编译和链接 LZSQL 程序

LZSQL 程序和 C/C++ 程序一样,构造一般需要经历编译(compiling)和链接(linking)这两个过程。

对应地,LZSQL 编译器工具链主要由两个实用程序组成,一是 lzsql-compile,执行编译操作;一是 lzsql-link,执行链接操作.

lzsql-compile 程序接受一个或者多个 .lzsql 输入文件,将它们分别编译为对应的 .oul 目标文件。例如

    lzsql-compile foo.lzsql bar.lzsql

将会生成 foo.oulbar.oul 这两个对应的目标文件。每一个 .oul 目标文件中都存放着对应的 Lua 函数的定义代码。每一个 Lua 函数的名字与对应的 .lzsql 源文件的文件基本名(base name)相同。例如,foo.lzsql 编译成的 foo.oul 目标文件中存放的是一个名为 foo 的 Lua 函数的实现代码。.oul 都是纯文本文件,所以你仍然可以通过你最喜爱的纯文件编辑器打开 <.oul> 文件进行查看。

当用户的 .lzsql 输入文件存在语法或者语义上的错误时,LZSQL 编译器会打印出具体的出错信息以及错误在输入文件中的具体行号信息。你可以从 "LZSQL 编译器错误诊断" 一节查看到所有常见的 LZSQL 编译报错的具体含义。

lzsql-compile 程序支持多种命令行选项,可以通过 -h 选项来查看这些选项的含义和用法。

最常用的还有 -O 选项,可用于指定 LZSQL 代码的优化编译的级别,其级别值越高,优化程度也就越高。一般地,用于生产的 LZSQL 程序,都使用 -O2 选项进行编译。

lzsql-link 程序负责将一个或者多个 .oul 目标文件链接为一个完整的 Lua 模块(.lua 文件)。例如

    lzsql-link -m bah -o blah.lua foo.oul bar.oul

这里,我们将 foo.oulbar.oul 这两个目标文件链接为一个名为 bah 的 Lua 模块,并存放在 blah.lua 文件中。

当然,一般我们都希望 Lua 模块的名字和 .lua 输出文件的基本名(base name)相一致,这样这些 Lua 模块的用户代码就可以直接 require 它们。唯一的例外是当我们在 Lua 模块名中含有圆点(.)的时候,例如

    lzsql-link -m lightface.core -o lightface/core.lua *.oul

这里最终生成的 .lua 文件需要是在 lightface/ 目录下的 core.lua 文件中,以适应 Lua require 函数的要求。

LZSQL 程序经过编译和链接,得到最终的 Lua 模块之后,其运行就不再依赖于 LZSQL 编译器和链接器了。但是,这个 Lua 模块本身的运行还依赖于 "LZSQL 运行时库".

LZSQL 注释

LZSQL 语言支持两种形式的程序注释,即“行注释”和“块注释”。

行注释

LZSQL 支持 Lua 风格的行注释,即以 -- 起始直到行尾都是被 LZSQL 编译器忽略掉的注释文本。

下面是一个例子:

    -- This line is a comment
    -- This is another one ;)

块注释

LZSQL 支持 C/C++ 风格的块注释记法,即使用 /**/ 围起来的文本都会被当作程序注释处理。和 C/C++ 块注释一样,/**/ 之间允许跨越多行,同时也不支持嵌块注释嵌套。块注释总是在第一次出现 */ 的地方结束,而不管 /* 在之前出现过几次。

下面是一个例子:

    /* This is a block
       comment sample in
       the LZSQL language */

LZSQL 值类型与变量类型

LZSQL 是强类型的语言。不仅 LZSQL 的值拥有类型,所有的 LZSQL 变量也拥有类型。这与 Lua 这样的语言是不同的(在 Lua 中,只有值拥有类型,而变量本身是没有类型概念的)。

在 LZSQL 语言中,定义了"布尔类型""文本类型""数值类型"(包含了 "整数类型""实数类型")、"符号类型""位置类型""数组类型""结果集类型"

布尔类型

布尔类型的值只有两个,即 truefalse.

目前不支持定义布尔类型的 "LZSQL 变量"

文本类型

文本类型(text type)对应其他语言中的“字符串”类型,是由一系列字节组成的字节序列。

在 LZSQL 中,文本类型的常量一般是通过单引号(')引起来的字符串来表示的。下面是几个例子:

    'abc'
    'hello, world'
    'he says, "yes!"'

当字符串本身中有字符 ' 的时候,可以使用 \ 进行逃逸,例如

    'he says, \'okay!\''

类似地,当字符串本身出现字符 \ 时,也可以用 \ 进行逃逸,例如:

    '"\\" itself can be escaped by "\\".'

LZSQL 文本类型的值并不携带任何有关字符集编码方面的具体信息。LZSQL 程序员自己负责解释和追踪文本值的具体字符集编码,比如是用 GBK 编码的还是 UTF-8.

文本类型的常量一般用于 LZSQL 的 SQL 表达式中,例如

    @res :=
        select *
        from cats
        where name = 'jummy'
        at some_mysq_node;

声明文本类型的 LZSQL 标量时,使用类型名 text,例如

    text        $keyword;

数值类型

LZSQL 的数值类型(numeric type)包含了"整数类型"实数类型

整数类型

下面是几个整数类型(int type)的值的例子:

    -32
    102
    0

声明整数类型的 LZSQL 标量时,使用类型名 int,例如

    int        $product_id;

实数类型

下面是几个实数类型(real type)的值的例子:

    3.1415926
    -5.28

科学计数法是不支持的,例如 2e16, -5E-21 都是非法的表示。同时,直接以小数点起始或者结尾的实数表示也是不支持的,例如,.32728. 都是非法表示。

目前不能声明实数类型的 LZSQL 标量;这是一个 TODO.

符号类型

符号类型(symbol type)的值对应各种标识符性质的符号,一般用来表示数据库的库名、表名和列名等等。

符号的值须以英文字母起始,后面可以跟若干个可选的英文字母、数字以及下划线(_).

下面是合法符号类型的值的例子:

    cats
    dp_lz_pv_times
    A32_B56

在下面这条 LZSQL 语句中,

    @res :=
        select pv_times, uv_times, day
        from history_table
        where day = '2010-09-01'
        at some_mysql_node;

pv_times, uv_times, day, history_table> 都是符号类型的常量。但是值得注意是,some_mysql_node 并不是符号类型,而是所谓的“位置类型”,详情请见 "位置类型".

符号类型的常量不会跟 "LZSQL 变量"在记法上产生冲突,因为无论是 LZSQL "矢量变量"还是"标量变量",都会在其变量名前加上特殊符号 @ 或者 $.

声明符号类型的 LZSQL 标量时,使用类型名 symbol,例如

    symbol        $column;

符号类型的变量可以用于"远方 SQL 查询""本地 SQL 查询"中原本引用列表、表名或者库名的地方,从而实现动态绑定。下面是一个例子:

    symbol      $database, $table, $column;

    return
        select $column, count(id)
        from $database.$table
        where day = '2010-03-03'
        group by $column;

位置类型

位置类型(location type)专用于 LZSQL 的 at 子句结构中,一般用于表示某种远方的机器节点,但并不同于具体的机器名、机器域名或者 IP 地址。

位置类型的常量值的记法和"符号类型"是完全相同的。区分二者的唯一标准就是它们是否使用于 LZSQL 的 at 子句中。

在实现上,一般对应 nginx 配置文件中定义的 upstream 的名字,是一种抽象的标识,一般已经对应了具体的机器名和端口定义,甚至可能对应着一组具体的远方机器(在此种情况下,这个 upstream 名事实上代表着一个机器集群)。

例如在下面的例子中

    @res :=
        select pv_times, uv_times, day
        from history_table
        where day = '2010-09-01'
        at some_mysql_node;

只有 some_mysql_node 是位置类型的值。

声明位置类型的 LZSQL 标量时,使用类型名 location,例如

    location        $report_db;

位置类型的变量可以用于动态绑定某一条"远方 SQL 查询"所运行的具体的后端节点。例如

    location        $some_db;

    return
        select count(*)
        from cats
        at $some_db;

数组类型

数组类型(array type)是一种聚集类型,在"文本类型""数值类型", 和 "符号类型" 的基础之上定义的有序集合类型。

下面是几个数组类型定义的 LZSQL 标量变量的例子:

    int[]       $ids;
    symbol[]    $columns;
    text[]      $names;

数组类型的变量可用于选择不定数量的列,一般用于"远方 SQL 查询"中的 select 子句和 group by 子句,例如:

    symbol[]    $cols :local;

    %%lua s_cols = { 'name', 'age' }

    return
        select $cols
        from cats
        at some_mysql_node;

这里,我们通过"内联 Lua 语句"symbol[] 类型的"标量变量" $cols 进行初始化,即初始化为一个拥有两个列名元素的 Lua table. 然后我们在一个"远方 SQL 查询"中引用了此 $cols 变量。

下面是一个在 group by 子句中引用数组类型的标量的例子:

    symbol[]    $cols :local;

    %%lua s_cols = { 'name', 'age' }

    return
        select sum(rate)
        from cats
        at some_mysql_node
        group by $cols
        at some_mysql_node;

结果集类型

结果集类型(resultset type)的值是一种二维表结构,一般用来表示数据库查询的结果,或者本地 SQL 查询所操作的内存数据表。在目前的 LZSQL 编译器的实现中,结果集类型的值被表示为一种嵌套结构的 Lua table. 下面是一个例子:

    {
        { name = "Marry Green", age = 64, is_female = true },
        { name = "Job Steve", age = 58, is_female = false },
        { name = "Yichun Zhang", age = 26, is_female = false },
    }

在这个结果集中,一共有三行记录(record),每一行记录又是由一个 Lua table 构成,且均有 name, age, 和 is_female 这三个字段名(或称为“列名”)。

在用户内联 Lua 代码中,可以通过对 LZSQL 矢量变量的引用,直接对这种结果集类型的值进行直接操纵。

空的结果集被表示为空的 Lua table,用 Lua 代码来表示就是 {}.

NULL 值被表示为 Lua 中的 _e.null,而并非 Lua 语言中的 nil. 这里的 Lua 变量 _e 是 LZSQL 编译器自动生成和初始化的,对应着 lz.sqlengine 这个 Lua 包的实例。

结果集类型的 LZSQL 变量又称为"矢量变量";其他非结果集类型的变量又称为"标量变量"

LZSQL 变量

LZSQL 语言和其他面向过程的语言一样,支持用户自定义的变量。变量是值的容器。特定类型的变量只能存放相同类型的值。

LZSQL 变量分为两大类,一是"矢量变量",一是"标量变量"

矢量变量

LZSQL 矢量变量用来存放"结果集类型"的数据。

矢量的变量名的开头都由字符 @ 来修饰,类似 Perl 语言中的变量名特殊前缀(sigil). 矢量变量名本身由一个或者多个字母、数字和下划线组成,但须以英文字母起始。

矢量变量的几个例子是 @abc, @A1_B2, 和 @f.

矢量变量不需要在使用前声明和定义。它们一般通过 LZSQL 赋值语言直接获得值,或者直接通过用户内联的 Lua 代码中被初始化。下面是几个例子:

    -- assign the result set of a remote mysql query to @foo
    @foo := select * from cats at some_mysql_node;

    -- constructs an empty result set and store it in @foo
    @foo := lua { return {} };

矢量变量可以在用户内联在 LZSQL 中的 Lua 代码中访问。所有的矢量变量都会被编译为与之对应的 Lua 变量。例如,@foo 会被编译为 Lua 变量 a_foo,即在矢量变量的变量名(不含 @)之前添加 a_ 前缀。a_ 前缀取意自“at”这个单词,对应 @ 符号。

LZSQL 程序员应当避免在内联 Lua 代码中定义自己的以 a_ 前缀起始的 Lua 变量,以防止和 LZSQL 矢量变量名冲突。

LZSQL 矢量变量在定义后,必须在当前的 .lzsql 源文件中使用,即须在某一条"本地 SQL 查询"中被引用,或者在某一个 Lua 内联代码块中通过其编译后的 Lua 变量名(即 a_xxx)被引用。否则,LZSQL 编译器会报错。注意,出现在"赋值语句"“左手边”(left-hand side)的矢量名并不被认为是引用矢量。

标量变量

LZSQL 标题变量是指那些不是"结果集类型"的变量。标量变量在引用时都以字符 $ 在开头修饰(就像"矢量变量"在引用时都以 @ 修饰一样)。

标量变量在使用之前必须定义。标量变量的定义语法是

    <变量类型> <变量名1>, <变量名2>, ... ;

这里有一个例子:

    text    $prod_id;

这里我们定义了一个"文本类型"的标量变量 $prod_id.

标量变量可以在用户内联在 LZSQL 中的 Lua 代码中访问。所有的矢量变量都会被编译为与之对应的 Lua 变量。例如,$foo 会被编译为 Lua 变量 s_foo,即在标量变量的变量名(不含 $)之前添加 s_ 前缀。s_ 前缀取意自“scalar”这个单词,对应 $ 符号。

LZSQL 程序员应当避免在内联 Lua 代码中定义自己的以 s_ 前缀起始的 Lua 变量,以防止和 LZSQL 标量变量名冲突。

默认情况下,标量变量都会从外部接收同名的参数来初始化。举例来说,假设我们有一个 foo.lzsql 源文件,其中定义了下面这几个标量:

    text        $a, $b;
    int         $c;

然后我们通过 LZSQL 编译器将之编译为一个 foo.oul 文件,并最终链接为一个名为 Bar 这样的 Lua 模块。在 foo.oul 文件中包含有一个叫做 foo 的 Lua 函数,它的骨架类似下面这个样子:

    function foo (_args)
        local s_a = _args.a
        local s_b = _args.b
        local s_c = _args.c

        ...
    end

所以在从外部调用这个 Lua foo 方法时,调用者负责传递参数 a, b, c,同时确保它们拥有对应类型的值,例如:

    local bar = require "Bar"
    local res = bar.foo({ a = "Hello", b = "World", c = 56 })

在 LZSQL 编译器生成的 foo 函数中,还会对传入的 a, b, c 这三个参数的值进行对应类型的合法性检查。比如,当调用者传入 c 的值为非法的整数时,foo 函数会抛出一个运时行异常。

LZSQL 程序员可以标记某一个 LZSQL 标量为“本地变量”,即该变量并不通过调用者通过参数传入来初始化,而是在用户内联 Lua 代码块中进行初始化。标记的方法是在标量定义时,在标量变量名之后加上 :local 属性标记,例如:

    text        $a :local, $b;
    int         $c :local;

此时,变量 $a$c 都被声明为了“本地变量”,不再从外部参数传入初始化值;而变量 $b 仍然从外部传入初始化值。于是,在这种情况下,LZSQL 编译器生成的 Lua foo 函数就变成类似下面这个样子:

    function foo (_args)
        local s_a, s_c
        local s_b = _args.b

        ...
    end

LZSQL 标量变量在定义后,必须在当前的 .lzsql 源文件中使用,即须在某一条 SQL 表达式中被引用,或者在某一个 Lua 内联代码块中通过其编译后的 Lua 变量名(即 s_xxx)被引用。否则,LZSQL 编译器会报错。

LZSQL 语句

LZSQL 程序由若干个 LZSQL 编译单元构成。每一个 LZSQL 编译单元对应一个以 .lzsql 作为扩展名的源文件。而每一个 .lzsql 源文件的代码在最高层面上,又是由若干条 LZSQL 语句构成的。

变量声明语句

在 LZSQL 中,只有标量变量需要声明或者说定义。在 LZSQL 中,是不区分变量声明与变量定义的。

标量变量的声明语法,请参见"标量变量"一节。

赋值语句

目前在 LZSQL 中,只支持对"矢量变量"进行赋值。

产生结果集结果的表达式都可以作为赋值语句的“右手边”(right-hand side)。例如把一个"远方 SQL 查询"的结果赋给一个矢量变量:

    @var :=
        select id, name
        from cats
        where age > 32
        at some_mysql_node;

也可以把一个"内联 Lua 表达式"的值赋给一个矢量变量,例如:

    @var := lua { return generate_some_resultset() };

返回语句

LZSQL 返回语句的语法是

    return <LZSQL Expression>;

作为返回值的 LZSQL 表达式返回的是当前 LZSQL 编译单元的返回结果。

例如,考虑下面的 foo.lzsql 文件:

    return lua { return { name = "Amy", age = 18 } };

当将 foo.lzsql 源文件编译为 foo.oul 目标文件,并最终链接为名为 Bar 的 Lua 模块之后,调用者执行代码

    local bar = require "Bar"
    local res = bar.foo(...)

则 res 变量中将存放着 foo.lzsql 这个编译单元的返回值,即 Lua table

    { name = "Amy", age = 18 }

也可以直接返回某一个矢量变量的值,例如

    return @foo;

或者某一个 "本地 SQL 查询" 或者 "远方 SQL 查询",例如

    return @history_data union all @realtime_data;

或者

    return select * from cats at some_mysql_node;

内联 Lua 语句

内联 Lua 语句允许用户在 LZSQL 语句级别上插入任意的 Lua 代码片段。LZSQL 编译器并不会检查这些 Lua 代码的合法性,所以依赖 LZSQL 程序员自己去维护这些 Lua 代码。

内联 Lua 语句中的 Lua 代码会被直接发射到目标 .oul 文件中的对应上下文中。

内联 Lua 语句主要有两种形式:

行内联 Lua 语句

通过 %%lua 起始的行被识别为“行内联 Lua 语句”。

下面是一个例子:

    %%lua if s_animal == "dog" then

        return select count(*) from dogs at some_mysql_node;

    %%lua else

        return select count(*) from others at some_mysql_node;

    %%lua end
块内联 Lua 语句

在 LZSQL 语句级别上通过 lua { ... } 指定的表达式是“块内联 Lua 语句”。下面是一个例子:

    int    $type, $type_id :local;

    lua {
        if s_type == "dog" then
            s_type_id = 3
        elseif s_type == "cat" then
            s_type_id = 4
        else
            error("...")
        end
    }

    return
        select *
        from animals
        where type_id = $type_id;

注意,块内联 Lua 语句末尾是没有分号的。

块内联 Lua 语句和"内联 Lua 表达式"拥有相似的语法,但是二者的使用上下文不同,因此其语义亦有区别。

LZSQL 表达式

LZSQL 表达式构成了 LZSQL 语言中最重要的组成部分。通过 LZSQL 表达式我们既可以表达强大的 SQL 语义的数据计算逻辑,也可以通过内联 Lua 代码来进行任意复杂度的求值计算。

LZSQL 表达式都可以直接用于"赋值语句""返回语句" 中。

LZSQL 支持的 SQL 查询支持 contains 运算符,这不是 SQL 标准的一部分,它用于在一个数据列中查找是否出现一个查询串。例如下面的例子:

    text        $keyword;

    return
        select *
        from cats
        where name contains $keyword
        at some_mysql_node;

这里我们在 cats 表中寻找 name 列中包含 $keyword 指定的子串的记录。

根据目前的实现,上面的 LZSQL 的 SQL 查询在 keyword 参数是字符串 abc 时,会被编译为类似下面的 mysql 查询

    select *
    from cats
    where name like '%abc%'

远方 SQL 查询

在 LZSQL 中可以直接表达运行于某个远方后端的 SQL 查询。

LZSQL 编译器会解析这些查询,识别用户直接嵌入这些查询中的 LZSQL 标量变量,执行可选的查询优化操作,最后再生成最终的 MySQL 查询(或者到 OceanBase 等后端的具体调用)。

远方 SQL 查询须携带一个 at 子句,一个例子是

    select *
    from cats
    where name = 'Amy'
    at report_db;

这里通过 at 子句指定当前查询须运行于远方的 report_db 这个 upstream 后端。关于如何确定这里的 upstream 后端名,请咨询负责项目 nginx.conf 配置文件编写的工程师。

at 子句并不允许在 SQL 子查询中使用,只能用于最外层的 SQL 主查询。

在 LZSQL 远方查询中可以使用 LZDBLZSDB 这一类特殊名字空间下的“SQL 函数”。这些特殊的名字空间并不对应任何远方 MySQL 后端中的数据库,相反,它们是 LZSQL 针对量子分表逻辑的特殊封装。

在远方查询中不允许使用矢量变量作为数据源。LZSQL 编译器会在编译期对此进行检查。

LZDB 函数

"远方 SQL 查询"中可以使用 LZDB 这一特殊名字空间下的函数。这个名字空间的解释是由 LZSQL 编译器本身来规定的。它的引入是为了抽象掉量子基础报表库为代表的按月分表的细节。

在量子基础报表和直通车报表的 mysql 按月分表的规则中,每个月的数据会放在单独的表中(不过此表亦会有多个用户共享),例如,

    dp0001.dpunit_purl_result__201003
    dp0001.dpunit_purl_result__201004
    dp0001.dpunit_purl_result__201005

分别对应 2010 年 3 月、4 月和 5 月的数据表。同时,“今天”当天的数据并不存储在 mysql 中(它们是从实时引擎后端取得的)。

我们来看一个例子。假设我们有下面的 purl_result.lzsql 文件的定义:

    symbol  $db;
    text    $begin, $end;

    return
        select url_index, pv_times
        from LZDB.dpunit_purl_result($db, $begin as begin, $end as end, 54848 as uid) as a
        at some_mysql_node;

这里,LZDB.dpunit_purl_result 函数指定我们去访问 dpunit_purl_result__YYYYMM 这些表,而 YYYYMM 月表后缀取决于查询的时间段,即 $begin$end 这两个变量所指定的日期区间。

LZDB.dpunit_purl_result 函数可以携带下面这些按名传递的参数:

db 参数

在上例中取值为 $db,指定了我们访问哪一个 mysql 的“数据库名”。注意库名不同于 mysql 机器名,因为一个 mysql 服务器实例允许有多个 mysql 数据库。

begin 参数

指定当前查询的起始日期,在上线中取值为 $begin.

end 参数

指定当前查询的终止日期,$end.

uid 参数

这个参数则指定了当前查询的用户的“unit id”,即量子店铺统计的卖家 id,在上例中取值 54848.

本参数在 no_uid 参数为 true 时不得指定,否则则是必需的。

max_days 参数

此参数指定了 begin 参数end 参数 指定的日期区间最多允许跨越的天数。该参数是可选的,默认是 31.

no_uid 参数

此参数接受 truefalse 这两种布尔类型的值,用于控制是否需要传递 uid 参数.

当此参数传递 true 值时,是不允许传递 uid 参数的。在生成的 mysql 查询中,也不会引用 unit_id 这一列。

本参数是可选的,默认值是 false.

由于是按名传递,这四个参数的传递顺序可以是任意的。特别地,当传递的实参值是一个"标量变量"并且该标量变量的名字(不含 $ 前缀)和当前参数的名字相同时,“as 参数名”部分可以省略。于是使用此简写技巧后,上面的 purl_result.lzsql 文件可以改写为下面的形式:

    symbol  $db;
    text    $begin, $end;

    return
        select url_index, pv_times
        from LZDB.dpunit_purl_result($db, $begin, $end, 54848 as uid) as a
        at some_mysql_node;

注意这里 db, begin, 和 end 参数在传递时,分别省略了 as db, as begin, 和 as end 部分;而另两个参数的 as 别名部分不可省略。

所有的 LZDB. 函数都使用相同的调用协议.

LZSQL 编译器并不限制 LZDB. 名字空间下的具体的函数名;用户可以自由指定。

现在我们编译一下上面的 purl_result.lzsql 源文件:

    lzsql-compile -O2 purl_result.lzsql

注意这里我们通过 -O2 选项,启用了最高级别的优化。

如果一切顺利,我们可以得到一个 purl_result.oul 文件。我们再链接它为一个完整的 LZSQL 程序:

    lzsql-link -m test -o test.lua purl_result.oul

现在我们在 apiproxy 项目的 nginx.conf 的 web service 接口定义中这样去调用它:

    location = /test {
        content_by_lua '
            local t = require "test"
            t.purl_result({
                ["begin"] = ngx.var.arg_begin,
                ["end"]   = ngx.var.arg_end,
                ["db"]    = "dp0001",
            })
        ';
    }

我们试着去调用它:

    curl localhost/test?begin=2010-03-14&end=2010-03-20

如果我们打开调试选项,可以看到 LZSQL 运行时生成的最终的 mysql 查询是下面这个样子的(经过简单的重新排版,以便于阅读):

    select url_index, pv_times
    from (dp0001.dpunit_purl_result__201003 as a)
    where unit_id = 54848 and day between '2010-03-14' and '2010-03-20'

前面提到,由于“今天”的数据不可能放在 mysql 数据库中,所以当日期区间包含今天时,今天这个日子会先被 "LZSQL 运行时库"剔除。假设“今天”的日期是 2010-09-01,则请求

    curl localhost/test?begin=2010-08-14&end=2010-09-01

实际生成和运行的 mysql 查询中会忽略“今天”:

    select url_index, pv_times
    from (dp0001.dpunit_purl_result__201008 as a)
    where unit_id = 54848 and day between '2010-08-14' and '2010-08-31'

当查询的日期区间跨越多张月表时,LZSQL 编译器可以对我们的"远方 SQL 查询"自动进行优化,只要我们启用了 lzsql-compile-O 选项。(前面我们在编译我们的 purl_result.lzsql 源文件时启用了 -O2 优化选项。)下面是一个例子:

    curl localhost/test?begin=2010-07-25&end=2010-08-24

生成和运行的 mysql 查询便成为下面这个样子:

    select url_index, pv_times
    from ((
        (select pv_times, url_index
         from dp0001.dpunit_purl_result__201007
         where unit_id = 54848 and day >= '2010-07-25'
        ) union all
        (select pv_times, url_index
         from dp0001.dpunit_purl_result__201008
         where unit_id = 54848 and day <= '2010-08-24')
    ) as a)

我们看到,这个查询涉及到两张月表,LZSQL 先在每一张月表中进行查询,然后再把两个子结果 union all 为最终的结果集。由于前面在编译 purl_result.lzsql 文件时启用了优化选项,我们看到,这里 "LZSQL 运行时库"生成的 SQL 通过下放约束条件,尽量减少子表查询的结果集的大小,从而最大化总体查询的效率。

对于使用了聚集函数的复杂查询,LZSQL 编译器也能进行高级的查询优化。假设我们把上例中的 purl_result.lzsql 文件改成下面这个样子:

    symbol  $db;
    text    $begin, $end;

    return
        select url_index, sum(pv_times) / count(pv_times) as pv_times
        from LZDB.dpunit_purl_result($db, $begin, $end, 54848 as uid) as a
        group by url_index
        at some_mysql_node;

则下面的请求

    curl localhost/test?begin=2010-07-25&end=2010-08-24

将生成下面这样的 mysql 查询:

    select url_index, sum(_x1) / sum(_x2) as pv_times
    from ((
        (select count(pv_times) as _x2, sum(pv_times) as _x1, url_index
         from dp0001.dpunit_purl_result__201007
         where unit_id = 54848 and day >= '2010-07-25'
         group by url_index) union all
        (select count(pv_times) as _x2, sum(pv_times) as _x1, url_index
         from dp0001.dpunit_purl_result__201008
         where unit_id = 54848 and day <= '2010-08-24'
         group by url_index)
    ) as a)
    group by url_index

我们看到复杂的含有聚集函数的表达式也能被 LZSQL 编译器成功地拆分到子表子查询中。这种复杂的拆分聚集函数计算的高级优化只有在启用 lzsql-compile 编译器的 -O2 优化选项时才会进行。

LZDB 函数可以使用变量作为其具体的函数名,从而实现迟绑定,例如:

    symbol      $table, $db;
    int         $uid;
    text        $begin, $end;

    select buyerid
    from LZDB.$table($db, $begin, $end, $uid)
    at some_mysql_node;

但是,值得指出的是,LZDB 这个名字空间本身是不能通过变量来实现迟绑定的,因为 LZDB 的实现大部分都依赖编译器,而非"LZSQL 运行时库".

LZDB 函数间的连接操作

受实现上的限制,下面这种写法在 LZSQL 编译器启用优化选项编译时会报错:

    symbol  $db;
    text    $begin, $end;

    return
        select url_index, pv_times
        from
            LZDB.foo($db, $begin, $end, 54848 as uid) as a
            left join
            LZDB.bar($db, $begin, $end, 54848 as uid) as b
            on a.day = b.day
        at some_mysql_node;

使用 lzsql-compile 通过 -O1 或者 -O2 优化选项编译时,会报下面这个错误:

    ERROR: Failed to optimize "LZDB.foo" function (please consider putting it into a single "from" clause) at a.lzsql line 7.

那个警告的意思是说它不能对 LZDB.foo 函数进行优化,建议用户把 LZDB.foo 放到一个单独的 from 子句中。换言之,使用子查询就可以让 LZDB.foo 单独出现在一个 from 子句中,即这个子查询的 from 子句。

LZSQL 编译器只能对单独出现在一个 from 子句中的 LZDB.foo 函数进行查询优化。

上面的示例使用子查询改写成下面的形式就可以通过编译了:

    symbol  $db;
    text    $begin, $end;

    return
        select url_index, pv_times
        from
            (select url_index, pv_times
             from LZDB.foo($db, $begin, $end, 54848 as uid) as a)
            left join
            (select url_index, pv_times
             from LZDB.bar($db, $begin, $end, 54848 as uid) as b)
            on a.day = b.day
        at some_mysql_node;

下面这个例子也不能通过编译:

    symbol $db;
    text $begin, $end;
    int $uid;

    return
        select *
        from (LZDB.foo($db, $begin, $end, $uid) as a)
        at blah;

去掉 from 子句里多余的括号就可以了:

    symbol $db;
    text $begin, $end;
    int $uid;

    return
        select *
        from LZDB.foo($db, $begin, $end, $uid) as a
        at blah;

LZSDB 函数

量子店铺统计的销售报表业务使用了的 mysql 数据库的分表规则不同于 "LZDB 函数"所抽象的形式。

在销售报表的数据逻辑中,并不允许用户指定任意的日期区间。相反,日期区间被限制在“周”(7 天),“月”(28 天至 31 天不等),“日”(1 天)这三种情况,对应着所谓的“周表”,“月表”和“日表”这三种不同类型的 mysql 数据表。用户在查询时,仅指定起始日期。同时,不存在一个 查询需要跨越多张表的情形。

为了抽象掉这种类型的分表逻辑,LZSQL 语言在 "远方 SQL 查询"中引入了特殊的 LZSDB 函数。

下面是一个例子:

    -- itembuy.lzsql
    text    $begin;
    symbol  $db;

    return
        select buyerid, trade_repeat
        from LZSDB.dpunit_sr_buyer_info_d($db, $begin as day)
        where unit_id = 54848 and day = $begin
        order by alipay_trade_amt
        at some_mysql_node;

我们使用 lzsql-compile 将这个 itembuy.lzsql 源文件编译为 itembuy.oul 目标文件:

    lzsql-compile -O2 itembuy.lzsql

再链接之为 test.lua 模块文件:

    lzsql-link -m test -o test.lua itembuy.oul

然后我们再在 apiproxy 项目的 nginx 配置文件中定义下面的测试用的 web service 接口:

    location = /test {
        content_by_lua '
            local t = require("test")
            t.itembuy({
                begin = "2010-08-30",
                db    = "dp0001",
            })
        ';
    }

我们使用 curl 来测试之:

    curl localhost/test

在 apiproxy 项目的调试选项打开时,在 nginx 的 error.log 中可以看到最终生成的 mysql 查询:

    select buyerid, trade_repeat
    from (dp0001.dpunit_sr_buyer_info_d__201008)
    where unit_id = 54848 and day = '2010-08-30'
    order by alipay_trade_amt

LZSDB 函数可以使用变量作为其具体的函数名,从而实现迟绑定,例如:

    symbol      $table, $db;
    int         $uid;
    text        $begin, $end;

    return
        select buyerid
        from LZSDB.$table($db, $begin as day)
        at some_mysql_node;

但是,值得指出的是,LZSDB 这个名字空间本身是不能通过变量来实现迟绑定的,因为 LZSDB 的实现大部分都依赖编译器,而非"LZSQL 运行时库".

本地 SQL 查询

LZSQL 编译器和运行时支持在 Web 服务器本地内存中直接执行和 mysql 兼容的 SQL 语句,可以直接操纵"矢量变量".

下面是一个例子:

    return
        select day, sum(pv_times) as pv, sum(uv_times) as uv
        from (@hist union all @rt)
        group by day
        order by day;

这里,我们通过本地查询操纵"矢量变量" @hist@rt 中的结果集.

因为这样的查询是在 nginx 进程空间内执行的,所以在 LZSQL 编程中需要特别注意的一个问题是尽量让 @hist@rt 这样的"矢量变量"中的结果集尽可能地小,以减少对前端机内存的压力。

内建函数

本地 SQL 查询中只允许使用 LZSQL 语言规定的 SQL 函数。下面是一个比较完整的列表:

count

同 SQL 标准中的 count 聚集函数。

sum

同 SQL 标准中的 sum 聚集函数。

proj

完成值的多值映射功能,考虑下面的例子

    select proj(name, null, '-', '', '-') as name
    from @data

这里,当 name 列的值为 null 或者为空串 '' 时,proj 函数都返回字符串 '-',否则则返回 name 列的值本身。

本函数一般用于量子的下载数据接口。对于 JSON 格式的接口,涉及展现的处理都应尽可能地在前端 JavaScript 等代码中进行。

cn_hms

用于将秒为单位的数值时间值,格式化为中文的时间格式。例如本地查询

    select cn_hms(3663) as time

会返回

    { { time = '1 小时 1 分 3 秒' } }

本函数一般用于量子的下载数据接口。对于 JSON 格式的接口,涉及展现的处理都应尽可能地在前端 JavaScript 等代码中进行。

concat

同 MySQL 的 concat 函数。

floor

同 SQL 标准中的 floor 聚集函数。

ceiling

同 SQL 标准中的 ceiling 聚集函数。

percent

接受一个数值类型的参数,返回第一个参数的百分比表示。例如

    select percent(0.32152) as val

返回结果集

    { { val = '32.15%' } }

默认情况下,保留百分比值的两位小数。调用者可以传递第二个参数来控制保留的小数位数,例如

    select percent(0.32152, 1) as val

返回的结果是

    { { val = '32.2%' } }
round

接受一个数值类型的参数,返回第一个参数的四舍五入后的结果。例如

    select round(4.321) as val

返回结果集

    { { val = 4 } }

默认情形下,是对整数部分作舍入。可以通过第二个参数来指定保留的小数部分的位数,例如

    select round(4.325, 2) as val

返回

    { { val = 4.33 } }

LZRTI 函数

量子店铺统计的中间层经常需要访问实时引擎中的实时数据。为此,LZSQL 语言提供了内建的支持,通过提供 LZRTI 名字空间下的函数。

LZSQL 语言会自动把用户调用的 LZRTI 名字空间下具体的函数名映射到同名的实时接口. 具体的实时接口的名字,参数及返回结果集的定义在 git@git.corp.taobao.com:lz/lz-st-nginx-module 项目的 spec/lzrti.tcp 文件.

LZRTI 函数只能在本地 SQL 查询中使用。"远方 SQL 查询"中是不允许使用这个名字空间下的函数的,如果使用了,编译器会报错。

下面是一个具体的例子:

    text    $end;
    int     $uid;

    @rt :=
        select $today as day, sum(pv_times) as pv_times, sum(uv_times) as uv_times
        from LZRTI.getD($end as day, $uid as uid);

LZRTI 函数接受下面几个按名传递的参数:

day

指定查询的日期。只有当此参数的值与“今天”的日期相同时,"LZSQL 运行时库"才会实际访问量子实时引擎,否则直接返回空结果集。

uid

此参数用于指定查询的店铺 ID,即 unit id.

cluster

此参数是可选的,用于指定查询的实时引擎集群的名字。黙认值为 std. 例如在无线报表业务中,经常需要这么写

    LZRTI.getD($end as day, $uid, 'mobile' as cluster)

这里就指定了 mobile 这个实时引擎集群。

LZDB 函数相同,当传递的参数值就是一个"标量变量"并且此变量的名字与当前参数的名字相同时,“as 参数名”部分可以省略。例如上例可以改写成下面的形式:

    text    $end;
    int     $uid;

    @rt :=
        select $today as day, sum(pv_times) as pv_times, sum(uv_times) as uv_times
        from LZRTI.getD($end as day, $uid);

即这里的 as uid 部分是可以省略的。

内联 Lua 表达式

LZSQL 语言支持表达式级别的用户内联 Lua 代码。表达式级别的 Lua 代码块会被放在一个自动生成的 Lua 闭包中,例如下面的 LZSQL 代码片段

    @foo := lua { ... };

会被 LZSQL 编译器展开为类似下面的 Lua 代码:

    a_foo = function () ... end

同时,使用在这里的用户 Lua 代码块需要自己返回结果。例如

    @foo := lua { return {} };

这样在效果上,我们这里把一个空结果集赋值给了"矢量变量" @foo.

LZSQL 运行时库

"LZSQL 运行时库"是由一系列纯 Lua 编写的 Lua 模块组成的;它们一般随 LZSQL 编译器和链接器一起发布。

具体来说,运行时库目前是由下列 Lua 模块构成的:

lzsql

在 LZSQL 编译器生成的 Lua 代码中,该 Lua 模块通过 require 加载后,对应 Lua 变量 _rt.

csv

量子的下载接口都是生成逗号分隔(CSV)格式,为此,运行时库提供了这个函数用于简化从结果集类型的值生成 CSV 格式的字符串。

下面是一个例子:

    return lua {
        local dataset = {
            { name = 'Tom', age = 32 },
            { name = 'Bob', age = 18 },
        }
        return _rt.csv(dataset, { 'name', 'age' })
    };

这里会返回类似这样的字符串结果:

    Tom,32
    Bob,18
lz.sqlengine

在 LZSQL 编译器生成的 Lua 代码中,该 Lua 模块通过 require 加载后,对应 Lua 变量 _e.

附录

LZSQL 语言文法

XXX TODO

LZSQL 编译器错误诊断

XXX TODO

关于本文档

本文档的 POD 源码位于 git@git.corp.taobao.com:lz/lzsql 项目的 doc/lz-manual.pod 文件中。

章亦春 <chunlai@taobao.com> 编写了本文档的初稿以及 LZSQL 编译器的最初实现。

支家乐和郭颖为 LZSQL 语言最初的设计贡献了许多智慧,同时他们俩在短短几个月的时间内,使用 LZSQL 语言完成了店铺统计 3.0 第一批近百个老接口的改写工作。