知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • DTCC 2025 的 AI 部分观察

    ·

    上一次写DTCC已经是15年前了(参考:DTCC关于MySQL的未来),今天又有写一点什么的冲动了。因为要“练摊”,所以也只能是“部分”观察。

    AI 到底会如何改变数据库领域

    这次会场上,对于“佰晟智算”和“银联”在AI方向的一些探索关注的多一些,其他时间,则主要是在NineData展台“练摊”。说说一些感受吧。“佰晟科技”主要聚焦于国产化数据库的优化、监控管理等方向,最为亮眼的创新在于,将大模型的能力与“运维知识”深度的结合起来。白鳝很早就在给产品做市场预热了,所以,在产品推出比较短的时间能,就快速的获得了一些早期的种子客户。

    “银联”则是在内部的数据库管理上,做了很多的探索。银联有非常强的自主研发能力,也做了很多的智能化探索,包括Text2SQL、SQL的性能诊断等。Text2SQL很多的企业做了探索,但由于表名、列名的识别对于大模型来说,是非常困难的(一方面由于列可能非常多、而且命名比较简短),会让大模型出现非常强的幻觉问题,这使得在复杂OLTP场景,Text2SQL依旧难以胜任。但对于一些较为简单的场景,例如比较比较少,表、列都使用非常规范的时候,对于部分开发者,依旧有帮助。也注意到,有很多的企业在BI或者“取数”、“报表”场景,做了非常多的探索。

    在SQL优化的方向,AI的能力,已经得到了开发者比较一致的认可,大模型虽然可能会给出一些不太实用的建议,但是“正确”的优化建议,也总是在大模型给出的建议列表之中。这对于,DBA渐少的时代,对开发者来说,确实非常友好。

    NineData 在这个方向上,也做了很多探索,从最早发布ChatDBA以来,后续持续在“DDL转换”、“SQL 优化”、“Text2SQL”、“国产化转换”等方向去尝试,这些功能随着基础大模型的增强,以及辅助以各种优化,确实可以让开发者的数据库管理变得简单一些。

    Memobase

    还听了一场 Memobase 的分享,是一个关于大模型“memroy”的产品。创始人非常技术,整个介绍听下来,如果稍不留神,甚至不知道演讲者是来介绍 Memobase 的,而是把业界的“memroy”产品以及相关的技术栈介绍了清楚。本以为这可能是一个略微冷门的话题,但从现场的问答环节来看,开发者们在这个方向上有很多的问题要解决。

    这个是一个依旧在快速发展的方向。无论是Memobase还是Mem0,这类产品,与数据或者数据库的关系比较有趣的。简单的数据存储,即便是多模态存储,是无法简单的解决此类问题的。此类问题,当前,依旧是比较偏场景化的解决方案,例如,当天讨论的最多的,包括问答机器人、智能对话陪伴等。这些具体的场景下,产品需要深入到场景之中,才能解决开发者的问题。至于存储方案,可能不是当前最为紧迫的问题,所以存储上,可以用S3、也可以用“EloqData”、也可以考虑类似于MongoDB等其他的产品。

    Memobase 比较强调自己在 Latency 方面的优势,但是,目前来看,在一个Chat的场景下,多个百毫秒的Latency,似乎并不是问题。

    之所以,这里比较强调多模态的存储,主要在于:在这个场景中,通常会使用如下的方案,包括,使用 Graph 存储一些关联关系,例如一个人的朋友、“属性”等信息;还会大量使用json存储诸如profile、conversation 历史等信息。此外,这类方案,与RAG类似,也非常依赖注入Embedding、bm25等相似搜索,用于处理历史消息等。

    总得来说,是一个场景化的,混合的存储方案,去应对业务场景。

    创新与迭代是唯一出路

    这次在现场也与很多朋友讨论了 AI 对数据库从业者(不限于)的影响,大家也都有着类似的看法,如果你不是做基础大模型的,那么,基本上,如果你能够更好的使用 AI,那么就有可能开发者出更好的产品;如果你的产品,能够更好的使用AI的能力,你的产品可能会在市场上有更强的竞争力。

    对于开发者来说,确实应该更加积极、甚至激进的去拥抱 AI 技术。LLM从出圈到现在,一共也就两三年时间,所以,“大家的起点都一样,不要犹豫,往前跑就可以了”。

    这个说法在当初ChatGPT刚出来时,Google也有类似的论断类似:““我们没有护城河,OpenAI也没有(We Have No Moat, And Neither Does OpenAI)”。事实上,经过也就两年的时间,Google Gemini 的能力、体验与市场,已经逐步在赶上ChatGPT。

    另一方面,现在整个社会最多的风投资金、最聪明的人都聚集到了这个领域,这个领域的发展和变化,可以说是“日新月异”,这个领域一定会出现很多新的商业模式和企业。但如果,跑得晚了,后面的追赶会更加吃力。从最近的Zack如此大价钱的挖掘 AI 人才,也可能看出,即便是,最头部的厂商,在这个势头下,也是非常焦虑的。

    向量数据库是AI还是数据库

    向量存储在搜索在多个AI场景都有这广泛的使用,这次大会上,包括腾讯、华为、中兴、Oracle等厂商都介绍自己自己在这个方向的探索,包括海量存储下的性能优化、标量与向量混合查询的性能、面相RAG常见的效果优化、高效的向量缓存方案等。

    最后

    DTCC 是一年一度的数据库领域朋友聚会,非常开心。因为要“练摊”的原因,错过了很多的主题分享,今年的DTCC就简单记录如上。

  • 大语言模型非常非常强大,但也有一些弱点。例如,需要精确推理与计算的场景、实时数据(如天气)获取等。MCP则是对这类外部能力扩展的一个接口,让大模型/Agent都能够便捷的接入外部工具解决此类问题。

    本文通过演示创建一个外部计算24点的程序(MCP Server),让Cursor Agent访问LLM时具备快速的24点计算能力,从而帮助开发者快速了解如何构建一个MCP Server。该24点计算的MCP Server也已经在🤗 Hugging Face对外发布,你也可以接入你的Cursor(或其他MCP Host)进行测试(参考本文小结:在Cursor中配置MCP工具)[1]

    创建 MCP Server

    现在各种工具框架已经把MCP入门构建的门槛降低非常低了,这里将使用Gradio构建一个24点计算的程序,并以MCP Server的方式提供给各MCP Host(本文是Cursor)使用,同时将该MCP Server发布在🤗 Hugging FaceSpace上,以供其他人测试和使用。

    创建24点计算的MCP Server

    使用Gradio创建MCP的代码如下:

    #
    # A project for mcp learning by orczhou
    #
    from solve_24_game import solve_24_game
    import gradio as gr
    
    def gradio_interface(a, b, c, d):
        return solve_24_game([a, b, c, d])
    
    # Create the Gradio interface
    demo = gr.Interface(
        fn=gradio_interface,
        title="solve the 24 game/puzzle",
        inputs=[
            gr.Number(label="Number 1", value=1),
            gr.Number(label="Number 2", value=2),
            gr.Number(label="Number 3", value=3),
            gr.Number(label="Number 4", value=4),
        ],
        outputs="text",
        flagging_mode="never",
        description="Solves the 24-point game. Given a list of four numbers, it attempts to find a mathematical expression using addition, subtraction, multiplication, and division that evaluates to 24. Each number must be used exactly once.",
        theme=gr.themes.Ocean()
    )
    
    # Launch the interface and MCP server
    if __name__ == "__main__":
        demo.launch(mcp_server=True)

    Gradio 不仅可以快速构建可视化的交互界面(通常用于机器学习领域),还可以非常简单的构建起MCP Server,并将其托管于🤗 Hugging Face

        demo.launch(mcp_server=True)

    在启动时,新增mcp_server=True即可以同时启动一个与此界面“相同”的MCP Server

    左侧的代码首先创建了一个如下Web服务:

    在创建了上述的Web服务的同时,Gradio还会同时创建了一个如下Endpoint的MCP Server:

    https://orczhou-solve-24-game.hf.space/gradio_api/mcp/sse

    如果是本地运行,Endpoint则可能是:

    http://127.0.0.1:7860/gradio_api/mcp/

    这里解决24点问题的代码存储在文件solve_24_game.py中,代码参考:solve_24_game.py。如何解决24点问题并不是本文的重点,这里不做详述。

    本地运行该MCP Server

    在本地,则只需要使用python3 app.py命令即可运行:

     python3 app.py
    * Running on local URL:  http://127.0.0.1:7860
    * To create a public link, set `share=True` in `launch()`.
    
    🔨 Launching MCP server:
    ** Streamable HTTP URL: http://127.0.0.1:7860/gradio_api/mcp/
    * [Deprecated] SSE URL: http://127.0.0.1:7860/gradio_api/mcp/sse

    在Hugging Face上发布 MCP Server

    🤗 Hugging Face上可以非常方便的创建并托管简单的MCP Server。详细的介绍可以参考:Spaces as MCP servers。这里演示如何进行操作。

    前提要求

    1. 首先,你要有一个🤗 Hugging Face的账号,注册即可
    2. (可选)可能还需要进行充值与信用卡绑定
    3. 准备好🤗 Hugging FaceToken,并配置好权限

    通常,Hugging Face运行程序的资源是需要付费的。但是也有部分免费资源,例如MCP托管的时候,提供了一个免费的CPU Basic(2 vCPU 16 GB RAM)的免费资源(当前免费,未来也可能是计费的)。

    在本地代码向Space上推送的时候,则需要通过Token的方式进行认证和权限管理。

    创建 Space

    进入🤗 Hugging Face,进入Space,点击“+ New Space”创建新的Space,则进入右侧的创建表单。

    注意到,在Space Hardware选项中,这里的CPU Basic 2 vCPU 16GB是免费规格,这里用作个人测试故选择免费。

    此外,这里选择了Gradio模板进行创建。

    可以看到,这里🤗 Hugging Face把相关操作的入门门槛降到了非常低的程度,对初学者非常友好。

    提交代码

    在完成创建后,可以使用 git想仓库中提交代码,Space则会根据代码架构,完全自动化的构建一个MCP Server向公网提供服务。

    代码提交可以参考如下命令:

    git clone https://huggingface.co/spaces/orczhou/solve_24_game
    cd solve_24_game
    git add solve_24_game.py app.py
    git commit -m "mcp for 24 point game"
    git remote add origin https://huggingface.co/spaces/orczhou/solve_24_game
    git push -u origin main

    补充说明:首先在Hugging Face托管则需要把程序命名为app.py,此外,还需要编写一个requirements.txt文件说明Pythone程序需要的一些模块,这里仅需要gradio[mcp]

    cat requirements.txt
    gradio[mcp]

    调试

    这里使用Hugging FaceGradio构建的MCP服务,可以非常方便的使用可视化的界面进行查看服务,例如,在这里可以通过,如下的URL来进行查看该服务是否正常:solve the 24 game/puzzle

    此外,可以通过curl命令进行调试,以确认MCP服务是否正常:

    curl -X POST \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
      }' \
      -L \
      https://orczhou-solve-24-game.hf.space/gradio_api/mcp/
    data: {
      "jsonrpc":"2.0",
      "id":1,
      "result":{
        "tools":[{
          "name":"solve_24_game_gradio_interface",
          "description":"",
          "inputSchema":{
            "type":"object",
            "properties":{
              "a":{"type":"number", "default":1 },
              "b":{"type":"number", "default":2 },
              "c":{"type":"number", "default":3 },
              "d":{"type":"number", "default":4 }
            }
          }
    }]}}

    在Cursor中配置该MCP服务

    配置新的MCP服务

    CursorSettings->Tools & Integrations中找到MCP Tools,就可以通过New MCP Server处编辑mcp.json,从而增加MCP Tools

    具体的配置,参考如下:

    {
      "mcpServers": {
        "solve_24_game": {
          "type": "sse",
          "url": "https://orczhou-solve-24-game.hf.space/gradio_api/mcp/sse"
        }
      }
    } 

    查看Cursor中MCP配置状态

    首次测试和使用MCP时,还是比较容易出错的。可以通过上述的MCP Tools处(右图)可以查看MCP的状态,已经该MCP中可用的工具。

    在Cursor中使用该MCP服务

    Agent模式下进行对话,Cursor背后的大模型就可以使用该MCP的能力。为了避免Agent生成代码(Cursor的Agent总是倾向于生成代码解决问题),故使用如下提示词提问:

    不要生成任何代码,使用工具计算如下24点问题: 2 5 5 10”。

    返回的结果如右图。可以看到,大模型拿到返回的结果(5-(2/10))*5后,进行了必要的解释与回答。MCP详细的调用过程如下,包括了使用的参数、返回的结果:

    在我的Cursor上使用该MCP

    因为这里创建的MCP Server是运行在公网环境,故在你本地的Cursor或者其他MCP Host上也可以配置和使用该MCP服务,配置的方法参考上述小结“配置新的MCP服务”,这里不再赘述。

    其他

    更换Gradio的默认配色主题

    可以使用 theme=gr.themes.Ocean()选项更改主题选项:

    # Create the Gradio interface
    demo = gr.Interface(
        ...
        theme=gr.themes.Ocean()
    )

    更多的配色主题可以参考:Theming Guide

    最后

    大模型说:“我不需要帮助,愚蠢的人类!”

  • MySQL的向量处理现状

    向量数据库或者说向量处理是,个人认为,最为重要数据库AI能力。目前,各个数据库都在围绕着向量数据库构建更为丰富的LLM/AI相关功能。而MySQL 9最为重要的特性之一就是新增了向量处理能力。当前的版本主要包括了:(a) 向量数据类型;(b) 简单的向量处理函数。其中部分向量处理函数放在了MySQL的企业版或云版本中。

    因为当前MySQL 9系列的版本均为创新版(并不是稳定版),所以相关功能还会不断的迭代和发展。期待未来做出更多丰富功能:(a) 新增向量相似性搜索功能;(b) 并将完整的向量处理能力放到社区版中。后续依旧会持续关注这部分的产品能力。

    本文测试环境为Oracle Cloud,你可以参考在 Oracle 云上免费测试数据库[3]文中创建一个免费的MySQL实例进行测试。

    MySQL中的向量数据类型

    向量数据存储

    MySQL 9中新增了数据类型vector用来存储向量数据[1],简单的使用方式如下:

    create table vector_t01 (
        id int,
        s_v_01 vector(390),
        s_v_02 vector(390)
    );

    这里表示s_v_01、s_v_02均为390维的向量,每个维度在MySQL中使用4 bytes的单精度浮点类型存储。

    写入向量数据

    为了测试使用方便,这里使用string_to_vector对向量进行转换并进行存储:

    insert into vector_t01 values (1,string_to_vector('[1,2,3]'),string_to_vector('[4,5,6]'));

    查询向量数据

    使用 VECTOR_TO_STRING

    使用 SELECT 直接查询向量数据的话,则返回的是二进制形式,可以使用函数VECTOR_TO_STRING做一次转换:

    select id,VECTOR_TO_STRING(s_v_01),VECTOR_TO_STRING(s_v_02) FROM vector_t01;
    +------+---------------------------------------+---------------------------------------+
    | id   | VECTOR_TO_STRING(s_v_01)              | VECTOR_TO_STRING(s_v_02)              |
    +------+---------------------------------------+---------------------------------------+
    |    1 | [1.00000e+00,2.00000e+00,3.00000e+00] | [4.00000e+00,5.00000e+00,6.00000e+00] |
    +------+---------------------------------------+---------------------------------------+

    如果不使用VECTOR_TO_STRING则返回的是底层的二进制存储内容:

    select id,s_v_01,s_v_02 FROM vector_t01;
    +------+--------------+--------------+
    | id   | s_v_01       | s_v_02       |
    +------+--------------+--------------+
    |    1 |   �?   @  @@  |   �@  �@  �@    |
    +------+--------------+--------------+

    十六进制查询

    如果使用十六进制展示,则有:

    select id,hex(s_v_01),hex(s_v_02) FROM vector_t01;
    +------+--------------------------+--------------------------+
    | id   | hex(s_v_01)              | hex(s_v_02)              |
    +------+--------------------------+--------------------------+
    |    1 | 0000803F0000004000004040 | 000080400000A0400000C040 |
    +------+--------------------------+--------------------------+

    这里的0000803F0000004000004040一共是3个bytes,每个byte表示一个分量,例如:0000803F表示第一个分量,即为单精度浮点型的1.0(感兴趣的可以尝试做个转换,这里不再详述)。

    计算向量距离

    目前,MySQL 支持最为常见的“距离”的计算,具体包括:点积(默认)、欧式距离、余弦距离的计算:

    -- s_v_01: (1,2,3)
    -- s_v_01: (4,5,6)
    SELECT 
      DISTANCE(s_v_01,s_v_02,"DOT") as dis_dot,
      DISTANCE(s_v_01,s_v_02,"COSINE") as dis_cos,
      DISTANCE(s_v_01,s_v_02,"EUCLIDEAN") as dis_ecu
    FROM vector_t01;
    
    +---------+----------------------+-------------------+
    | dis_dot | dis_cos              | dis_ecu           |
    +---------+----------------------+-------------------+
    |      32 | 0.025368213653564453 | 5.196152210235596 |
    +---------+----------------------+-------------------+

    需要注意的是,目前该距离计算函数(DISTANCE[2])仅在Oracle Cloud或MySQL企业版本中提供。后续还将持续关注MySQL所提供的向量产品能力、以及其他GenAI相关功能。

    参考链接

  • 理解 NumPy 中的高维数组

    ·

    在机器学习中大量的使用NumPy作为其基础的数据结构,ndarrayNumPy的核心数据对象。对于ndarray高维数组的一个非常容易产生的误解是,使用数学中的矩阵(或者叫“行列式”)概念去尝试理解更高维的场景,或者使用更高维空间去理解,这样都会导致难以较好的理解更高维(5或6维)的数组。本文使用较为直观的示例和可视化的展示,更为“标准”(文档推荐的)的方式去理解ndarray的更高维数组。更多详细内容,可以参考阅读:

    问题

    在机器学习中,经常要对多维的数组做各种操作。对高维数组建立更好的直觉理解,则有利于去理解这些操作。例如,我们考虑右侧的代码,想一想该代码的输出是什么?

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (考虑输出是什么)

    要回答这个问题,则需要建立对于多维数组结构的理解。

    文档中对于高维数组理解的建议

    NumPy: the absolute basics for beginners中有如下一段话:

    It is familiar practice in mathematics to refer to elements of a matrix by the row index first and the column index second. This happens to be true for two-dimensional arrays, but a better mental model is to think of the column index as coming last and the row index as second to last. This generalizes to arrays with any number of dimensions.

    “矩阵”是“线性代数”的主要研究对象,一个\( m \times n \)的矩阵即是一个平面上的\( m \)行\( m \)列的行列式。一种常见的向高维扩展的思考方式是,会将三维数组扩展为三维空间中的数组。但,这样的扩展,非常不利于去理解更高维的数组。这里提到的方案是这样:“a better mental model is to think of the column index as coming last and the row index as second to last.”。

    这种理解,也是本文的核心,概况如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

    从 4×5 的数组开始

    先通过直观的书写表达,看看这样的数组应该是怎样的。

    一般的矩阵(行列式表示):

    \begin{bmatrix}
    0 & 1 & 2 & 3 & 4 \\
    5 & 6 & 7 & 8 & 9 \\
    10 & 11 & 12 & 13 & 14 \\
    15 & 16 & 17 & 18 & 19 \\
    \end{bmatrix}

    本文推荐的理解方式:

    \[
    \begin{bmatrix}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19]
    \end{bmatrix}
    \]

    numpy的输出:

    >>> np.arange(20).reshape(4,5)
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14],
           [15, 16, 17, 18, 19]])

    如果按照“矩阵”思想去理解这个矩阵很简单。但这里,我们重新按照上述的原则去理解这个数组。即:

    • 最后一个维度(即第二个维度),该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即第一个维度,该维度的长度是4,将其理解为维度

    形式上,这与一般的矩阵,是完全一致的。只是,思维方式,反过来了。

    再考虑 3x4x5 的数组

    这个数组已经不能用简单的平面表示了,这里使用了符合上述描述的形式描述,“剩余的维度,则是通过层的方式去构建”,则有:

    本文推荐的理解方式:

    \[
    \begin{array}{r c}
    \text{Layer 1:} &
    \left[
    \begin{array}{c}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19] \\
    \end{array}
    \right]
    \\
    \text{Layer 2:} &
    \left[
    \begin{array}{c}
    [20 & 21 & 22 & 23 & 24] \\
    [25 & 26 & 27 & 28 & 29] \\
    [30 & 31 & 32 & 33 & 34] \\
    [35 & 36 & 37 & 38 & 39] \\
    \end{array}
    \right]
    \\
    \text{Layer 3:} &
    \left[
    \begin{array}{c}
    [40 & 41 & 42 & 43 & 44] \\
    [45 & 46 & 47 & 48 & 49] \\
    [50 & 51 & 52 & 53 & 54] \\
    [55 & 56 & 57 & 58 & 59] \\
    \end{array}
    \right]
    \end{array}
    \]

    >>> np.arange(60).reshape(3,4,5)
    array([[[ 0,  1,  2,  3,  4],
            [ 5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14],
            [15, 16, 17, 18, 19]],
    
           [[20, 21, 22, 23, 24],
            [25, 26, 27, 28, 29],
            [30, 31, 32, 33, 34],
            [35, 36, 37, 38, 39]],
    
           [[40, 41, 42, 43, 44],
            [45, 46, 47, 48, 49],
            [50, 51, 52, 53, 54],
            [55, 56, 57, 58, 59]]])

    这时,矩阵的想法就不太好用了。这里继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第三个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第二个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度(第一个维度)该维度长度是3,将其理解为行列式前面的

    循着这样的思考模式,不断地叠加更多的“层”,就可以理解更高维度的数组了。

    考虑 2x3x4x5 的数组

    这里先试用“层”的思维,可视化的表示该数组如下:

    \[
    \left[
    \begin{array}{c}
    \text{Layer}^{(0)}_1
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [000 & 001 & 002 & 003 & 004] \\
    [005 & 006 & 007 & 008 & 009] \\
    [010 & 011 & 012 & 013 & 014] \\
    [015 & 016 & 017 & 018 & 019] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [020 & 021 & 022 & 023 & 024] \\
    [025 & 026 & 027 & 028 & 029] \\
    [030 & 031 & 032 & 033 & 034] \\
    [035 & 036 & 037 & 038 & 039] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [040 & 041 & 042 & 043 & 044] \\
    [045 & 046 & 047 & 048 & 049] \\
    [050 & 051 & 052 & 053 & 054] \\
    [055 & 056 & 057 & 058 & 059] \\
    \end{array}
    \right]
    \end{array}
    \right] \\
    \text{Layer}^{(0)}_2
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [060 & 061 & 062 & 063 & 064] \\
    [065 & 066 & 067 & 068 & 069] \\
    [070 & 071 & 072 & 073 & 074] \\
    [075 & 076 & 077 & 078 & 079] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [080 & 081 & 082 & 083 & 084] \\
    [085 & 086 & 087 & 088 & 089] \\
    [090 & 091 & 092 & 093 & 094] \\
    [095 & 096 & 097 & 098 & 099] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [100 & 101 & 102 & 103 & 104] \\
    [105 & 106 & 107 & 108 & 109] \\
    [110 & 111 & 112 & 113 & 114] \\
    [115 & 116 & 117 & 118 & 119] \\
    \end{array}
    \right]
    \end{array}
    \right]
    \end{array}
    \right]
    \]

    >>> np.arange(120).reshape(2,3,4,5)
    array([[[[  0,   1,   2,   3,   4],
             [  5,   6,   7,   8,   9],
             [ 10,  11,  12,  13,  14],
             [ 15,  16,  17,  18,  19]],
    
            [[ 20,  21,  22,  23,  24],
             [ 25,  26,  27,  28,  29],
             [ 30,  31,  32,  33,  34],
             [ 35,  36,  37,  38,  39]],
    
            [[ 40,  41,  42,  43,  44],
             [ 45,  46,  47,  48,  49],
             [ 50,  51,  52,  53,  54],
             [ 55,  56,  57,  58,  59]]],
    
    
           [[[ 60,  61,  62,  63,  64],
             [ 65,  66,  67,  68,  69],
             [ 70,  71,  72,  73,  74],
             [ 75,  76,  77,  78,  79]],
    
            [[ 80,  81,  82,  83,  84],
             [ 85,  86,  87,  88,  89],
             [ 90,  91,  92,  93,  94],
             [ 95,  96,  97,  98,  99]],
    
            [[100, 101, 102, 103, 104],
             [105, 106, 107, 108, 109],
             [110, 111, 112, 113, 114],
             [115, 116, 117, 118, 119]]]])

    继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第四个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第三个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度,即第二个维度,该维度的长度是3,将其理解为行列式前面的
    • 倒数第四个维度,即第一个维度,该维度的长度是2,将其理解为行列式前面的层的层,也就是上述的“Layer”

    使用这样的模式,就可以将一个多维数组的表示平面化。并且注意到,这与ndarray输出的形式是几乎完全一致的。

    回到前面的问题

    有了上面的可视化展示以及上面逐步的介绍,应该可以更容易理解前面NumPy: the absolute basics for beginners所提到的直觉“a better mental model is to think of the column index as coming last and the row index as second to last”。

    有了这个直觉,我们再来考虑最前面提到的问题:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape

    最内层的列,就是最后的维度长度,这里是 1,所以就是 ? x 1;该列所对应的行数,就是倒数第二个维度的长度,这里做如下的格式化,可以看到有两行,所以这是一个? x 2 x 1的数组;再向上看一层,共有两个该2x1的数组,故,该数组的shape时:2x2x1

    [
      [
        [1],
        [2]
      ],
      [
        [3],
        [4]
      ]
    ]

    再确认最后的输出:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (2, 2, 1)

    最后

    这种理解的核心即是“a better mental model is to think of the column index as coming last and the row index as second to last”,简单概括如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

  • Oracle LogMiner 使用

    ·

    Oracle官方文档Using LogMiner to Analyze Redo Log Files[2]中,对该功能有详细的介绍,包括了LogMiner的配置与使用、数据过滤、补充日志(Supplemental Logging)、使用示例等。

    Oracle 何时引入的LogMiner?

    自 1999 年发布 Oracle 8i 的时候,正式引入 LogMiner 功能(参考:Redo Log Analysis Using LogMiner[1])。该功能支持以SQL的形式分析redo中的数据,最初考虑的应用场景,主要还是偏于故障恢复、异常诊断、审计等,但是该功能的潜力很大,现在已经逐步成为Oracle CDC的主流方案之一。

    目前已经有很多的集成/同步工具都使用LogMiner进行变化数据获取,虽然,目前官方依旧不推荐这么做,文档中的原文如下:“Note:LogMiner is intended for use as a debugging tool, to extract information from the redo logs to solve problems. It is not intended to be used for any third party replication of data in a production environment.”

    根据经验来看,LogMiner用于数据集成并没有什么太大的问题。

    打开补充日志

    ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;

    查看归档日志

    SQL> SELECT name FROM v$archived_log ORDER BY FIRST_TIME DESC FETCH FIRST 3 ROWS ONLY;
    
    NAME
    ----------------------------------------------------------------------------------------------------
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_761_n0mbccbd_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_760_n0mbcc62_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_759_n0m6tdfp_.arc

    添加需要解析的日志文件

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/PATH_TO_YOUR_ARCHIVE/o1_mf_1_761_n0mbccbd_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    例如,实际的SQL可能是如下的样子:

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_955_n0mwc01v_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /
    
    o1_mf_1_955_n0mwc01v_.arc
    o1_mf_1_909_n0mv4sc3_.arc
    o1_mf_1_908_n0mv4ohs_.arc

    启动LogMiner

    启动时,可以带不同的参数以指定LogMiner不同的行为。

    使用在线数据字典启动
    EXECUTE DBMS_LOGMNR.START_LOGMNR( -
       OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);
    使用日志中的数据字典启动
    EXECUTE DBMS_LOGMNR_D.BUILD( -
        OPTIONS => DBMS_LOGMNR_D.STORE_IN_REDO_LOGS);

    获取LogMiner中的变更数据

    获取变更数据
    SELECT 
      SCN,
      TIMESTAMP,
      OPERATION,
      SQL_REDO,
      SQL_UNDO,
      SEG_OWNER,
      TABLE_NAME,
      USERNAME
    FROM 
      V$LOGMNR_CONTENTS
    WHERE 
        OPERATION IN ('INSERT', 'UPDATE', 'DELETE')
    --  AND SEG_OWNER = 'TEST_USER'
        AND (TABLE_NAME = 'T2' OR TABLE_NAME = 't2')
    ORDER BY TIMESTAMP DESC
    FETCH FIRST 3 ROWS ONLY;

    退出 LogMiner

    EXECUTE DBMS_LOGMNR.END_LOGMNR();

    解析未归档的 Redo 日志

    除了解析归档之外,LogMiner 可以直接解析当前正在使用的 redo 文件。先根据小节“获取当前正在使用的redo文件”中的SQL获取当前正在使用的 redo 文件,然后在添加日志文件时,像上述添加归档一样添加即可。

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u04/app/oracle/redo/redo003.log',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    使用上述(小结“获取变更数据”)的SQL可以获得如下的输出:

           SCN TIMESTAMP OPERATION	  SQL_REDO
    ---------- --------- ------------ ---------------------------------------------
       3175619 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('31');
       3175846 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('32');

    一些常见的 SQL

    获取归档日志

    获取最新的ARCHIVE LOG的列表,以及对应的SCN号范围:

    SELECT 
      FIRST_CHANGE#,
      TO_CHAR(FIRST_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NEXT_CHANGE# ,
      TO_CHAR(NEXT_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NAME 
    FROM  
      V$ARCHIVED_LOG 
    ORDER BY NEXT_CHANGE# DESC 
    FETCH FIRST 3 ROWS ONLY;
    强制切换归档日志文件
    ALTER SYSTEM SWITCH LOGFILE;
    获取当前正在使用的redo文件
    SELECT A.GROUP#,A.MEMBER, B.STATUS
    FROM   V$LOGFILE A
    JOIN   V$LOG B ON A.GROUP# = B.GROUP#
    WHERE  B.STATUS = 'CURRENT'; 

    参考

  • 为什么需要间隙锁

    关于为什么需要 “Gap Locks” 或者 “Next-key Locks” ,在MySQL的文档“17.7.1 InnoDB Locking”的小节中有较为详细的介绍,这里不再赘述。这里使用一个具体的示例,以便开发者方便构造与观察间隙锁。

    Repeatable-Read隔离级别下,在整个事务的过程中,数据需要保持一致,经常需要使用间隙锁对数据或资源进行保护。例如,在如下的事务中:

    DROP TABLE IF EXISTS t1;
    CREATE TABLE t1 (
      id int,
      nick varchar(32),
      age int,
      KEY ind_n (nick),
      PRIMARY KEY (id)
    );
    
    INSERT INTO t1 VALUES ( 1, "a", 27 );
    INSERT INTO t1 VALUES ( 11, "k" ,23 );
    INSERT INTO t1 VALUES ( 24, "x" ,22 );
    
    START TRANSACTION;
    SELECT * FROM t1 WHERE nick >= "k" for update;
    +----+------+------+
    | id | nick | age  |
    +----+------+------+
    | 11 | k    |   23 |
    | 24 | x    |   22 |
    +----+------+------+
    2 rows in set (0.00 sec)  Warnings: 0

    在上述事务中,为了实现将 SELECT 涉及的“相关记录”全部加上“排它锁”,从而阻止其他事务对该部分数据进行修改。即,如果有任何其他的事务,尝试修改该事务中的“相关记录”,都需要被阻塞。这里的“相关记录”,具体是指:WHERE nick >= "k" 查询扫描到的索引入口(二级索引),以及对应的数据(即主键入口)。

    试想,如果有其他事务尝试写入一条 nick = 'm'的记录,那么上述的...for update语句则也会返回该记录。为了阻止上述的不一致,上述事务不仅要对单个记录或索引入口进行加锁,还需要对索引入口之间的间隙进行加锁。

    具体的,该案例中详细的锁信息如下:

    mysql> SELECT
        ENGINE_TRANSACTION_ID AS TRX_ID,     OBJECT_NAME,     
        INDEX_NAME, LOCK_MODE,     LOCK_STATUS,     
        LOCK_DATA    FROM performance_schema.data_locks 
      WHERE LOCK_TYPE="RECORD";
    +--------+-------------+------------+---------------+-------------+------------------------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_MODE     | LOCK_STATUS | LOCK_DATA              |
    +--------+-------------+------------+---------------+-------------+------------------------+
    |  10703 | t1          | ind_n      | X             | GRANTED     | supremum pseudo-record |
    |  10703 | t1          | ind_n      | X             | GRANTED     | 'k', 11                |
    |  10703 | t1          | ind_n      | X             | GRANTED     | 'x', 24                |
    |  10703 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 11                     |
    |  10703 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 24                     |
    +--------+-------------+------------+---------------+-------------+------------------------+

    上述的锁信息显示,该事务获得了索引 (a,supremum)间隙的排它锁,以及对应的数据记录锁。

    如果说上述案例中的 SELECT ... FOR UPDATE在实际中不那么常见的话,类似的直接使用UPDATE语句也需要类似的加锁信息,这里使用SELECT ... FOR UPDATE展示则更为直接。

    InnoDB 中“间隙锁”的表示

    理解 InnoDB 锁的困难有很多,其中一个非常大的困难就是理解在 InnoDB 间隙锁的表示,因为 InnoDB 使用了一个比较反直觉的模式去实现间隙锁。具体的:InnoDB 在表示间隙锁的时候,并没有把 GAPNext-Key当做一个“资源”。而是依旧把Record当做资源,然后,以“锁类型的”(Lock Mode)不同的取值表示记录锁或间隙锁。例如,

    • Lock Mode取值为S,GAP时,则表示间隙锁,锁类型为S
    • Lock Mode取值为S,REC_NOT_GAP时,则表示记录锁,锁类型为S

    锁的对象,均为索引入口或主键入口。

    关于该实现,在InnoDB Data Locking – Part 2 “Locks”中有着较为详细的描述:

    “Even without knowing too much about how databases like InnoDB operate, we can guess, that sometimes the operation involves just the record, sometimes the gap before a record, and at yet another times we need to access both, the record and a gap. One way to model that, would be to consider records and gaps to be two different kinds of resources which you can lock independently. Current InnoDB implementation takes a different approach: there is just one resource for each point, but there are multiple kinds of access right you can request for it, and the access right specifies if you need the row, the gap or both parts. One benefit of this is that it is optimized for the most common case where you need both.”

    InnoDB Data Locking – Part 2 “Locks”

    锁类型与加锁模式

    这里使用如下表格表示了InnoDB在进行加锁时常见的加锁对象与类型。这里Dm表示具体的加锁数据,例如索引入口数据或主键数据。详细的表格如下:

    LOCK_DATA
    加锁目标/资源
    LOCK_MODE
    加锁模式
    加锁对象/
    资源
    加锁类型
    DmS,REC_NOT_GAPDmS
    DmX,REC_NOT_GAPDmX
    DmS,GAP(D(m-1),Dm)S
    DmX,GAP(D(m-1),Dm)X
    DmS(D(m-1),Dm]S
    DmX(D(m-1),Dm]X

    通常的,

    • performance_schema中看到的Dm取值可能是这样:0x000000000207
    • show engine innodb status\G中看到的Dm会是:0: len 4; hex 00000001; asc ;;
    • D(m-1)则表示在索引中,Dm前面的一个索引入口或主键取值
    • (...)表示开区间;(...]表示半开半闭区间

    show engine innodb status\G 输出示例:

    RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `sysbenchdb`.`t1` trx id 10094 lock mode S locks rec but not gap waiting
    Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 00000001; asc     ;;
     1: len 6; hex 00000000276d; asc     'm;;
     2: len 7; hex 810000008d0110; asc        ;;
     3: len 1; hex 61; asc a;;
     4: len 4; hex 8000000c; asc     ;;

    UPDATE 语句的间隙锁

    环境说明

    mysql> show variables like '%iso%';
    +-----------------------+-----------------+
    | Variable_name         | Value           |
    +-----------------------+-----------------+
    | transaction_isolation | REPEATABLE-READ |
    +-----------------------+-----------------+
    1 row in set (0.01 sec)

    场景构造

    Session ASession B
    DROP TABLE IF EXISTS t1;
    CREATE TABLE t1 (
    id int,
    nick varchar(32),
    age int,
    KEY ind_n (nick),
    PRIMARY KEY (id)
    );

    INSERT INTO t1 VALUES ( 1, "a", 27 );
    INSERT INTO t1 VALUES ( 11, "k" ,23 );
    INSERT INTO t1 VALUES ( 24, "x" ,22 );
    START TRANSACTION;
    UPDATE t1 SET age = 127 WHERE nick = "k";

    观测间隙锁

    mysql> SELECT      
      ENGINE_TRANSACTION_ID AS TRX_ID,     
      OBJECT_NAME,  INDEX_NAME, LOCK_MODE,     
      LOCK_STATUS,  LOCK_DATA
    FROM performance_schema.data_locks WHERE LOCK_TYPE="RECORD";
    +--------+-------------+------------+---------------+-------------+-----------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+-------------+------------+---------------+-------------+-----------+
    |  10644 | t1          | ind_n      | X,GAP         | GRANTED     | 'x', 24   |
    |  10644 | t1          | ind_n      | X             | GRANTED     | 'k', 11   |
    |  10644 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 11        |
    +--------+-------------+------------+---------------+-------------+-----------+

    可以看到,在索引上(KEY ind_n (nick))的锁范围为:(a,x)。这个范围由两个锁构成,分别是:

    • X,GAP : 'x', 24 对应的范围为 (k,x)
    • X : 'k', 11 对应的范围为 (a,k]

    故,上述两个范围,共同构成的范围为:(a,x),锁类型都是排它锁(X)。

    测试验证

    可以尝试写入一个在 (a,k)(k,x)范围为的记录,观察阻塞情况。继续上述的两个Session,并执行如下的SQL 语句进行观察:

    Session ASession B
    START TRANSACTION;
    INSERT INTO t1 VALUES (2,"c",32);
    -- blocking / waiting
    ROLLBACK
    START TRANSACTION;
    INSERT INTO t1 VALUES (2,"m",32);
    -- blocking / waiting
    ROLLBACK

    参考链接