拒绝停服, 随时回退:Sybase 到 Postgresql 的无缝数据库双向迁移方案

本期内容亮点:高可用灾备实时数据同步解决方案

**背景:**某公营机构负责管理地区医疗数据与公共卫生信息。随着数据需求与监管要求日益增加,机构原使用的 Sybase ASE 数据库因版本老化、性能瓶颈及官方维护即将终止面临紧迫的迁移需求。

挑战与需求

  • 业务连续性:机构所管理的医疗和卫生数据为关键业务数据,迁移过程必须确保业务零中断。

  • 数据完整性与精度:迁移后的数据精度不能有任何损失,特别是时间戳和数字类型的精确匹配问题。

  • 实时同步与回滚能力:迁移后须具备实时数据同步功能,以实现平滑的业务过渡及必要时的快速回滚。

  • 高可用性与负载均衡:迁移新环境需要具备高可用架构,避免单点故障导致服务中断。

概述

本项目的重点在于将关键业务应用从 Sybase ASE 数据库迁移至 PostgreSQL 数据库。为确保平滑、准确、完整的迁移,同时实现最小停机时间和可回退能力,策略如下:

  1. 建立实时复制链路:从 Sybase ASE 到 PostgreSQL 实时同步数据;

  2. 数据验证:在切换应用至 PostgreSQL 前完成数据一致性校验;

  3. 切换与反向同步:应用切换至 PostgreSQL 后,反向复制数据,实现双库并行运行(burn-in period),确保当应用在 PostgreSQL 运行期间遇到任何问题时,能够及时回退至 Sybase ASE,从而避免数据丢失。

步骤1:安装 TapData 并配置数据库

TapData 与 MongoDB 部署在三台服务器上,构建了一套高可用、负载均衡的三节点TapData集群架构。该架构的核心特性包括:

  1. 高可用性:若任一节点故障,剩余节点可自动接管服务,避免单点故障导致的系统停机;

  2. 负载均衡:流量通过均衡机制均匀分配至所有节点,防止单节点过载,确保整体性能不会受到影响,从而提升系统响应速度与稳定性;

  3. 数据一致性:通过集群内同步机制保障节点间数据一致,即使节点故障或切换时,数据仍可完整保留,确保业务连续性与可靠性。

📖 相关文档:部署高可用 TapData Enterprise(三节点)

步骤2:数据库对象迁移(从 Sybase ASE 到 PostgreSQL)

除了基本表结构迁移之外,还有其他数据库对象需要迁移,并评估其对同步链路的影响,包括:逻辑视图(logical views)、物化视图(materialized views)、存储过程(stored procedures)、触发器(triggers)、索引(indexes)、约束(constraints)、序列(sequences)以及默认值(default values)。

即使是基础架构本身,也需要针对标识列(identity columns)、主键(primary keys)、外键(foreign keys)和字段类型映射(field type mapping)进行细致调整。

尽管 Sybase ASE 和 PostgreSQL 都使用类似的 SQL 语法,但仍存在需要注意的差异。通过使用 TapData 工具进行迁移,除了视图、存储过程、触发器和检查约束(check constraints)需要手动迁移外,上述其他对象均可由 TapData 自动完成迁移工作。

字段类型映射

在从一个数据库复制数据到另一个数据库时,总会存在某些字段类型无法找到精确对应的情况,此时需选择最接近的类型进行映射。这种不精确的类型映射可能导致两个数据库间的数据值范围或精度存在差异。由于现有应用使用的是 Sybase ASE,因此在迁移时,我们倾向于让 PostgreSQL 中的字段类型具备更大的范围或更高的精度,以确保从 Sybase ASE 向 PostgreSQL 迁移过程中不会产生数据精度损失。

但同时,这也意味着当我们进行反向数据复制(即从 PostgreSQL 向 Sybase ASE)时,可能存在数据精度丢失的风险。不过,这种风险被部分缓解了,因为应用已适配PostgreSQL写入逻辑,实际数据溢出风险可控。

主键/唯一性注意事项

在将字段用作主键进行数据匹配时需格外谨慎,精度差异可能导致主键意外冲突。例如,尽管两数据库均支持时间戳字段类型,但其精度存在显著差异:

  • Sybase ASE 中,DATETIME 类型的精度为 1/300秒(约3.33毫秒)

  • 而在 PostgreSQL 中,TIMESTAMP 类型(包括 TIMESTAMP WITHOUT TIME ZONETIMESTAMP WITH TIME ZONE)支持更高的精度,最小可达微秒级(1/1,000,000秒)

