当前位置: 首页 > news >正文

PostgreSQL GIN 索引揭秘

文章目录

  • 什么是GIN Index?
  • 示例场景
  • GIN Index的原理
  • GIN Index结构
    • Metapage
    • Entries
    • Leaf Pages
    • Entry page 和 Leaf page 的关系
    • Posting list 和posting tree
    • 待处理列表(Pending List)
  • 进阶解读GIN index索引结构
  • 总结

什么是GIN Index?

GIN (Generalized Inverted Index) 索引常用于为 array、jsonb 和 tsvector(用于 fulltext search)类型建立索引。
在 array 的场景下,可以用来验证一个 array 是否包含另一个 array 或元素(比如 <@ operator)。
在早前的postgresql-一文读懂index中的operator,你可以看到完整的 operator 列表。

但我在这篇文章里真正想回答的问题是:
“为什么我们要在这些数据类型和 operator 上使用 GIN 索引?”
在 PostgreSQL 中,GIN(Generalized Inverted Index,广义倒排索引)之所以被称为“倒排”,是因为它的数据结构和查询方式与传统索引(如 B-tree)的工作原理相反。倒排索引的核心思想是将数据的存储和查询从“正向”转向“反向”。
具体来说:

  • 传统索引(如 B-tree) 是“正向”索引,基于键值(key)直接映射到对应的记录位置。查询时,从键值出发,找到相关记录。
  • 倒排索引 则是将数据的属性或值作为索引的起点,记录哪些文档或记录包含这些值。例如,对于全文搜索,GIN 会为每个词条(term)维护一个列表,列出包含该词条的所有记录的标识。这种“值到记录”的映射方式被称为“倒排”,因为它反过来存储了“记录到值”的关系。

在 GIN 的上下文中,这种设计特别适合处理多值属性(如数组、JSON 或全文搜索),因为它能够高效地查找包含特定值的记录集合。PostgreSQL 中的 GIN 实现了这种广义倒排索引,支持多种数据类型和操作符类(如数组操作符或全文搜索操作符),因此得名“广义倒排索引”。
总结来说,“倒排”反映了 GIN 从值反向映射到记录的独特索引机制,这使其在特定场景下(如复杂查询或全文检索)表现出色。

示例场景

创建一个表 articles,并使用 GIN 索引来支持全文搜索:

-- 创建表
CREATE TABLE articles (id SERIAL PRIMARY KEY,content TEXT
);-- 插入一些示例数据
INSERT INTO articles (content) VALUES
('The quick brown fox jumps over the lazy dog'),
('A quick jump over the brown fence'),
('The lazy dog sleeps');-- 创建 GIN 索引,用于全文搜索
CREATE INDEX idx_gin_content ON articles USING GIN (to_tsvector('english', content));

我们利用to_tsvector函数,查看上面GIN Index将会生成的8个items如下:

demo=# SELECT DISTINCT word
FROM ts_stat('SELECT to_tsvector(''english'', content) FROM articles');word
-------fencdogsleepjumpfoxbrownquicklazi
(8 rows)

GIN Index的原理

当我们对 content 列创建 GIN 索引时,PostgreSQL 会使用 to_tsvector 函数将文本分解为词条(terms),并为每个词条生成一个倒排列表。
例如,to_tsvector(‘english’, ‘The quick brown fox jumps over the lazy dog’) 会分解为词条:brown, dog, fox, jump, lazy, over, quick, the。
GIN 索引会记录每个词条及其出现在哪些 id 中的信息,例如:

brown -> [id: 1, id: 2]
dog -> [id: 1, id: 3]
quick -> [id: 1, id: 2]
......

这种“词条到记录 ID”的映射就是“倒排”的体现,与传统索引从记录 ID 查找值的正向方式相反。

GIN Index结构

GIN 索引的结构和 BTree 索引非常接近。接下来我们将探讨其中的一些差异。

Metapage

像 BTree Index 一样,GIN index 的第一页是 metapage,其中包含关于索引的信息。不同之处在于,这些信息会稍有不同。例如,在 GIN index 中你不会找到btree中的 fast root结构。
我们可以通过 postgres extension pageinspect 来查看 metapage 的信息。

demo=# select * from gin_metapage_info(get_raw_page('idx_gin_content',0));
-[ RECORD 1 ]----+-----------
pending_head     | 4294967295
pending_tail     | 4294967295
tail_free_size   | 0
n_pending_pages  | 0
n_pending_tuples | 0
n_total_pages    | 2
n_entry_pages    | 1
n_data_pages     | 0
n_entries        | 8
version          | 2

