2026 年第 18 周,PostgreSQL 社区活动密集:比利时 PGConf、柏林与爱丁堡的本地 Meetup 同期举行,而最值得关注的是 Talking Postgres 播客新一期——Adam Prout 分享了从 MemSQL 迁移到 HorizonDB 的工程师旅程。迁移故事比会议日程更有实操价值,本文先梳理社区动态,再聚焦迁移话题,给出可直接改造的评估脚本。
比利时 PGConf:演讲阵容与组织细节
PGConf Belgium 于 5 月 5 日在比利时举办,Wim Bertels、An Vercammen 和 Grégory Gioffredi 既负责组织也参与演讲遴选。演讲者阵容覆盖了从内核到应用的多个方向:
- Bruce Momjian——长期 PostgreSQL 核心贡献者,通常讲内核架构或新特性演进。
- Franck Pachot——擅长性能调优与执行计划分析,对 Oracle 到 PG 的迁移有深入研究。
- Robert Treat——同时在柏林 Meetup 也有演讲,活跃于社区运营与最佳实践推广。
- Jan Karremans、Boriss Mejias、Dwarka Rao、Emrah Becer、Mohsin Ejaz、Afroditi Loukidou、Gianni Ciolli、Matt Cornillon、Xavier Fisher、Thijs Lemmens、Josef Machytka、Claire Giordano、Aaron Wislang——覆盖了分布式、安全、运维、云原生等主题。
会议具体演讲内容在摘要中未展开,但从演讲者背景可以推断重点方向:内核机制、性能诊断、迁移策略。如果你关注某一演讲者的专长领域,可以在 PGConf Belgium 官网查找演讲录影。
播客亮点:从 MemSQL 到 HorizonDB
5 月 6 日,Claire Giordano 和 Aaron Wislang 发布了 Talking Postgres 新一期,嘉宾 Adam Prout 讲述了从 MemSQL(现 SingleStore)迁移到 HorizonDB 的过程。
这条迁移路径有几个值得注意的技术点:
- MemSQL 的定位——分布式 HTAP 数据库,主打实时分析 + 事务混合负载,语法兼容 MySQL 但有大量专有扩展(如
REFERENCE表、聚合索引)。 - HorizonDB——摘要未给出技术细节,但从迁移叙事推断,它大概率是一个基于 PostgreSQL 或兼容 PostgreSQL 协议的分布式方案。迁移的核心挑战通常集中在:专有语法替换、分布式拓扑映射、查询计划差异导致的性能回退。
- 迁移策略——从 MemSQL 类系统迁出,常见做法是先用
pg_dump/逻辑复制建立基线,再逐模块替换专有语法,最后做性能对标。Adam 的具体步骤需听播客确认,但工程经验表明,这类迁移最容易踩坑的地方是聚合索引的替代方案——PostgreSQL 没有直接等价物,需要用物化视图或列式扩展(如citus分区 + 聚合表)来弥补。
柏林与爱丁堡 Meetup
5 月 7 日同一天,两个城市各自举办了 PostgreSQL Meetup:
- 柏林:Andreas Scherbaum、Daria Aleshkova、Sergey Dudoladov、Oleksii Kliukin 组织;Robert Treat 和 Celeste Horgan 演讲。Andreas 长期推动柏林 PG 社区,Oleksii 在 PG 高可用与存储引擎方面有实战经验。
- 爱丁堡:Jimmy Angelakos 组织;River MacLeod、Jim Gardner 和 Jimmy Angelakos 本人演讲。Jimmy 是爱丁堡 Meetup 的持续推动者。
Meetup 的价值在于面对面讨论生产环境问题——如果你在附近城市,这类活动是获取非文档化经验的好渠道。
实践:数据库迁移评估脚本
播客中的迁移故事启发了一个实际问题:当你需要从任意源数据库迁到 PostgreSQL(或兼容 PG 的系统)时,如何快速评估兼容性差距?下面是一个可改造的 Python 脚本,它连接源库,扫描专有语法模式,输出兼容性报告。
前提:你需要 psycopg2(目标 PG)和源库驱动(如 mysql-connector-python 用于 MemSQL/MySQL 兼容源)。脚本以 MySQL 兼容源为例,替换连接参数即可适配其他源。
#!/usr/bin/env python3
"""
migration_assess.py — 从 MySQL/MemSQL 兼容源扫描专有语法,评估 PG 迁移差距。
用法: python migration_assess.py
输出: JSON 格式的兼容性报告,列出需手动处理的 SQL 模式。
"""
import re
import json
import sys
from pathlib import Path
# ---------- 配置区 ----------
SOURCE_CONFIG = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "secret",
"database": "my_app",
}
# MemSQL / MySQL 专有语法模式 → PG 替代建议
PATTERNS = [
{
"name": "REFERENCE table (MemSQL)",
"regex": r"CREATE\s+REFERENCE\s+TABLE",
"pg_alternative": "用普通表 + 外键替代,或考虑 Citus 分布式引用表",
"severity": "high",
},
{
"name": "聚合索引 (MemSQL)",
"regex": r"CREATE\s+INDEX\s+.*\s+ON\s+.*\s*\(\s*.*\s*\)\s*AGGREGATE",
"pg_alternative": "用 MATERIALIZED VIEW 替代聚合索引,定期 REFRESH",
"severity": "high",
},
{
"name": "UNSIGNED 整数 (MySQL)",
"regex": r"\bUNSIGNED\b",
"pg_alternative": "PostgreSQL 无 UNSIGNED,用 CHECK 约束: CHECK (col >= 0)",
"severity": "medium",
},
{
"name": "AUTO_INCREMENT (MySQL)",
"regex": r"\bAUTO_INCREMENT\b",
"pg_alternative": "用 SERIAL 或 GENERATED ALWAYS AS IDENTITY",
"severity": "low",
},
{
"name": "GROUP_CONCAT (MySQL)",
"regex": r"\bGROUP_CONCAT\b",
"pg_alternative": "用 string_agg(col, delimiter)",
"severity": "medium",
},
{
"name": "IF() 函数 (MySQL)",
"regex": r"\bIF\s*\(",
"pg_alternative": "用 CASE WHEN ... THEN ... ELSE ... END",
"severity": "low",
},
]
# ---------- 扫描逻辑 ----------
def fetch_schema_sql(config: dict) -> list[str]:
"""从源库导出 CREATE 语句。这里用文件模拟;生产环境可用 SHOW CREATE TABLE。"""
# 实际生产中,用 mysql-connector-python 执行:
# cursor.execute("SHOW TABLES")
# for table in cursor.fetchall():
# cursor.execute(f"SHOW CREATE TABLE {table[0]}")
# sql_list.append(cursor.fetchone()[1])
#
# 此处为可运行示例,从本地 SQL 文件读取:
sql_file = Path("schema_dump.sql")
if sql_file.exists():
return sql_file.read_text().split(";")
# 无文件时返回模拟数据
return [
"CREATE REFERENCE TABLE ref_orders (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY)",
"CREATE TABLE events (name VARCHAR(255), tags GROUP_CONCAT)",
"SELECT IF(status='active', 1, 0) FROM users",
]
def assess_compatibility(sql_list: list[str]) -> dict:
"""逐条扫描 SQL,匹配专有模式,生成报告。"""
findings = []
for sql in sql_list:
sql_stripped = sql.strip()
if not sql_stripped:
continue
for pattern in PATTERNS:
if re.search(pattern["regex"], sql_stripped, re.IGNORECASE):
findings.append({
"pattern": pattern["name"],
"severity": pattern["severity"],
"pg_alternative": pattern["pg_alternative"],
"sample_sql": sql_stripped[:120],
})
# 汇总
high = sum(1 for f in findings if f["severity"] == "high")
medium = sum(1 for f in findings if f["severity"] == "medium")
low = sum(1 for f in findings if f["severity"] == "low")
return {
"total_issues": len(findings),
"high": high,
"medium": medium,
"low": low,
"recommendation": (
"高风险项 > 0:建议先做专项 PoC,验证替代方案性能"
if high > 0 else
"中风险项为主:可分批迁移,逐模块替换语法"
if medium > 0 else
"仅低风险项:可直接用 pg_dump 迁移后微调"
),
"findings": findings,
}
def main():
sql_list = fetch_schema_sql(SOURCE_CONFIG)
report = assess_compatibility(sql_list)
print(json.dumps(report, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
运行步骤:
- 把源库 schema 导出为
schema_dump.sql(或修改fetch_schema_sql直连源库)。 python migration_assess.py,输出 JSON 报告。- 根据严重程度排序,先处理
high级别项——这正是 MemSQL → PG 迁移中REFERENCE TABLE和聚合索引这类没有直接等价物的部分。
改造方向:
- 替换
PATTERNS列表,加入你源库的专有语法(如 Oracle 的DECODE、SQL Server 的TOP)。 - 在
fetch_schema_sql中接入真实数据库驱动,自动拉取全部 DDL。 - 输出 Markdown 报告而非 JSON,直接贴进迁移文档。
迁移决策清单
听完播客、跑完评估脚本后,实际决策可以按这个清单推进:
| 步骤 | 关键问题 | 验证手段 |
|---|---|---|
| 1. 语法兼容性扫描 | 有多少专有语法无 PG 等价物? | 上面的脚本 + 人工复核存储过程 |
| 2. 分布式拓扑映射 | 源库的分片/分区策略在 PG 侧如何实现? | Citus / Patroni + 测试集群 PoC |
| 3. 性能基线对标 | 核心查询在 PG 上的延迟是否可接受? | pg_stat_statements + 源库 APM 对比 |
| 4. 数据迁移工具选择 | 全量 + 增量如何衔接? | pg_dump 全量 → logical replication 增量 |
| 5. 回退方案 | 迁移失败能否快速切回源库? | 双写窗口 + 流量灰度切换 |
社区活动提供了人脉和经验交流的渠道,但迁移的硬核问题最终要靠脚本、PoC 和数据说话。如果你正在做类似迁移,柏林和爱丁堡 Meetup 的演讲者名单里有几位在高可用和分布式方面有实战经验,值得在下次活动时当面请教。