关键说明

  • PostgreSQL默认精度为微秒,但可显式指定精度(如TIMESTAMP(3)表示毫秒,TIMESTAMP(6)表示完整微秒);

  • 冲突场景举例:

    • PostgreSQL中,2024-12-23 18:48:13.1252024-12-23 18:48:13.128为唯一值;

    • Sybase ASE中,两者可能被截断为同一值(如2024-12-23 18:48:13.126),若该字段为主键或要求唯一性,将导致数据冲突。

简而言之,如果此类字段用作主键或具有唯一性要求,就可能在 Sybase ASE 中引起重复键值的冲突问题。

字段类型映射表

下表为 TapData 的默认字段类型映射规则(虽然未涵盖 Sybase ASE 所有可能的字段类型,但已覆盖本项目实际使用的所有类型)。此外,值得一提的是,TapData 还支持自定义字段类型映射规则,用户可根据需求调整配置。

类型类别

Sybase ASE 类型

PostGreSQL 类型

Character

CHAR

CHAR

VARCHAR

VARCHAR

TEXT

TEXT

UNICHAR

UNICHAR

UNITEXT

TEXT

UNIVARCHAR

VARCHAR

Integer

BIGINT

BIGINT

INT

INT

SMALLINT

SMALLINT

TINYINT

TINYINT

UNSIGNED BIGINT

NUMERIC(20)

UNSIGNED INT

BIGINT

UNSIGNED SMALLINT

INT

Numeric, decimal, float

NUMERIC

NUMERIC

DECIMAL

DECIMAL

FLOAT

DOUBLE

DOUBLE

DOUBLE

REAL

REAL

Date/Time

DATE

DATE

DATETIME

TIMESTAMP

SMALLDATETIME

TIMESTAMP(0)

TIME

TIME

Money

MONEY

MONEY

SMALLMONEY

MONEY

Binary

BINARY

BYTEA

VARBINARY

IMAGE

TIMESTAMP

Bit

BIT

BOOLEAN

部分映射还需进行数据值转换,例如位类型(BIT)的 0/1 映射到 BOOLEAN 类型的 true/false。此类转换可由 TapData 自动处理,无需手动配置。

数据范围丢失情况

  1. UNSIGNED BIGINT 映射到 NUMERIC(20,0)
  • UNSIGNED BIGINT(Sybase ASE 15.7 及以上版本):

    • 范围:从 018,446,744,073,709,551,615(即 2⁶⁴ - 1)。
  • NUMERIC(20,0)(PostgreSQL):

    • 范围:从 -9,999,999,999,999,999,9999,999,999,999,999,999,999

在本案例中,PostgreSQL 的数值范围相对较小。然而,经过确认,本次迁移中 Sybase ASE 的 UNSIGNED BIGINT 字段内不存在数值大于 9,999,999,999,999,999,999 的数据,因此该映射方式对实际业务未造成影响。

空格填充处理

部分字段类型(如CHAR()UNICHAR())在写入时会自动填充空格以满足目标字符串长度,Sybase ASE 和 PostgreSQL 这两个数据库的填充逻辑一致。但若将此类字段映射为VARCHAR(),则填充空格可能导致两库数据差异,需在数据验证阶段特别处理。本项目中,我们则是通过合理的映射配置规避了此问题。

TapData 的自动处理机制

  1. 源数据修剪 :从源库 Sybase ASE 的CHAR()UNICHAR()字段读取数据时,自动移除右侧多余空格;

  2. 目标库填充:写入 PostgreSQL 时,目标库按字段定义重新填充空格至指定长度。

时间戳处理

Sybase ASE 的 TIMESTAMP 类型为非标准实现,其本质是用于行版本控制(row versioning)与乐观并发控制(optimistic concurrency control)的二进制值,不存储实际时间。使用 TapData 进行模式迁移时,Sybase ASE 的 TIMESTAMP 字段类型将被映射为 PostgreSQL 的 BINARY 类型,这样可以确保源数据库的二进制数据得以无损传输到目标库。