以上输出字段说明如下:

  1. Pending list 相关字段
  • pending_head = 4294967295
  • pending_tail = 4294967295
    4294967295是一个特殊值 (InvalidBlockNumber),表示当前 没有 pending list。
  • tail_free_size = 0
    如果 pending list 存在,这里会表示尾页剩余的可用空间。现在为 0,说明没有 pending list。
  • n_pending_pages = 0
    表示待处理页面(pending pages)的数量。值为 0 表明当前没有待处理的页面,这与 pending_head 和 pending_tail 的值一致,说明索引未处于批量更新模式
  • n_pending_tuples = 0
    没有等待合并到 entry tree 的 tuple。
    说明:索引里的数据已经都合并进 entry pages 了,没有暂存的东西。
  1. Page 统计
  • n_total_pages = 2
    整个索引文件占用 2 个 page:
  • page 0:metapage
  • page 1:entry page
  • n_entry_pages = 1
    有 1 个 entry page(page 1),entry page 存放的是 key → posting list/tree 的入口。
  • n_data_pages = 0
    没有 data page。
    data page 存 posting tree 的叶子节点。因为目前 posting list 很小,直接存放在 entry page 里,不需要单独开 data page。
  1. 索引条目
  • n_entries = 8
    这个的索引里总共有 8 个不同的 key(比如 array 元素 / tsvector token / jsonb key)。
  1. 版本号
  • version = 2
    GIN 索引的格式版本,目前 Postgres 的 GIN 是 version 2(相比 v1 支持更紧凑的存储方式,posting list 压缩等)。

在一个 GIN index 中存在两种类型的 page:entry pages 和 data pages。

  • Data pages 是位于 posting tree 内部的 page。
  • Entry pages 是包含索引中 value 的 page。

这两类 page 都带有 opaque data,其中包含:

  • 一个 flag 用于定义类型(leaf、data、compressed、meta)
  • right sibling
  • maxoff

Entries

在一个 GIN index 中,keys(entries) 存储在 entry pages 中,以 binary tree 的形式组织。这一点和 BTree index 非常接近。实际上,索引的第一页是 metapage,然后这些 keys 会被存储到一个 binary tree 中。
GIN 的 entry pages 以二叉树形式组织,看上去和 BTree 很像, 但是,这里还是存在一些主要的差异……
首先,如果你在 BTree index 中对一个 array 建立索引,那么存储的值会直接是这个 array。

  • BTree:key → 指向 tuple
  • GIN:key → posting list(或 posting tree) → tuple
    例如,如果被索引的字段的值是一个阵列:array{1,6,12}
    BTree:整个数组{1,6,12}被当作一个 key:
Key (array)          → Tuple(s)
-------------------------------
{1,6,12}(0,1)

GIN:数组会被拆开,存成多个 entry:1 、 6、12,各自维护 posting list:

Key (entry)          → Posting List (heap pointers)
-------------------------------
1(0,1)
6(0,1)
12(0,1)

这正是 GIN 擅长全文检索的原因。
在我们的例子中,虽然不是数组,但 to_tsvector 把句子拆成了多个词,每个词单独建 entry。

GIN Entry Page
+---------+-------------------+
| "dog"   | -> (0,1), (0,3)   |
| "brown" | -> (0,1), (0,2)   |
| "sleep" | -> (0,3)          |
| "quick" | -> (0,1), (0,2)   |
| ......  | -> ......         |
+---------+-------------------+

第二个区别是:在 GIN index 中,values 是唯一的。
在 BTree 中,同一个 value 可以对应多个 items, tuple (value, pointer) 来保证 index entry 的唯一性。
而在 GIN index 中,values 的唯一性使得它非常适合用于同一个 value 出现在许多不同 rows 的情况。

Leaf Pages

在BTree index中,在leaf level上,items的数量与rows的数量相等。因此同一个值可能会重复出现多次:

BTree Leaf Page"brown" → Row1"brown" → Row2"dog"   → Row1"dog"   → Row3"fox"   → Row1"jumps" → Row1"lazy"  → Row1"lazy"  → Row3"over"  → Row1"over"  → Row2"quick" → Row1"quick" → Row2"the"   → Row1"the"   → Row3

正如我之前所说,在GIN index中entries是唯一的。因此leaf levels包含指向rows的pointers的list或tree,即post list或post tree。

GIN Entry Tree"brown"[Row1, Row2]"dog"[Row1, Row3]"fox"[Row1]"jumps"[Row1]"lazy"[Row1, Row3]"over"[Row1, Row2]"quick"[Row1, Row2]"the"[Row1, Row3]

Entry page 和 Leaf page 的关系

在 entry tree 里,和 BTree 类似:

  1. Entry pages (internal pages)
  • 存放的是 entries 的范围信息(keys):
  • 每个 entry 对应一个 posting list 或 posting tree 的指针
    这类似于 BTree 的 internal page。
  1. Leaf pages
    存放具体的 entry (value)

entry 对应的是posting list还是posting tree的指针取决于索引的大小:

  1. 小索引(entry page = root = leaf,posting list 内联)
Entry Tree└── Entry Page (root & leaf)├── entry = "dog"   → posting list [ctid(0,1), ctid(0,3)]├── entry = "quick" → posting list [ctid(0,1), ctid(0,2)]├── entry = "the"   → posting list [ctid(0,1), ctid(0,3)]└── ...

这里只有 metapage + 一个 entry page,entry page 既是 root 也是 leaf,posting list 全部内联。
2. 大索引(entry page 内存指针,posting list 太大 → posting tree)

Entry Tree├── Entry Page (internal)│       key range + child pointers│└── Leaf Page├── entry = "dog" → posting list [ctid(0,1), ctid(0,3)]├── entry = "quick" → posting list [ctid(0,1), ctid(0,2)]├── entry = "the" → pointer to posting tree└── ...

在这种情况下:

  • entry page 作为 internal page(只做导航);
  • leaf page 存 entry,但如果某个 entry(如 “the”)太大,就存一个指针指向 posting tree。

Posting list 和posting tree

在一个 leaf page 中,entries 包含一个称为 posting list 的 item pointers 列表,这个列表是以压缩格式存储的。

如果列表变得太大,以至于该 item 无法再放入 index page,那么 posting list 会被拆分到不同的页面中,这些页面以 BTree 组织。这就是所谓的 posting tree。在 leaf item 中,会存储指向这个树的指针,而不是 posting list。
在 posting list 中指向 heap 的指针是按物理顺序存储的。而在 posting tree 中,这些指针则作为 keys。
所以,现在我们讲完了 leaf,这里展示一下 GIN 索引的各个层级结构
在 GIN 的 entry tree 里,每个 entry 是唯一的。
比如 “dog”:

Leaf Page (GIN)
Entry: "dog"Posting list of item pointers:→ (Row1, ctid=(0,1))(Row3, ctid=(0,3))

这里 posting list 很小,能直接放在 leaf page 里。

当 posting list 太大时 → Posting tree
假设 “the” 出现在 100 万行文章中,posting list 太大,无法塞进一个 index page。
这时,GIN 会把 posting list 拆分成多个 page,用 BTree 组织,形成 posting tree:

Entry Tree (Entry Page / Leaf Page)
Entry: "the"→ pointer to Posting Tree

Posting Tree 结构:

Posting Tree Root Page (BTree internal)├── → Data Page 1 [Row1, Row3, Row8, Row20, ...]├── → Data Page 2 [Row101, Row102, Row110, ...]└── → Data Page 3 [Row999, Row1000, ...]

这里:

  • leaf item 里不再存 posting list,而是存一个 指针,指向 posting tree root。
  • posting tree 内部的指针就是 keys,用来导航到具体的 data page。

待处理列表(Pending List)

在 GIN index 中插入新行是相当慢的,因为 values 的唯一性,插入操作比在普通 BTree 中插入更慢——这是因为必须更新 posting list 或 posting tree。

为了优化插入,我们将新的 entries 存储在一个 pending list 中,它是一个简单的线性 pages 列表。当 pending list 达到限制,或者发生 VACUUM 时,这些 entries 会被移动到 BTree 中,使用的是 bulk insert,这种方式经过优化,尤其是在每个 value 对应多行的情况下。

pending list 的大小限制可以逐个索引设置:

CREATE INDEX ... WITH (gin_pending_list_limit=...)
ALTER INDEX ... SET (gin_pending_list_limit=...)

或者通过全局配置参数 gin_pending_list_limit 来设置。

gin_pending_list_limit = '64MB';

pending list 的缺点是:在 GIN index 中进行搜索时,必须同时扫描 BTree 和 pending list。
如果你的场景中数据很少发生变化,并且你不在乎更新操作很慢,那么可以通过在创建索引时,或者使用 ALTER INDEX 将 fastupdate 设置为 false 来禁用 pending list。