但是,如果在迁移完成后,在 PostgreSQL 中绕过 TapData 复制直接向同一表中插入新记录,这些新记录不会自动生成类似的时间戳数据,导致该行的BINARY字段值为空(NULL)。因此,应用开发者在迁移到 PostgreSQL 后,必须小心地采用其他方式实现并发控制机制。

如果项目仅需要真实的时间戳数据(不用于并发控制),则可以考虑在 PostgreSQL 中直接使用 TIMESTAMP DEFAULT now() 语法来满足该需求。

主键与标识列

主键处理

部分 Sybase ASE 源表未定义主键,但 CDC(变更数据捕获)需依赖主键区分插入与更新操作。在 TapData 中,这类用于识别更新的字段被称为"更新字段"(update fields)。而针对这些缺失主键的表,TapData 的解决方案是:当存在不确定时,如果源数据库表没有明确定义主键,我们可以将表中的所有字段都设置为主键以满足复制需求。

标识列

标识列(Identity column,也称自增列或序列列)是一种自动生成唯一数字值的字段,通常用作主键。在 Sybase ASE 和 PostgreSQL 中均有类似的实现,但存在一些细微差异。

具体差异表现为:

  • PostgreSQL 禁止使用 NUMERIC 类型定义标识列,而 Sybase ASE 则允许。因此,TapData 在处理该场景时,未直接沿用前面提及的默认类型映射表,而是单独处理。当 Sybase ASE 的 NUMERIC(20,0) 类型字段被定义为标识列时,TapData 会自动将其映射到 PostgreSQL 的 BIGINT 类型。

  • 如果尝试直接将 Sybase ASE 的 NUMERIC IDENTITY 标识列映射到 PostgreSQL 的 NUMERIC IDENTITY 标识列,将导致 PostgreSQL 报错。

标识列与 CDC 同步风险

核心风险

由于本项目使用 CDC 技术捕获 Sybase ASE 变更并同步至 PostgreSQL,因此若标识列处理不当,会带来一定风险。试想,如果目标库将从 Sybase ASE 生成的标识(ID)替换为自身重新生成的标识,并且二者不匹配,就会导致严重问题,尤其是这些标识通常被作为其他表中的外键使用。此时,其他表使用的是源数据库的标识,而不是目标数据库新生成的标识。一些可能引起此类标识错配的场景包括数据更新更新操作乱序、变更记录丢失,或是两端序列号生成器调整不同步,等等。

解决方案:允许手动插入自增值

为避免上述风险,我们需要确保所有标识列支持手动插入数据,从而保证 Sybase ASE 生成的标识值可原样准确地写入 PostgreSQL 数据库。同时,为了支持可能发生的反向数据同步(如容灾场景下的回切需求),两个数据库均应具备相同的灵活性。在 PostgreSQL 中,可以将字段创建为:

vbnet 复制代码
GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

以允许该字段进行手动插入。

然而,在 Sybase ASE 中,标识列的实现存在限制。要覆盖自动生成的标识值,需要在执行插入操作前,使用:

vbnet 复制代码
SET IDENTITY_INSERT ON

但一次只能对一个表开启该设置。如果在第二个表启用该设置,Sybase ASE 会自动关闭第一个表的设置。幸运的是,TapData 会自动处理这个问题,在向具有标识列的表写入数据前,自动执行该命令,以确保标识值的一致性。

*另外需注意的是,Sybase ASE 的序列(sequences)功能与标识列不同,且提供更多灵活性,但本项目并未涉及 Sybase ASE 的序列功能,因此本文档中未对其作详细说明。

标识序列号管理

需要注意的是,当为标识列(自增字段)提供显式值时,序列号不会自动递增。如果需要更新序列号,必须额外执行一些操作,例如在 PostgreSQL 中执行以下查询:

sql 复制代码
SELECT setval('EmployeeSeq', 50001);
ALTER TABLE Employee ALTER COLUMN EmployeeSeq SET GENERATED BY DEFAULT RESTART WITH 50001;

此外,在故障转移场景中,当主库离线并且备库升级为新主库时,旧主库可能存在未及时复制到新主库的事务。为了在旧主库恢复后更容易取回这些数据,避免主键 ID 冲突,建议新主库在生成序列号时,不要简单从最新接收的 ID 后续数字开始,而是留有一定的序列号空间。简言之,新主库的序列号起始值应远高于旧主节点的最新值(而非连续递增),避免 ID 重叠。