需要注意的是,如果你使用 ALTER INDEX 禁用了 pending list,那么已有的 pending list 并不会自动被刷新,因此你可能需要在表上执行 VACUUM,以确保所有数据都被移动到 BTree 中。

进阶解读GIN index索引结构

首先,我们透过pageinspace扩展去查看metapage所包含的内容

 pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------4294967295 |   4294967295 |              0 |               0 |                0 |             2 |             1 |            0 |         8 |       2
(1 row)

从输出中的n_total_pages来看,这个index共有2个page,其中metapage占用1个page,n_entry_page=1代表有1个entry page.
由于pageinspect并没有提供直接查看entry page的函数,我们只能从侧面来证明这个entry page的内容:

demo=# SELECT DISTINCT word
FROM ts_stat('SELECT to_tsvector(''english'', content) FROM articles');word
-------fencdogsleepjumpfoxbrownquicklazi
(8 rows)

共产生8个词条,这与n_entries=8是一致的。

另外,我们也可以透过pg_filedump来dump gin index的内部结构

 pg_filedump -i -f  -R 1 /var/lib/postgresql/16/main/base/16448/24863

这里-R 1,意指dump page 1(page 0是metapage,page 1是entry page)
输出如下:


*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility
*
* File: /var/lib/postgresql/16/main/base/16448/24863
* Options used: -i -f -R 1
*******************************************************************Block    1 ********************************************************
<Header> -----Block Offset: 0x00002000         Offsets: Lower      56 (0x0038)Block: Size 8192  Version    4            Upper    7952 (0x1f10)LSN:  logid      0 recoff 0x12b23040      Special  8184 (0x1ff8)Items:    8                      Free Space: 7896Checksum: 0x0000  Prune XID: 0x00000000  Flags: 0x0000 ()Length (including item array): 560000: 00000000 4030b212 00000000 3800101f  ....@0......8...0010: f81f0420 00000000 d89f4000 b89f4000  ... ......@...@.0020: a09f3000 889f3000 689f4000 489f4000  ..0...0.h.@.H.@.0030: 289f4000 109f3000                    (.@...0.<Data> -----Item   1 -- Length:   32  Offset: 8152 (0x1fd8)  Flags: NORMALBlock Id: 2147483664  linp Index: 2  Size: 32Has Nulls: 0  Has Varwidths: 11fd8: 00801000 02002040 0d62726f 776e0000  ...... @.brown..1fe8: 00000000 01000100 01000000 00000000  ................Item   2 -- Length:   32  Offset: 8120 (0x1fb8)  Flags: NORMALBlock Id: 2147483664  linp Index: 2  Size: 32Has Nulls: 0  Has Varwidths: 11fb8: 00801000 02002040 09646f67 00000000  ...... @.dog....1fc8: 00000000 01000100 02000000 00000000  ................Item   3 -- Length:   24  Offset: 8096 (0x1fa0)  Flags: NORMALBlock Id: 2147483664  linp Index: 1  Size: 24Has Nulls: 0  Has Varwidths: 11fa0: 00801000 01001840 0b66656e 63000000  .......@.fenc...1fb0: 00000000 02000000                    ........Item   4 -- Length:   24  Offset: 8072 (0x1f88)  Flags: NORMALBlock Id: 2147483664  linp Index: 1  Size: 24Has Nulls: 0  Has Varwidths: 11f88: 00801000 01001840 09666f78 00000000  .......@.fox....1f98: 00000000 01000000                    ........Item   5 -- Length:   32  Offset: 8040 (0x1f68)  Flags: NORMALBlock Id: 2147483664  linp Index: 2  Size: 32Has Nulls: 0  Has Varwidths: 11f68: 00801000 02002040 0b6a756d 70000000  ...... @.jump...1f78: 00000000 01000100 01000000 00000000  ................Item   6 -- Length:   32  Offset: 8008 (0x1f48)  Flags: NORMALBlock Id: 2147483664  linp Index: 2  Size: 32Has Nulls: 0  Has Varwidths: 11f48: 00801000 02002040 0b6c617a 69000000  ...... @.lazi...1f58: 00000000 01000100 02000000 00000000  ................Item   7 -- Length:   32  Offset: 7976 (0x1f28)  Flags: NORMALBlock Id: 2147483664  linp Index: 2  Size: 32Has Nulls: 0  Has Varwidths: 11f28: 00801000 02002040 0d717569 636b0000  ...... @.quick..1f38: 00000000 01000100 01000000 00000000  ................Item   8 -- Length:   24  Offset: 7952 (0x1f10)  Flags: NORMALBlock Id: 2147483664  linp Index: 1  Size: 24Has Nulls: 0  Has Varwidths: 11f10: 00801000 01001840 0d736c65 65700000  .......@.sleep..1f20: 00000000 03000000                    ........<Special Section> -----GIN Index Section:Flags: 0x00000002 (LEAF)  Maxoff: 0Blocks: RightLink (-1)1ff8: ffffffff 00000200                    ........*** End of Requested Range Encountered. Last Block Read: 1 ***

输出中,从item 1…item8,这个entrypage一共包含8个item,这与我们上面的查询一致的,并且每个item都包含这样的结构:

  1fb8: 00801000 02002040 09646f67 00000000  ...... @.dog....1fc8: 00000000 01000100 02000000 00000000  ................
  1. 第一行中( 1fb8: 00801000 02002040 09646f67 00000000 … @.dog…
    ):
  • 00 80 10 00 02 00 20 40
    这前 8 字节是元信息 / tuple header(Postgres 的 tuple header / GIN internal header),包含诸如 t_infomask、t_hoff、gin-item 的元字段等。此处不是我们关心的 posting-list 内容,因此不展开逐位解释
    这块包含 lexeme(词条)本身:
  • 09 64 6f 67 00 00 00 00
    09:这是 varlena/文本的头部字节(包含长度/标志等),常见于 Postgres 存储的短文本格式。
    我们不必在此把 varlena 的头位 bit-by-bit 拆开 —— 重点是后面实际的字符字节。
    64 6f 67 是 ASCII “dog”,这是索引的 lexeme(词条)。
demo=# demo=# select chr(x'64'::int),chr(x'6F'::int),chr(x'67'::int);chr | chr | chr
-----+-----+-----d   | o   | g
  • 后面 00 00 00 00 是对齐/填充,使后面 posting-list 从对齐位置开始。
  1. 第二行中:
    在 GIN 数据页里,紧跟词条后面就是 posting list 或 posting tree 的指针。
    在进一步解读前,我们需要先了解posting list/posting tree指针的源码结构:
typedef struct ItemPointerData
{BlockIdData ip_blkid;   /* 4 字节,块号 */OffsetNumber ip_posid;  /* 2 字节,行号(slot) */
} ItemPointerData;
  • BlockIdData = 4 字节,存储 heap 表的 block number
  • OffsetNumber = 2 字节,存储该 block 上的 行号 (line pointer index)
    注意:实际存储是 小端字节序(Postgres 在磁盘上是 little-endian)
    一个 ItemPointerData 占 6 字节,但在实际存储时会补齐到 4 字节对齐,所以通常会看到 8 字节一组。

第二行中的 16 字节是:

00 00 00 00   01 00 01 00   02 00 00 00   00 00 00 00

我们拆开:
00 00 00 00 —— 对齐 / padding(跳过)。
01 00 01 00 —— 关键的第一段,按 16-bit 分成两部分(小端):

  • 前两个字节 01 00(小端) = 0x0001 = 十进制 1,这是block number(块号),代表指向heap table的第1个块号,而在heap table中第一个块号是0
  • 后两个字节 01 00(小端) = 0x0001 = 十进制 1,即第一个 offset(offset1)= 1。
    到这里,我们获得post list中第一个tid (block, offset1) = (0, 1)。
    02 00 00 00 —— 接下来是一个 32-bit 小端整数:0x00000002 = 十进制 2,这是一个 offset delta(后续 offset 相对前一 offset 的增量)。
    于是,我们获得同一块的第二个offset(1+2=3),因此第二个tid (block, offset1) = (0, 3)。
    00 00 00 00 —— 结束 / 填充(通常用 0 作为终结标记)。

最终结论(映射回 heap table的tid)
所以这段 posting-list 对应的两个 heap tuple 是:
(0,1) —— 表里 id = 1,内容 “The quick brown fox jumps over the lazy dog”(含 “dog”)
(0,3) —— 表里 id = 3,内容 “The lazy dog sleeps”(含 “dog”)
也就是说 “dog” 在行 (0,1) 和 (0,3),与表里的文本一致。

demo=# select ctid,* from articles;ctid  | id |                   content
-------+----+---------------------------------------------(0,1) |  1 | The quick brown fox jumps over the lazy dog(0,2) |  2 | A quick jump over the brown fence(0,3) |  3 | The lazy dog sleeps
(3 rows)

总结

一个 GIN 索引包含:

  • 一个 metapage
  • 一个 BTree of key entries(键条目的 B 树)
  • 叶子页 (leaves) 要么包含指向 posting tree 的指针,要么包含一个 posting list of heap pointers(堆指针的 posting 列表)
  • 这些指针在物理内存中是有序的;在 posting tree 中,使用 tid 作为键来构建树
  • 还没有被索引的行存放在 pending list 中

GIN 索引具有非常独特的结构。最重要的部分是理解:
被索引的 values 会被拆分以生成 keys
这也是它在 fulltext search、arrays 和 jsonb 上非常高效的原因。

不过,GIN 也有一些针对 integers 的扩展。例如,如果想索引一个不同值(different values)不多的列,这可能会很有价值,因为 BTree 会被优化。但在搜索方面,我发现它不一定比 BTree 更好,可能是因为需要访问 posting lists 和 posting trees。


文章转载自:

http://cZdAgpSw.qxkjy.cn
http://TN3vWvhv.qxkjy.cn
http://3llHP4kY.qxkjy.cn
http://1JoO6DZq.qxkjy.cn
http://obzG0mWZ.qxkjy.cn
http://hrBeB6L0.qxkjy.cn
http://C4AMRriO.qxkjy.cn
http://lqo1Xoxs.qxkjy.cn
http://pJIG13yz.qxkjy.cn
http://PP6bv8tX.qxkjy.cn
http://7YvpGjIr.qxkjy.cn
http://76qH2zcq.qxkjy.cn
http://5sy58X1p.qxkjy.cn
http://3bU15uGI.qxkjy.cn
http://xMoPNdgk.qxkjy.cn
http://U3pfLKjL.qxkjy.cn
http://ePJDfK2a.qxkjy.cn
http://o9Bq2PIm.qxkjy.cn
http://5bI9WWuK.qxkjy.cn
http://xhOHdYqP.qxkjy.cn
http://SN9SU3ER.qxkjy.cn
http://Bet7YUFh.qxkjy.cn
http://SRIKpIy3.qxkjy.cn
http://Sd9Cm847.qxkjy.cn
http://O9VXiDR3.qxkjy.cn
http://TchctkDT.qxkjy.cn
http://kXXtrL0P.qxkjy.cn
http://n0Hjq3wn.qxkjy.cn
http://7O0DGEAH.qxkjy.cn
http://oajuRSiq.qxkjy.cn
http://www.dtcms.com/a/385461.html

相关文章:

  • 老鸟对单片机全局变量常用用法(读写在2个独立函数中)
  • 大前端社交应用中 AI 驱动的内容审核与反垃圾信息机制
  • MP3的ID3信息简介及其如何解析
  • MyBatis-Plus 扩展全局方法
  • java中的泛型
  • 使用 AWS Comprehend 综合指南
  • 使用秩和比拟解决非独立同分布情况下的投毒攻击
  • 七、vue3后台项目系列——包装scss、全句变量scss与导入
  • 煤矿山井下绝绝缘监测故障定位
  • 海外分部人员OA请假申请时长为0
  • MySQL --JDBC
  • python使用pyodbc通过不同认证方式连接sqlserver数据源
  • java通过线程池加CompletableFuture实现批量异步处理
  • Coze源码分析-资源库-创建知识库-后端源码-详细流程梳理
  • 极简版 Nginx 反向代理实验步骤
  • python-86-基于Graphviz或Mermaid绘制流程图
  • 智能农机无人驾驶作业套圈路径规划
  • Rayon Rust中的数据并行库入门教程
  • NumPy数组与Python列表的赋值行为解析
  • 基于 AI 的大前端智能家居控制应用开发
  • RAGFlow集成SGLang部署的大模型:实现OpenAI API兼容的自定义LLM调用
  • sqlsever 内存配置错误无法连接,后面恢复连接
  • 51c大模型~合集182
  • 2025.9.15总结
  • 深入理解 Roo Code 的 Code Actions 功能
  • Java---线程池讲解
  • PEFT QLora Deepspeed Zero Stage 3 Offload Trainning
  • 线程概念,控制
  • 扫描仪常见样式:平板与馈纸的特性与适用场景
  • Python进程和线程——多线程