针对上述问题,TapData 提供了一个巧妙的功能:允许你设置同步标识序列值的页面大小。例如,设置页面大小为 100,则每次 CDC 流程从源库读取日志页面时,会额外插入一条 SQL 语句,将目标库的序列号提前设置为源数据库当前序列号加 100。这样一来,无论在任何时刻,应用从 Sybase ASE 切换到 PostgreSQL 写入数据时,PostgreSQL 中的序列号已经预先设置为未使用的值,不再需要进行额外调整,可以直接使用。

虽然在序列号切换时可能会出现少量序列号的断层(即序列号不连续),但这并非重大问题,反而在故障转移场景中有益处。假设源库中存在因崩溃未能及时同步至目标库的数据行,当源库恢复后,缺失数据可无缝同步至目标库,无需担心序列号冲突。

采用这种方法的核心价值在于:

  • 免手动干预:持续同步并更新目标库序列号,无需人工维护;

  • 高可用保障:故障转移时无序列号调整需求,降低操作复杂度;

  • 零冲突风险:通过预留间隔策略,即使发生主备库切换,也无需额外调整序列,彻底规避标识列 ID 冲突。

外键与约束

在使用 CDC 技术时,外键和类似的跨表约束是潜在的问题高发区。当源库执行涉及多行数据的增删改 SQL 操作(如批量更新)时,CDC 会将这些这也操作记录在日志文件中,并将其逐条拆分成单独的 SQL 语句,以便分别在目标数据库中重放执行。这可能会在目标库端触发外键约束或其他约束的冲突。

为了提高数据复制的吞吐量,TapData 提供了并行写入(parallel writes)功能,该功能将日志文件中的更新操作分配到不同的线程并发执行。为确保单个数据表更新顺序的一致性,所有针对同一张表的更新操作都会被分配到同一个线程内执行。但该方法并不能确保多个表之间的更新操作顺序完全与源库执行时的顺序一致。

为避免并行写入场景下外键约束可能引发的写入失败问题,TapData 在实现 PostgreSQL 数据同步时采用了创新的会话级约束管理策略:即在当前会话中临时禁用外键约束检查及其他关联约束验证机制,以防止数据写入过程中出现外键约束冲突的问题。具体使用的命令如下:

ini 复制代码
session_replication_role = 'replica'

此方法仅对当前复制会话有效,其他数据库连接不会受到影响。这样一来,可以安全地启用并行写入特性,避免因外键冲突导致的失败。

然而,对于 Sybase ASE,并不存在类似的会话级别关闭约束功能。因此,在向 Sybase ASE 复制包含外键或其他约束的数据时,建议不要开启并行写入功能,以防止数据写入过程中出现外键约束冲突的问题。

默认值处理

理论上,默认值处理应该是非常直接的。默认值在 Sybase ASE 与 PostgreSQL 中均仅作用于 INSERT 插入操作,因此在 UPDATE 更新场景中无需考虑。当然,你也可以显式地将一个值更新为默认值(DEFAULT),从而将其恢复为默认设置,但这并不会引起额外的问题。

例如,如果源数据库中字段 quantity 的默认值是1,目标数据库也设置为同样的默认值,则具体场景如下:

场景

源数据库

目标数据库

显式地将 quantity=3 插入源数据库

quantity=3

quantity=3

向源数据库插入新行且未指定 quantity 值

quantity=1(来自默认值)

quantity=1 (来自源数据库)

向目标数据库插入新行且未指定 quantity 值

quantity=1 (来自默认值)

显式地将 quantity=3 插入目标数据库

quantity=3

但值得注意的是,Sybase ASE 与 PostgreSQL 在默认值定义的具体语法上存在一些差异,例如定义时间戳字段时的差异:

  • Sybase ASE:

    CREATE TABLE Employees ( -- other fields...

    CreatedAt DATETIME DEFAULT getdate() );

  • PostgreSQL:

    CREATE TABLE Employees ( -- other fields...

    CreatedAt TIMESTAMP DEFAULT NOW() );

此外,Sybase ASE 在许多情形下不支持默认值,而 PostgreSQL 则支持。例如,在 Sybase ASE 中:

  • TEXT 或 IMAGE 类型的字段不支持设置默认值;

  • 不支持复杂表达式作为默认值;

  • 对函数作为默认值的支持有限,仅在 v16 及以上版本支持特定函数(如 getdate(), newid(), host_name(), suser_name())。

不过在本项目中,这些问题并未带来额外挑战,因为本次迁移是从对默认值支持较为有限的 Sybase ASE 向默认值支持更为广泛的 PostgreSQL 进行的。

索引迁移

总体而言,索引的迁移也是较为简单直接的过程,但在两种数据库支持的索引类型上存在一些差异值得关注:

  • Sybase ASE 支持聚簇索引(CLUSTERED INDEX),而 PostgreSQL 不支持;

  • PostgreSQL 支持表达式索引(expression/function indexes)和部分索引(partial indexes),而 Sybase ASE 不支持。

本项目中,我们主要处理的是聚簇索引的迁移问题。PostgreSQL 虽然不直接支持自动维护聚簇索引,但可以通过周期性地执行 CLUSTER 命令来手动维护聚簇索引。然而,PostgreSQL 不会自动持续维护聚簇状态。由于聚簇索引仅对数据库性能产生影响,并不会改变数据库的功能行为,因此可以简单地通过定期执行手动的 CLUSTER 命令(通常安排在低负载时段)来维护聚簇状态。

TapData 对空表执行 CLUSTER 命令没有任何作用,并且 TapData 本身无法确定合适的执行时间,因此 TapData 不处理聚簇索引维护工作,而是由数据库管理员(DBA)根据自身需求和负载情况,自行安排定期执行 CLUSTER 命令。

当 TapData 在 PostgreSQL 中创建 schema 和索引时,会忽略源数据库定义中与聚簇索引相关的部分(即不创建聚簇索引)。

另外,在索引方面还需注意的是,用于 CDC 更新查找的主键字段应当全部建立索引。不过,TapData 也会自动为缺失索引的主键字段创建索引,因此用户无需额外处理。

逻辑视图与物化视图

逻辑视图(Logical Views)和物化视图(Materialized Views)在数据库更新时不会写入更新日志,因此不会对 CDC 捕获机制产生任何影响。简而言之,在源库和目标库上建立这些视图属于相互独立的操作,可以单独维护,不会参与或影响 CDC 数据流。

因此,这些视图的迁移需人工手动完成,而不是通过特定工具自动进行。TapData 在复制数据库架构和数据时,会自动忽略这些视图对象。

存储过程

存储过程(Stored Procedures)本身不会被写入数据库更新日志。实际上,调用存储过程的 SQL 查询会导致数据库执行一条或多条更新操作,而这些操作(插入、更新、删除)才会被记录在数据库更新日志中。因此,大多数存储过程可以同时存在于 CDC 复制的源库和目标库中,但当存储过程被调用时,仅在源库执行一次,随后产生的数据变动则会通过 CDC 机制复制到目标库。目标库上的存储过程实现,在用户正式切换到目标库使用前通常处于闲置状态,不会被触发执行。

有一些存储过程会导致架构结构(DDL)的变更,而非数据的变更。如果这些 DDL 结构变更也被 CDC 机制捕获复制,那么它们的处理方式与数据变更是一样的。但如果 CDC 不复制这些 DDL 变更,那么需仔细考虑如何单独处理这些脚本,以确保目标数据库与源数据库保持一致。

*由于存储过程的语法、功能及实现细节在两个数据库之间通常存在较多差异,因此每个存储过程的迁移都是单独的任务,需要进行人工逐一迁移和处理。这项工作超出本文讨论的范围,需要单独进行规划与实施。

触发器

触发器(Triggers)与存储过程类似,但由于它们可被数据的更新操作自动触发,因此在 CDC 复制场景下可能产生问题,需要特别谨慎处理。举例而言,假如表 A 有一个触发器,当表 A 有数据更新时,会自动向表 B 插入一条数据。如果源库和目标库都定义了这个触发器,那么目标库的表 B 就可能产生两条重复的数据记录:一条来自于源库复制过来的数据,另一条则是目标库自身触发器生成的数据。因此,每个触发器的功能都必须单独仔细评估,并决定具体如何进行处理。

处理触发器的选项包括:

1.如果触发器的唯一功能是向另一张表(例如审计表,audit table)插入审计记录,并且该审计表中没有手工插入数据,所有记录都只通过触发器生成,那么可以在目标库中保留启用此触发器,但 CDC 复制时应排除该审计表的数据,以确保审计记录保持本地化,不会重复。

2.如果触发器对某个表的写入或更新操作,并非仅仅由触发器生成,同时还存在其他途径的更新操作,那么最佳做法是让 CDC 流程负责复制该表数据,同时在 CDC 复制过程中,目标库的相关触发器应暂时禁用。当 CDC 复制停止、目标数据库变成主库后,再重新启用这些触发器。

TapData 采用的即是此种方法,它会自动在会话(session)级别禁用目标数据库的触发器,而不会影响其他用户连接的使用。

3.如果某个触发器涉及对多个表的写入或更新操作,并且无法清晰归类到上述第1或第2种情况,则可能需要采用第2种方法,除非可以将触发器的功能拆分,以分别处理多个表的更新。

无论选择哪种处理方案,触发器具体实现脚本的细节都至关重要,并需要经过仔细的测试,以确保达到期望的效果。

总结

综上所述,在 TapData 的帮助下,该公营机构近期成功启动核心业务系统从 Sybase ASE 至 PostgreSQL 的数据库迁移。本项目以高可用性、零数据丢失、最小停机时间为核心目标,通过 TapData 工具链与定制化策略的结合,攻克了异构数据库迁移中的多项技术挑战,最终达成平稳过渡与业务连续性保障。

核心挑战与解决方案回顾

  1. 实时同步与回退能力

    • 部署 TapData 三节点高可用集群,实现 Sybase ASE 至 PostgreSQL 的双向实时同步,支持故障时秒级回退至原库;

    • 通过数据校验工具确保迁移前后数据一致性,规避潜在差异风险。

  2. 关键对象迁移适配

    • 字段类型映射 :自动化处理多数类型转换,针对时间戳、自增列等特殊场景设计精准映射规则(如DATETIME→TIMESTAMPNUMERIC→BIGINT);

    • 外键与约束 :通过 PostgreSQL 的session_replication_role临时禁用约束,保障并行写入效率;

    • 索引与性能 :保留非聚集索引结构,制定周期性CLUSTER优化任务,平衡存储性能与维护成本。

  3. CDC 风险管控

    • 启用 TapData 序列号预留策略,确保主备切换时无 ID 冲突;

    • 自动化执行SET IDENTITY_INSERT ON,解决Sybase ASE自增列手动插入限制。

    • ......

迁移成果与价值

  • 业务连续性:全程停机时间控制在分钟级,关键服务无感知切换;

  • 系统性能提升:PostgreSQL的并发处理与扩展能力支撑未来业务增长;

  • 运维成本优化:TapData自动化同步机制减少人工干预,DBA可聚焦高阶优化任务;

  • 合规与安全:全链路数据一致性保障,满足公营机构对数据完整性与审计的严苛要求。

本次迁移验证了 TapData 在异构数据库场景下的灵活性与可靠性,为公营机构后续系统升级提供了可复用的技术框架。未来,持续深耕异构数据实时同步与迁移领域,持续深化技术能力,为各行业客户提供更高效、更智能的解决方案。

相关推荐
日行月白4 分钟前
Day14:关于MySQL的索引——创、查、删
数据库·mysql
异常君1 小时前
深入解析 InnoDB 死锁:从案例到方案,全流程透视指南
数据库·后端·mysql
꧁坚持很酷꧂1 小时前
Qt实现文件传输服务器端(图文详解+代码详细注释)
开发语言·数据库·qt
苏牧keio1 小时前
安装MySQL8.0
数据库
半糖土豆爱编码_1 小时前
【mysql】Mac 通过 brew 安装 mysql 、启动以及密码设置
数据库·mysql·macos
Lary_Rock2 小时前
ubuntu20.04 Android14编译环境配置
大数据·数据库·elasticsearch
烂漫心空2 小时前
Windows 系统如何使用Redis 服务
数据库·数据仓库·redis·mysql·缓存·数据库架构
UniLCodes2 小时前
✅ MySQL 事务 & MVCC & ROLLBACK
数据库·mysql
SHIPKING3932 小时前
【LangChain核心组件】Memory:让大语言模型拥有持续对话记忆的工程实践
数据库·python·langchain·llm·memory
在努力的韩小豪3 小时前
MySQL中的UNION和UNION ALL【简单易懂】
数据库·sql·mysql·结果集合并·union和union all