触发器、序列与索引

共 98 题
📑 题目列表 98 题
#
★★★

1. INSTEAD OF 触发器在视图可更新性中的作用,如何通过触发器实现复杂视图的 INSERT/UPDATE/DELETE?

INSTEAD OF 触发器在视图可更新性中的作用是什么?如何通过它实现复杂视图的 INSERT/UPDATE/DELETE?

  • 不可更新视图的写入问题
  • INSTEAD OF 触发器的替换语义
  • 多表拆分写入的实现

作用:标准可更新视图只支持"单表简单查询",含 JOIN/聚合/UNION 的复杂视图不可直接 INSERT/UPDATE/DELETE(报错);INSTEAD OF 触发器把"视图上的 DML"整体接管——数据库不再尝试改写基表,而是执行触发器函数,由函数自行决定如何落库(可写多张基表、可校验、可转换),从而让任意复杂视图"可写"。INSTEAD OF 语义:触发器的动作"替代"原 DML(不是额外执行),FOR EACH ROW,函数内通过 NEW(新行)/OLD(旧行)访问视图逻辑行,按业务拆分写入基表。

实现示例:视图 v_emp_detail = 员工表 JOIN 部门表(展示部门名),INSERT INTO v_emp_detail 时 INSTEAD OF INSERT 触发器把 NEW 拆解——按部门名找到/创建部门(插入 department),再用返回的 dept_id 插入 employee;UPDATE 触发器比较 NEW/OLD 决定更新哪些表(员工字段更新 employee、部门名变化更新 department);DELETE 触发器按 OLD.id 删除 employee(可级联)。要点:其一,触发器必须处理"视图列与基表列的映射"(视图中的派生列(如部门名)可能不可写或需转换);其二,INSTEAD OF 触发器在视图上定义(PG/Oracle 支持;MySQL 不支持视图上的 INSTEAD OF——MySQL 对不可更新视图的写入直接报错,只能用"可更新视图"或应用层);其三,RETURN NEW/NULL 控制(PG 中 RETURN NULL 表示跳过该行);其四,事务——触发器函数与调用语句同事务(失败回滚);其五,与约束/审计结合——在拆分写入中可同时完成校验与日志。注意 INSTEAD OF 触发器也可以用于普通表(PG/Oracle 中用于视图为主,SQL Server 的 INSTEAD OF 触发器同时支持表与视图,是 SQL Server 表上替代 DML 的常用手段)。

答题先讲复杂视图不可直接写的问题与 INSTEAD OF 的"替换执行"语义,再以 JOIN 视图为例演示 INSERT/UPDATE/DELETE 的拆分实现(找部门、落员工、对比 NEW/OLD),最后列要点(列映射、RETURN 控制、MySQL 不支持、同事务)。

CREATE FUNCTION ins_emp_detail() RETURNS trigger AS $$
DECLARE did INT;
BEGIN
  INSERT INTO department (name) VALUES (NEW.dept_name)
    ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
    RETURNING id INTO did;
  INSERT INTO employee (id, name, dept_id) VALUES (NEW.id, NEW.name, did);
  RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_ins INSTEAD OF INSERT ON v_emp_detail
  FOR EACH ROW EXECUTE FUNCTION ins_emp_detail();
#
★★★

2. PostgreSQL 中 AFTER 触发器与 BEFORE 触发器的执行顺序与可见性,BEFORE 可修改 NEW、AFTER 只能读取。

PostgreSQL 中 AFTER 触发器与 BEFORE 触发器的执行顺序与可见性是什么?为什么 BEFORE 可修改 NEW 而 AFTER 只能读取?

  • 触发器执行顺序(BEFORE→约束→AFTER)
  • NEW/OLD 的可变性差异
  • 语句级触发器的时机

执行顺序(行级):BEFORE 行触发器(可修改 NEW)→ 实际写入(NOT NULL、CHECK、唯一/主键在写入过程中检查,违反即报错;非延迟外键在语句结束时统一检查)→ AFTER 行触发器(语句结束时触发、读取最终行,先于语句级 AFTER 触发器)→ 语句级 AFTER 触发器(语句最末);即 BEFORE 先于约束与写入、AFTER 后于写入。可见性差异:BEFORE 触发器中 NEW 是"尚未落库的新行"——函数可修改 NEW 的字段(清洗、补默认、改值),最终写入的是修改后的行,因此"约束检查看到修改后的值";AFTER 触发器中行已写入,NEW/OLD 是已落库行的快照——修改 NEW 不再影响已写入的数据(改动无效),AFTER 只能"读取"(审计、日志、级联联动)。BEFORE 中返回 NULL 阻止本次行操作(该行不写入),AFTER 中 RETURN 值无意义(行已写入,返回 NULL 也不阻止)。

其他顺序细节:同一表多触发器按名称字母序执行(PG;MySQL 按创建顺序);INSTEAD OF 触发器替代语句执行;语句级 BEFORE 触发器在语句开始、语句级 AFTER 在语句结束(各执行一次,不管多少行);TRUNCATE 只有语句级触发器。工程含义:需要"写入前干预"(清洗/校验/默认值)用 BEFORE 行触发器;需要"写入后响应"(审计、缓存失效、关联更新)用 AFTER 行触发器;AFTER 中读 NEW/OLD 做对比(NEW.status <> OLD.status)实现变更检测;注意 BEFORE 中做重的查询(如查其他表校验)会放大每次写入的延迟(触发器同步执行)。

答题先给完整执行链(BEFORE→写入(含行级约束检查)→AFTER→语句级 AFTER),再解释可见性差异(BEFORE 的 NEW 可改且约束看到修改后值、AFTER 的 NEW/OLD 只读)与 RETURN NULL 的语义差异,最后给"写入前干预用 BEFORE、写入后响应用 AFTER"的选择与多触发器顺序细节。

CREATE FUNCTION before_f() RETURNS trigger AS $$
BEGIN
  NEW.updated_at := now();      -- 修改 NEW,最终写入修改后的值
  IF NEW.amount < 0 THEN RETURN NULL; END IF;  -- 阻止写入
  RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg BEFORE INSERT OR UPDATE ON t FOR EACH ROW EXECUTE FUNCTION before_f();
#
★★★

3. 事件触发器(Event Trigger)在 PostgreSQL 中的 ddl_command_start、ddl_command_end、sql_drop 事件的应用?

事件触发器(Event Trigger)在 PostgreSQL 中的 ddl_command_start、ddl_command_end、sql_drop 事件有哪些应用?

  • 事件触发器与行触发器的区别
  • 三个事件时机的语义
  • 应用场景(DDL 审计、防护、变更追踪)

事件触发器(Event Trigger)作用于"DDL 事件"而非行 DML(行触发器挂在表上、事件触发器挂在"数据库"级,函数返回 event_trigger 类型);事件:ddl_command_start——DDL 语句开始执行前(可阻止 DDL:RAISE EXCEPTION 拒绝)、ddl_command_end——DDL 成功后(做记录/变更追踪)、sql_drop——DROP 语句中对象被删除时(记录被删对象:通过 pg_event_trigger_dropped_objects() 获取列表,配合 ddl_command_end 顺序执行、sql_drop 先于 ddl_command_end)。触发范围:CREATE/ALTER/DROP TABLE、INDEX、VIEW、FUNCTION、SCHEMA 等(TRUNCATE 不含,DDL 事件过滤器按命令标签过滤)。

应用场景:其一,DDL 审计——记录谁在何时执行了什么 DDL(event 触发器函数内读 current_user 与 tg_tag 写入审计表),满足合规要求;其二,DDL 防护——ddl_command_start 中按白名单阻止危险 DDL(禁止生产库 DROP TABLE/ALTER 特定表:RAISE EXCEPTION),实现"数据库层熔断";其三,变更追踪/同步——ddl_command_end 记录结构变更供迁移工具与监控消费(配合 pg_event_trigger_ddl_commands() 获取命令详情);其四,sql_drop——被删对象的归档(删表前导出其 DDL 到审计库)。注意:事件触发器要求超级用户创建、不能递归触发(事件触发器本身的 DDL 不会再触发自身?PG 会跳过事件触发器造成的递归)、sql_drop 在对象删除时触发且只能读元数据(对象可能已部分删除);普通行触发器无法感知 DDL,事件触发器补充了"结构变更的钩子"。对比:MySQL 无事件触发器(DDL 审计用 general log/审计插件),SQL Server 的 DDL 触发器(DATABASE 级)功能类似。

答题先定义事件触发器(数据库级、DDL 事件、与行触发器区别),再讲三个事件时机(start 可阻止、end 记录、drop 归档被删对象)与配套函数(pg_event_trigger_dropped_objects),最后列三类应用(审计、防护熔断、变更追踪)与权限/递归注意点。

CREATE FUNCTION block_drop() RETURNS event_trigger AS $$
BEGIN
  IF tg_tag = 'DROP TABLE' THEN
    RAISE EXCEPTION 'DROP TABLE is blocked on this database';
  END IF;
END $$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER evt_block_drop ON ddl_command_start
  EXECUTE FUNCTION block_drop();
-- 记录被删对象
CREATE FUNCTION audit_drop() RETURNS event_trigger AS $$
BEGIN
  INSERT INTO ddl_audit(tag, dropped)
  SELECT tg_tag, object_identity FROM pg_event_trigger_dropped_objects();
END $$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER evt_audit_drop ON sql_drop EXECUTE FUNCTION audit_drop();
#
★★★

4. 定时事件(Scheduled Event),MySQL EVENT、PostgreSQL pg_cron、SQL Server Agent Job 的对比与等价功能?

定时事件(Scheduled Event):MySQL EVENT、PostgreSQL pg_cron、SQL Server Agent Job 的对比与等价功能是什么?

  • 三库定时任务的实现
  • 创建与调度语法差异
  • 选型与外部替代(cron/工作流)

三库定时任务:MySQL EVENT——数据库内调度器(event_scheduler 需开启:SET GLOBAL event_scheduler = ON),CREATE EVENT ev ON SCHEDULE EVERY 1 DAY STARTS '2024-01-01 02:00:00' DO 语句(可执行 SQL 与存储过程,DO 单语句或 CALL 过程),支持一次性(AT)与周期(EVERY)、状态启用/禁用(ALTER EVENT ... ENABLE/DISABLE);事件随数据库实例运行(主从复制中事件默认只在主库执行?事件在每台实例独立调度,从库上需注意(MySQL 复制不复制事件,从库需单独配置,8.0 中事件不会自动在从库执行))。PostgreSQL pg_cron——扩展(需安装 pg_cron 插件并配置 shared_preload_libraries):cron.schedule('0 2 * * *', $$REFRESH MATERIALIZED VIEW mv$$)(cron 表达式 + 作业 SQL/函数调用),作业由后台工作进程调度,支持 分钟/小时/日/月/周 表达式;原生 PG 无内置定时器。SQL Server Agent Job——SQL Server Agent 服务管理:msdb 中定义 Job(多步骤、计划(Schedule)、通知),是功能最完整的调度器(步骤可 T-SQL/SSIS/PowerShell、失败重试、邮件通知、作业历史)。

对比与选型:功能完整性——SQL Server Agent > MySQL EVENT(数据库内自足)≈ pg_cron(需扩展);跨实例一致性——MySQL 事件在主从间不自动同步(需各实例配置或主库执行后靠复制 DML?事件执行的 DML 会复制,但事件定义不复制);pg_cron 的作业定义存库(cron.job)可备份;可靠性——数据库内调度随实例存活(实例挂任务不执行),外部替代:系统 cron + psql/客户端脚本(更简单可靠、可监控)、调度框架(Airflow/XXL-JOB)承担复杂编排。工程建议:简单数据库内维护任务(清理、刷新、归档)用库内调度(MySQL EVENT/pg_cron/Agent),复杂编排与监控用外部调度平台;定时任务必须记录执行日志与失败告警;注意时区与夏令时对计划的影响。

答题先逐个介绍三库实现(MySQL EVENT 语法、pg_cron 扩展、SQL Server Agent Job),再对比功能(Agent 最全)、复制行为(MySQL 事件不复制)、可靠性(库内随实例),最后给选型(简单库内任务 vs 外部编排平台)与日志告警建议。

-- MySQL EVENT
SET GLOBAL event_scheduler = ON;
CREATE EVENT ev_cleanup ON SCHEDULE EVERY 1 DAY STARTS '2024-01-01 02:00:00'
  ON COMPLETION PRESERVE DO CALL cleanup_proc();
-- pg_cron
SELECT cron.schedule('nightly-refresh', '0 2 * * *',
  $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales$$);
#
★★★

5. 触发器对复制的影响,行触发器 vs 语句触发器在主从复制中的差异?

触发器对复制的影响是什么?行触发器 vs 语句触发器在主从复制中有何差异?

  • 触发器与复制的交互模型
  • 行级日志与语句级日志
  • 双写与触发器重复执行问题

核心差异在"触发器在副本上是否重复执行":行级复制(Row-based Replication,MySQL RBR、PG 物理流复制+逻辑复制默认行级)——主库触发器产生的"数据变更"被记录为行事件(不记录触发器动作本身),从库"应用行变更"时如果从库上也有同样的触发器,触发器会再次执行(双写/重复副作用:如主库触发器写审计表,从库应用行事件时又写一次审计表);语句级复制(Statement-based Replication,如 MySQL 的 SBR)——复制的是"原始 SQL 语句",从库执行语句时若从库有触发器,触发器自然执行(行为与主库一致,主库触发器写审计、从库执行语句也写审计,两端一致);差异总结:SBR 下触发器"自然复制"(语句重放触发),RBR 下触发器"数据复制+副本触发器再执行"(需小心重复)。

具体影响:其一,MySQL——默认行级复制(binlog_format=ROW):从库若配置了相同的触发器,行变更应用时会再次触发(官方文档:RBR 下从库触发器会执行,若不想执行需从库不建触发器或用复制过滤);SBR 下语句在从库执行触发器行为一致但非确定性语句(NOW() 等)两端可能不同;其二,PostgreSQL——物理流复制中从库是"只读的完整数据":主库执行触发器产生的效果已经体现在 WAL 重放的数据中,从库不会执行触发器(从库只应用变更),无重复问题;逻辑复制(发布订阅)中,订阅端应用行变更时订阅端表的触发器默认会执行(ALTER TABLE ... ENABLE TRIGGER 与复制角色(replica identity)控制:默认订阅端触发器执行,可用 session_replication_role = replica 让触发器(非复制角色触发器)跳过,或对表禁用);其三,SQL Server——事务复制(日志读取器按行级变更复制)主库触发器产生的变更同样进入日志被复制,订阅库应用变更时触发器可能再执行(可将触发器标记 NOT FOR REPLICATION 禁用复制触发执行)。工程建议:设计"复制安全"的触发器——审计/更新缓存类触发器在副本上要么不建、要么用"复制角色跳过"(session_replication_role=replica 时触发器函数开头判断 current_setting)、要么接受双写并做幂等;发布-订阅两端触发器配置要一致并纳入文档。

答题先讲核心差异(SBR 重放语句自然触发 vs RBR 复制行事件+副本触发器再执行),再分别列 MySQL(RBR 双执行)、PG(物理复制不执行、逻辑复制默认执行可跳过)、SQL Server 的行为,最后给"复制安全触发器"设计建议(副本跳过/幂等/配置一致)。

#
★★★

6. 触发器的递归触发与无限循环风险,A 触发器更新 B 表导致 B 触发器又更新 A 如何防护?

触发器的递归触发与无限循环风险是什么?A 表触发器更新 B 表、B 表触发器又更新 A 表时如何防护?

  • 触发器的递归触发机制
  • 循环链路的产生(A→B→A)
  • 防护手段(开关、哨兵、深度限制)

递归触发:触发器函数内的 DML 会再次触发目标表的触发器,形成级联(A 表触发器 UPDATE B,B 表的触发器又 UPDATE A,A 的触发器再触发……)——若链路无终止条件,递归无限进行:PostgreSQL——默认允许递归(触发器函数内的 SQL 会再次触发其他触发器,包括同一表的同一触发器),无限递归最终报 "stack depth limit exceeded",需靠哨兵/深度计数防护,或用 session_replication_role = replica 临时禁用触发器;MySQL——InnoDB 禁止触发器修改"触发该触发器的语句正在使用的表"(报错 1442 "Can't update table ... already used by statement"),因此同一表自触发与 A→B→A 循环会直接报错而非死循环,但级联到其他表(A→B→C)会继续触发;SQL Server——默认禁止直接递归(RECURSIVE_TRIGGERS 默认 OFF,同一表的递归触发被阻止),跨表间接循环受嵌套层数上限(32 层)限制而报错。

防护手段:其一,会话级开关——SQL Server:ALTER DATABASE SET RECURSIVE_TRIGGERS OFF;PG:session_replication_role = replica 可让触发器失效;MySQL:用会话变量哨兵(IF @in_trigger = 1 THEN RETURN; SET @in_trigger = 1 ... 最后复位);其二,哨兵标志(会话变量/临时表)——触发器函数开头检查"是否已在触发链中"(会话变量或临时表记录),是则直接返回,防止重入;其三,深度限制——触发器内用会话计数器(每层 +1,超过阈值 RAISE 错误/返回);其四,设计层面避免——不让触发器相互写表(审计/联动改为应用层或队列、把级联联动改成单向、明确触发器只写"叶子表")。工程建议:触发器函数一律加"重入保护"(哨兵或深度计数);文档化触发器依赖图(谁触发谁);测试环境模拟循环场景验证终止;生产监控死锁/递归报错日志。注意:PG 中同一表触发器函数内对该表再执行 DML 会再次触发同表触发器(无默认抑制),因此 PG 也需要哨兵防护。

答题先讲递归触发机制与 A→B→A 循环链路,再按库列默认行为(MySQL 默认递归到死锁报错、SQL Server RECURSIVE_TRIGGERS 默认 OFF、PG 需哨兵),最后给防护手段(会话开关、哨兵标志、深度计数、设计单向化)与测试/监控建议。

-- MySQL:会话变量哨兵防重入
CREATE TRIGGER trg_a AFTER UPDATE ON a FOR EACH ROW
BEGIN
  IF @in_chain = 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'recursion'; END IF;
  SET @in_chain = 1;
  UPDATE b SET v = NEW.v WHERE ...;
  SET @in_chain = NULL;
END;
-- SQL Server:禁止递归
ALTER DATABASE db SET RECURSIVE_TRIGGERS OFF;
#
★★★

7. 触发器(Trigger)的分类体系,BEFORE/AFTER/INSTEAD OF、ROW/STATEMENT、INSERT/UPDATE/DELETE/TRUNCATE 的笛卡尔积组合如何声明?

触发器的分类体系(BEFORE/AFTER/INSTEAD OF、ROW/STATEMENT、INSERT/UPDATE/DELETE/TRUNCATE)如何理解?各组合如何声明?

  • 时机×级别×事件的三维分类
  • 合法组合与声明语法
  • 各库支持差异

三维分类:时机(Timing)——BEFORE(操作前)、AFTER(操作后)、INSTEAD OF(替代操作,主要用于视图);级别(Level)——行级(FOR EACH ROW,每行触发一次,可访问 NEW/OLD)、语句级(FOR EACH STATEMENT,语句触发一次,无逐行数据);事件(Event)——INSERT、UPDATE、DELETE、TRUNCATE。合法组合(笛卡尔积的可用子集):行级:BEFORE/AFTER × INSERT/UPDATE/DELETE(TRUNCATE 无行级——TRUNCATE 是语句级操作,只有语句级触发器);语句级:BEFORE/AFTER × INSERT/UPDATE/DELETE/TRUNCATE;INSTEAD OF:仅行级、仅视图(PG/Oracle 中视图的 INSTEAD OF 触发器;SQL Server 的表/视图都支持 INSTEAD OF 行级)。声明语法:CREATE TRIGGER 名 {BEFORE|AFTER|INSTEAD OF} {INSERT|UPDATE [OF 列]|DELETE|TRUNCATE} ON 表 [FOR EACH {ROW|STATEMENT}] [WHEN 条件] EXECUTE FUNCTION 函数()(PG;MySQL 语法:CREATE TRIGGER 名 {BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON 表 FOR EACH ROW 函数体——MySQL 只有行级触发器、无语句级、无 INSTEAD OF、无 TRUNCATE 事件(8.0 无 TRUNCATE 触发器);SQL Server:CREATE TRIGGER 名 ON 表 {FOR|AFTER|INSTEAD OF} {INSERT, UPDATE, DELETE} AS ...——AFTER 表 FOR、INSERTED/DELETED 伪表,无 TRUNCATE(用 DDL 触发器);Oracle:BEFORE/AFTER/INSTEAD OF + ROW/STATEMENT + INSERT/UPDATE/DELETE + TRUNCATE(语句级)全支持)。

组合的意义:行级触发器"逐行处理"(数据清洗、审计每行),语句级触发器"整批处理"(批量操作的汇总校验、性能优化——大事务逐行触发开销大);BEFORE 干预、AFTER 联动、INSTEAD OF 替代;UPDATE OF 列可指定"仅某列更新时触发"(PG/Oracle 支持)。工程建议:批量导入/大批 UPDATE 用语句级(或临时禁用行级);需要行级细节(NEW/OLD)用行级;同表多种组合可共存(BEFORE ROW + AFTER STATEMENT)。

答题先给三维分类(时机×级别×事件)与合法组合矩阵(TRUNCATE 仅语句级、INSTEAD OF 仅行级视图),再给 PG/MySQL/SQL Server/Oracle 的声明语法差异(MySQL 仅行级无 TRUNCATE、SQL Server 用 INSERTED/DELETED、Oracle 全支持),最后讲组合的选择(逐行 vs 整批)与工程建议。

-- PostgreSQL:各种组合
CREATE TRIGGER t1 BEFORE INSERT OR UPDATE OF amount ON orders
  FOR EACH ROW EXECUTE FUNCTION f1();
CREATE TRIGGER t2 AFTER DELETE ON orders FOR EACH STATEMENT EXECUTE FUNCTION f2();
CREATE TRIGGER t3 AFTER TRUNCATE ON orders FOR EACH STATEMENT EXECUTE FUNCTION f3();
-- MySQL(仅行级、无 TRUNCATE)
CREATE TRIGGER t1 AFTER UPDATE ON orders FOR EACH ROW SET @x = NEW.amount;
#
★★★

8. PostgreSQL 中 WHEN 子句(条件触发器)的语法与执行效率?

PostgreSQL 中 WHEN 子句(条件触发器)的语法与执行效率如何?

  • WHEN 条件的声明与引用(NEW/OLD)
  • 条件过滤的执行时机(触发前判定)
  • 与函数内 IF 判断的对比

语法:CREATE TRIGGER ... BEFORE/AFTER ... ON t FOR EACH ROW WHEN (NEW.status = 'ACTIVE') EXECUTE FUNCTION f();——WHEN 后是布尔表达式,可引用 NEW/OLD(行级)、PG 10+ 也支持语句级触发器的 WHEN(引用无 NEW/OLD);WHEN 表达式不能包含子查询(只能是当前行表达式)。执行时机:行级触发器的 WHEN 在"触发函数调用之前"求值——不满足条件的行"不调用函数"(BEFORE 中 WHEN 为假时行正常处理且函数完全不执行),因此 WHEN 是"过滤触发器执行"的机制;AFTER 行级触发器的 WHEN 在行确定后(写入后)求值。执行效率:WHEN 条件在"行级判定"阶段内联求值(比"每次调用函数后在函数内 IF 判断"少一次函数调用开销(函数调用含 plpgsql 解释与上下文),且让触发器函数保持"只处理需要处理的场景"——高频表上大部分行不满足条件时,WHEN 过滤收益显著;但 WHEN 表达式本身会随每行求值(复杂表达式有开销),且 AFTER 触发器仍要等行写入后判定)。

与函数内 IF 对比:WHEN 是"声明在触发器定义中的过滤条件",函数体内 IF 是"函数内部的逻辑分支"——语义等价(都可实现条件执行),差异在开销与可读性:WHEN 少一次函数调用(性能更好,适合"大部分行不触发"的场景)、定义层可见(审查触发器时直接看到条件);函数内 IF 灵活(可访问函数内状态、可做更复杂判断)。注意点:WHEN 中引用 NEW/OLD 的列需在触发事件中可用(INSERT 无 OLD、DELETE 无 NEW,引用报错);UPDATE OF 列与 WHEN 可组合(列变更 + 条件);语句级触发器 WHEN(PG 10+)在语句级判定(不能引用行值,可引用会话/时间等);触发器函数被"跳过"时事务仍正常。工程建议:能写 WHEN 就写 WHEN(声明式过滤、性能好),函数内只留真正需要函数逻辑的部分;大批量 UPDATE 中 WHEN 为假的行完全跳过函数调用,避免无谓开销。

答题先给 WHEN 语法(引用 NEW/OLD、布尔表达式、无子查询),再讲执行时机(函数调用前过滤、AFTER 在写入后判定)与效率(省函数调用、行级内联求值、大部分行不触发时收益大),最后对比函数内 IF(语义等价、开销与可读性差异)并给建议。

CREATE TRIGGER trg_log_paid AFTER UPDATE ON orders
  FOR EACH ROW WHEN (OLD.status <> 'PAID' AND NEW.status = 'PAID')
  EXECUTE FUNCTION log_payment();
-- WHEN 为假的行不调用函数,日志只记状态变为已支付的订单
#
★★★

9. 为何在 OLTP 高并发系统中应谨慎使用触发器?同步开销、调试困难、隐式逻辑如何避免?

为何在 OLTP 高并发系统中应谨慎使用触发器?同步开销、调试困难、隐式逻辑如何避免?

  • 触发器的同步执行开销
  • 隐式逻辑与可观测性
  • 替代方案与使用准则

谨慎使用的原因:其一,同步开销——触发器在"事务内、写入路径上"同步执行:每个受影响行都要调用触发器函数(行级),函数内任何查询/写入都叠加到本次事务的锁与 IO 上,批量操作(INSERT SELECT 万行)逐行触发放大延迟,高并发下写入吞吐显著下降(触发器成为隐性瓶颈);其二,隐式逻辑与不可见性——业务逻辑藏在触发器里,应用代码与 DBA 看不到(查代码看不到、EXPLAIN 看不到),排查"谁改了数据"困难(触发器写审计表才可见),且行为依赖"数据库对象状态"(触发器存在与否),迁移/灰度时容易漏;其三,调试与测试困难——触发器难以单测(需要触发真实 DML)、难以断点调试、报错栈不直观(错误定位到触发器函数需翻日志);其四,级联与递归风险、复制双写问题;其五,锁竞争——触发器内写其他表可能引入额外锁(死锁面扩大)。

避免与治理:其一,能不用就不用——同一逻辑优先用约束(CHECK/唯一/外键)、应用层事务、或显式调用(存储过程/服务方法);其二,触发器只做"小而明确"的事——审计日志(NEW/OLD 快照)、更新时间戳、简单校验,不做复杂业务(跨服务调用、重计算);其三,异步化——需要联动的大逻辑改为"触发器写队列表/CDC 事件 + 异步消费"(把同步开销移出写入路径);其四,工程治理——触发器清单纳入代码评审与文档、命名规范(表名+事件+用途)、测试覆盖(触发场景单测)、监控(触发器内耗时埋点或 pg_stat 观察);其五,批量场景禁用/语句级触发——大导入时临时禁用触发器(session_replication_role)或改用语句级触发器(只触发一次);其六,评估替代——物化视图(聚合自动维护)、CDC(Debezium 等统一变更流)可替代部分触发器职责。总结:OLTP 中触发器"能用但慎用":小逻辑可用(审计/时间戳),大逻辑必须异步化或移出数据库。

答题先列五类风险(同步写入路径开销、隐式逻辑不可见、调试测试难、级联递归与复制双写、锁竞争),再给治理清单(约束/应用层替代、小而明确、异步队列化、评审文档监控、批量禁用)与替代方案(CDC、物化视图),最后给"小逻辑可用、大逻辑异步"的结论。

#
★★★

10. CREATE TRIGGER 的基本语法示例(BEFORE INSERT 触发器)?

CREATE TRIGGER 的基本语法是什么?请给出一个 BEFORE INSERT 触发器的完整示例?

  • CREATE TRIGGER 语法骨架
  • BEFORE INSERT 行级触发器的完整示例
  • 触发器函数与触发器的关系

基本语法(PostgreSQL):CREATE [OR REPLACE] TRIGGER 触发器名 {BEFORE|AFTER|INSTEAD OF} {INSERT|UPDATE|DELETE|TRUNCATE} ON 表名 [FOR EACH {ROW|STATEMENT}] [WHEN (条件)] EXECUTE FUNCTION 函数名();——注意触发器与"触发器函数"分离:先 CREATE FUNCTION(plpgsql 等)再 CREATE TRIGGER 挂载(PG/Oracle 模式;MySQL 中触发器内联 BEGIN...END 函数体,无独立函数)。BEFORE INSERT 示例:函数在插入前清洗并填充默认值——CREATE FUNCTION norm_ins() RETURNS trigger AS $$ BEGIN NEW.email := lower(trim(NEW.email)); NEW.created_at := COALESCE(NEW.created_at, now()); RETURN NEW; END $$ LANGUAGE plpgsql; CREATE TRIGGER trg_norm BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION norm_ins();——插入 users 时每行先执行函数:改写 NEW.email(小写去空格)、填充 created_at、RETURN NEW 继续插入。

要点:其一,BEFORE 触发器函数必须 RETURN NEW(或修改后的 NEW)让操作继续、RETURN NULL 则跳过该行;其二,FOR EACH ROW 行级(默认 STATEMENT);其三,同表多触发器顺序(PG 按名称);其四,MySQL 版本:CREATE TRIGGER trg BEFORE INSERT ON users FOR EACH ROW SET NEW.email = LOWER(TRIM(NEW.email));(NEW 直接可用,无独立函数);其五,权限——创建需表 owner(PG)或 TRIGGER 权限(MySQL 需 TRIGGER 权限);其六,查看/删除——\d 表名 或 information_schema.triggers 查询、DROP TRIGGER 名 ON 表。应用场景:写入前数据清洗、默认值/审计字段填充、简单校验(不满足 RAISE EXCEPTION 阻断)。

答题先给 PG 的语法骨架(时机/事件/级别/WHEN/EXECUTE FUNCTION)与"函数+触发器分离"模型,再给完整 BEFORE INSERT 示例(清洗+默认值+RETURN NEW)逐行解释,最后补 MySQL 内联写法、权限与查看删除要点。

-- PostgreSQL:先建函数再挂触发器
CREATE FUNCTION norm_ins() RETURNS trigger AS $$
BEGIN
  NEW.email := lower(trim(NEW.email));
  NEW.created_at := COALESCE(NEW.created_at, now());
  RETURN NEW;   -- 返回修改后的行继续插入
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_norm BEFORE INSERT ON users
  FOR EACH ROW EXECUTE FUNCTION norm_ins();
-- MySQL:内联函数体
CREATE TRIGGER trg_norm BEFORE INSERT ON users FOR EACH ROW
  SET NEW.email = LOWER(TRIM(NEW.email));
#
★★★

11. MySQL EVENT 调度器如何启用?SET GLOBAL event_scheduler = ON?

MySQL EVENT 调度器如何启用?SET GLOBAL event_scheduler = ON 的作用与持久化方式是什么?

  • event_scheduler 参数与启用
  • 事件创建与调度
  • 持久化与状态管理

启用方式:MySQL 8.0 中 event_scheduler 默认 ON(官方文档 "ON is the default event_scheduler value";5.7 及更早默认 OFF 需手动开启):运行时 SET GLOBAL event_scheduler = ON(立即生效,对所有会话,但重启失效);持久化——写配置文件 my.cnf [mysqld] event_scheduler=ON(重启后保持),或 8.0 中 SET PERSIST event_scheduler = ON(写入 mysqld-auto.cnf,重启保留,SET PERSIST 是 8.0 持久化 GLOBAL 的标准方式)。查看状态:SHOW VARIABLES LIKE 'event_scheduler'(返回 ON/OFF/DISABLED——DISABLED 表示启动时禁用且无法运行时开启(需改配置重启));SHOW PROCESSLIST 可见 event_scheduler 线程(后台线程,Event Scheduler 用户)。

事件管理:启用后 CREATE EVENT ev ON SCHEDULE EVERY 1 HOUR DO ...(创建周期/一次性任务);SHOW EVENTS(查看本库事件)、SHOW CREATE EVENT;ALTER EVENT ev DISABLE/ENABLE/ON SCHEDULE 修改;DROP EVENT 删除;事件可指定数据库(CREATE EVENT db.ev)、DO 可执行 SQL 或 CALL 存储过程;ON COMPLETION PRESERVE/NOT PRESERVE 控制一次性事件执行后是否保留。注意点:其一,事件执行是"独立的连接/会话"(事件中的语句不受客户端事务控制);其二,事件权限(EVENT 权限创建、EVENT ADMIN 或 SUPER 管理全局);其三,复制——事件不会自动复制到从库(从库需单独创建,或仅在主库执行让 DML 经 binlog 复制;8.0 中事件可在复制拓扑中独立配置);其四,可靠性——事件由实例进程调度(实例宕机期间不执行、重启后按计划继续,错过的计划不补执行(除非设计);监控事件执行历史(mysql.event 表/performance_schema);其五,时区——事件按事件定义的时区(event time zone)调度)。

答题先讲启用三方式(SET GLOBAL 即时生效、my.cnf 持久、8.0 SET PERSIST)与 DISABLED 状态的含义,再讲事件生命周期管理(CREATE/ALTER/SHOW/DROP、调度语法、ON COMPLETION),最后列注意点(独立连接、权限、复制不自动同步、宕机错过不补、时区)。

SET GLOBAL event_scheduler = ON;          -- 立即启用(重启失效)
SET PERSIST event_scheduler = ON;         -- 8.0:写入 mysqld-auto.cnf 持久化
CREATE EVENT db.cleanup ON SCHEDULE EVERY 1 DAY
  STARTS '2024-01-01 03:00:00' ON COMPLETION PRESERVE
  DO DELETE FROM logs WHERE created_at < NOW() - INTERVAL 90 DAY;
SHOW EVENTS FROM db;
ALTER EVENT db.cleanup DISABLE;
#
★★★

12. MySQL 中如何创建 AFTER UPDATE 触发器?

MySQL 中如何创建 AFTER UPDATE 触发器?语法与示例是什么?

  • MySQL 触发器语法(行级、内联体)
  • AFTER UPDATE 与 NEW/OLD
  • 使用场景(审计)

语法:CREATE TRIGGER 触发器名 AFTER UPDATE ON 表名 FOR EACH ROW 函数体(BEGIN...END 复合语句或单语句);MySQL 触发器无独立触发器函数(函数体直接写在触发器里,支持 BEGIN...END 声明局部变量与流程控制,分隔符需用 DELIMITER 处理)。示例(审计):DELIMITER $$ CREATE TRIGGER trg_audit AFTER UPDATE ON users FOR EACH ROW BEGIN INSERT INTO users_audit(user_id, old_email, new_email, changed_at) VALUES (OLD.id, OLD.email, NEW.email, NOW()); END $$ DELIMITER ;——每次 UPDATE users 后,把旧值(OLD.email)与新值(NEW.email)写入审计表(行级触发器逐行执行:UPDATE 多行则每行插入一条审计)。

要点:其一,NEW/OLD——UPDATE 中两者都有(OLD 更新前、NEW 更新后),INSERT 只有 NEW、DELETE 只有 OLD;其二,AFTER 时机——行写入成功后执行(可以读 NEW/OLD 但改 NEW 无效);其三,权限——创建需 TRIGGER 权限;其四,同表多触发器——同一时机事件可多个(按创建顺序执行);其五,错误处理——触发器内语句失败使整个 DML 失败回滚(AFTER 中 SIGNAL 可主动阻断);其六,只能行级(MySQL 无语句级触发器、无 TRUNCATE 触发器);其七,信息查询——SHOW TRIGGERS 或 information_schema.triggers;删除 DROP TRIGGER trg_audit(无需 ON 表,MySQL 中 DROP TRIGGER 表名.触发器名)。使用场景:审计日志(谁改了什么)、缓存失效标记、汇总表维护(统计列更新)、更新时间戳(也可用 BEFORE 改 NEW)。注意 AFTER 中访问其他表注意锁与死锁(触发器内 UPDATE 其他表会持有额外锁)。

答题先给 MySQL 触发器语法(内联函数体、FOR EACH ROW、DELIMITER),再给 AFTER UPDATE 审计示例(OLD/NEW 写入审计表)逐行解释,最后列要点(NEW/OLD 可用性、TRIGGER 权限、只能行级、SIGNAL 阻断、查询/删除语法)与使用场景。

DELIMITER $$
CREATE TRIGGER trg_audit AFTER UPDATE ON users FOR EACH ROW
BEGIN
  INSERT INTO users_audit (user_id, old_email, new_email, changed_at)
  VALUES (OLD.id, OLD.email, NEW.email, NOW());
END $$
DELIMITER ;
SHOW TRIGGERS LIKE 'users';
DROP TRIGGER trg_audit;
#
★★★

13. PostgreSQL 中 CONSTRAINT TRIGGER 与普通 TRIGGER 的区别?

PostgreSQL 中 CONSTRAINT TRIGGER 与普通 TRIGGER 的区别是什么?

  • 约束触发器的定义与时机
  • 延迟(DEFERRABLE)能力
  • 与约束的配合

CONSTRAINT TRIGGER(约束触发器)是"实现约束语义的触发器":CREATE CONSTRAINT TRIGGER 名 AFTER ... ON t DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION f()——与普通触发器(CREATE TRIGGER)的核心区别:其一,可延迟性(DEFERRABLE)——约束触发器支持 DEFERRABLE/INITIALLY DEFERRED/IMMEDIATE 与 SET CONSTRAINTS 切换(普通触发器不可延迟,随语句立即执行);其二,检查时机——约束触发器默认在"事务提交时"检查(INITIALLY DEFERRED),或语句结束时(INITIALLY IMMEDIATE),适合"整批操作完成后统一校验"(如多行更新后校验合计);其三,实现约束语义——约束触发器专门用于"模拟/实现约束"(跨行/跨表校验:更新后检查不变式),与 CHECK/外键等约束同属完整性机制;其四,行为细节——约束触发器只支持 AFTER(不可 BEFORE)、只能是"约束"用途(不能随意改 NEW)、触发条件与约束一致(违反时语句/事务失败)、错误信息语义为约束违反;其五,pg_constraint 中可记录(contype='t' 约束触发器记录在 pg_trigger 的 tgisinternal?约束触发器在 pg_trigger 中标记 tgisconstraint)。查询区分:pg_trigger 中 tgisconstraint 字段(true 为约束触发器)、psql \d 显示约束触发器。

典型应用:跨行/跨表不变式的延迟校验(如"库存总和不低于 0"在事务提交时检查)、循环引用场景的延迟外键替代、批量导入后统一校验。注意:约束触发器与普通触发器可共存(顺序:普通 BEFORE → 行写入 → 普通 AFTER → 约束触发器(延迟则在提交));约束触发器函数抛错使事务失败(如同约束违反);PG 的约束触发器在 PG 9.1+ 支持 DEFERRABLE 设置。对比:MySQL 无约束触发器概念(用普通触发器+信号模拟);SQL Server 无直接对应(用 INSTEAD OF/AFTER + 事务校验)。

答题先定义约束触发器(实现约束语义、AFTER、可延迟),再列与普通触发器的四点区别(DEFERRABLE 延迟能力、提交/语句检查时机、只 AFTER、pg_trigger 标记),最后给典型应用(跨行校验、循环依赖、批量导入)与查询区分方法。

CREATE CONSTRAINT TRIGGER trg_check_total
AFTER INSERT OR UPDATE OR DELETE ON ledger
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION check_total();
-- 事务内可临时改为立即检查
SET CONSTRAINTS trg_check_total IMMEDIATE;
#
★★★

14. PostgreSQL 中 NEW 与 OLD 关键字的语义?

PostgreSQL 中触发器函数的 NEW 与 OLD 关键字语义是什么?各自的可用场景与限制是什么?

  • NEW/OLD 代表的行版本
  • 各事件的可用性(INSERT/UPDATE/DELETE)
  • 可变性与 RETURN 语义

语义:NEW 与 OLD 是触发器函数内的特殊"记录(record)"变量,代表"受触发行"的两个版本:OLD——操作前的行快照(UPDATE/DELETE 中有效);NEW——操作后的行(INSERT/UPDATE 中有效)。各事件可用性:INSERT——只有 NEW(新插入行);UPDATE——NEW(新值)与 OLD(旧值)都有(可对比变化);DELETE——只有 OLD(被删行);TRUNCATE——无 NEW/OLD(语句级)。字段访问:NEW.column / OLD.column(按列名取字段值,记录类型可用 %ROWTYPE 或字段引用)。

可变性与 RETURN 语义:BEFORE 触发器中 NEW 可修改(修改最终写入值,约束检查看到修改后的值),OLD 只读;AFTER 触发器中两者都只读(行已落库);函数 RETURN NEW(或修改后的 NEW)继续操作、RETURN OLD(DELETE 中)继续删除、RETURN NULL 跳过该行(BEFORE 中阻止操作;AFTER 中返回 NULL 只是"不处理"不影响已完成的写入);INSTEAD OF 触发器中 RETURN NEW 表示"已处理"(不返回 NULL 阻止)。使用场景:OLD/NEW 对比实现变更检测(NEW.status <> OLD.status)、审计(把 OLD/NEW 序列化入审计表)、BEFORE 中基于 NEW 清洗与默认值、DELETE 归档(OLD 全行入库)。注意:UPDATE 某列未改变时 NEW 与 OLD 该列相同;记录类型可整体赋值(NEW := NEW 无意义;可用 row_to_json(NEW) 序列化);MySQL 中 NEW/OLD 语义相同(无 RETURN 概念,直接用 SET NEW.col);Oracle 中 :NEW/:OLD 带冒号前缀(SQL 语句中)与 NEW/OLD(PL/SQL 触发器体内)。

答题先定义 NEW/OLD(操作前后行版本)与各事件的可用矩阵(INSERT 仅 NEW、UPDATE 双有、DELETE 仅 OLD、TRUNCATE 无),再讲可变性(BEFORE 可改 NEW、AFTER 只读)与 RETURN NEW/OLD/NULL 语义,最后列典型应用与各库差异(MySQL 无 RETURN、Oracle 冒号前缀)。

CREATE FUNCTION detect_change() RETURNS trigger AS $$
BEGIN
  IF TG_OP = 'UPDATE' AND NEW.status <> OLD.status THEN
    INSERT INTO status_log (id, old_s, new_s, at)
    VALUES (NEW.id, OLD.status, NEW.status, now());
  END IF;
  IF TG_OP = 'DELETE' THEN
    INSERT INTO archive SELECT (OLD).*;   -- 归档旧行
  END IF;
  RETURN COALESCE(NEW, OLD);  -- UPDATE/INSERT 返回 NEW,DELETE 返回 OLD
END $$ LANGUAGE plpgsql;
#
★★★

15. PostgreSQL 中触发器函数的 LANGUAGE 必须是 plpgsql 吗?

PostgreSQL 中触发器函数的 LANGUAGE 必须是 plpgsql 吗?其他语言如何实现触发器函数?

  • 触发器函数的语言要求
  • 各语言(SQL、C、plpython 等)的写法
  • 返回值类型 trigger 的要求

不必须。触发器函数可以是任意受支持的函数语言——LANGUAGE 可以是 plpgsql(最常见)、SQL、C(内建)、plpython/plperl/plv8 等过程语言扩展;唯一硬性要求:函数"返回类型必须是 trigger"(CREATE FUNCTION f() RETURNS trigger ...),且触发器函数不能有参数(参数用 TG_ARGV 传递)。各语言写法:LANGUAGE SQL——函数体是单个 SQL 语句(RETURN NEW 需要能返回记录:SQL 函数中触发器函数语法受限,PG 8.4+ 支持 RETURN NEW?SQL 语言触发器函数只支持"单语句且返回 trigger"(如 SELECT NEW,简单场景可用,复杂逻辑不便);LANGUAGE C——内建触发器函数(C 扩展,性能最好、写 plpgsql 无法表达的),常用内建 trigger 函数(suppress_redundant_updates_trigger、tsvector_update_trigger 等是 C 写的);LANGUAGE plpython/plperl——Python/Perl 过程语言(可访问 TD['new']/TD['old'] 等字典),灵活但引入额外依赖与性能开销)。

实践建议:默认 plpgsql(功能全、性能可接受、文档丰富);简单触发器可用 LANGUAGE SQL(如 RETURN NEW 的轻量清洗);性能极端场景用 C;多语言环境用 plpython 等需评估安全(过程语言扩展有超级用户安装要求与权限风险)。注意点:无论语言,函数语义一致——RETURN trigger 类型的行记录(NEW/OLD 由框架注入)、BEFORE 返回 NULL 跳过等规则对所有语言通用;LANGUAGE SQL 触发器函数体内不能写多个语句(单语句限制);查看触发器函数语言:\df+ 或 pg_proc.prolang。结论:语言可任选,关键是"RETURNS trigger + 无参数 + 按各语言访问 NEW/OLD"。

答题先答"不必须"(任意语言),再讲硬性要求(RETURNS trigger、无参数、TG_ARGV 传参)与各语言特点(plpgsql 默认、SQL 单语句受限、C 内建函数、plpython 的 TD 变量),最后给选型建议与语言无关的通用语义。

-- plpgsql(默认推荐)
CREATE FUNCTION f1() RETURNS trigger LANGUAGE plpgsql AS $$ ... $$;
-- LANGUAGE SQL(简单场景)
CREATE FUNCTION f2() RETURNS trigger LANGUAGE sql AS $$ SELECT NEW $$;
-- C 内建触发器函数(无需自写)
CREATE TRIGGER trg AFTER UPDATE ON t FOR EACH ROW
  EXECUTE FUNCTION suppress_redundant_updates_trigger();
#
★★★

16. 一个 BEFORE INSERT 触发器能否阻止 INSERT 操作?

一个 BEFORE INSERT 触发器能否阻止 INSERT 操作?如何实现与验证?

  • RETURN NULL 的阻止语义
  • RAISE EXCEPTION 的阻断
  • 阻止后的行为(不报错 vs 报错)

可以,两种方式:其一,静默阻止——BEFORE INSERT 行级触发器函数 RETURN NULL:该行"不被插入"(跳过),不报错、语句继续处理其他行(INSERT 多行时其余行正常插入),事务不受影响;这是"条件过滤插入"的机制(如 INSERT 时触发器判断数据非法直接丢弃)。其二,显式拒绝——触发器函数内 RAISE EXCEPTION 'msg'(或 MySQL 中 SIGNAL SQLSTATE '45000'):中止整个语句/事务并报错(调用方收到异常),用于"业务校验失败必须让调用者知道"(如唯一性业务规则、数据合法性)。两者差异:RETURN NULL 是"默默跳过该行"(审计需自行记录跳过原因),RAISE EXCEPTION 是"强制失败"(调用方必须处理)。

实现细节:BEFORE 触发器中 RETURN NULL 只影响"当前行"(行级触发器逐行执行,返回 NULL 即该行被丢弃,其他行继续);语句级 BEFORE 触发器返回 NULL 会阻止整个语句?PG 中语句级触发器 RETURN NULL 无阻止语义(语句级函数返回值被忽略);AFTER 触发器返回 NULL 不影响已完成的插入。验证:插入后 SELECT 该行不存在(RETURN NULL)或捕获异常(RAISE);也可在触发器函数内用 TG_OP/TG_WHEN 组合做条件阻止(WHEN (条件) + RETURN NULL)。MySQL 差异:MySQL 的 BEFORE 触发器无 RETURN 机制——"阻止插入"只能通过 SIGNAL 抛错(或让语句失败),无法静默跳过;Oracle 中 RETURN NULL 同 PG(阻止该行)。工程场景:RETURN NULL 用于"忽略非法行"(日志表过滤)、RAISE 用于"必须拒绝"(强校验);注意 RETURN NULL 的静默性在生产中易掩盖问题,建议同时记录(写日志表或 NOTICE)。

答题先答"可以",分两种方式(RETURN NULL 静默跳过该行不报错 vs RAISE EXCEPTION 显式拒绝中止语句)并对比适用场景,再讲行级/语句级与 AFTER 的差异细节、MySQL 无 RETURN(用 SIGNAL),最后给工程建议(静默跳过需记录原因)。

-- 方式一:RETURN NULL 静默跳过
CREATE FUNCTION block_spam() RETURNS trigger AS $$
BEGIN
  IF NEW.email LIKE '%@spam.com' THEN RETURN NULL; END IF;  -- 该行不插入
  RETURN NEW;
END $$ LANGUAGE plpgsql;
-- 方式二:RAISE EXCEPTION 显式拒绝
CREATE FUNCTION validate_age() RETURNS trigger AS $$
BEGIN
  IF NEW.age < 0 OR NEW.age > 150 THEN
    RAISE EXCEPTION 'invalid age %', NEW.age;
  END IF;
  RETURN NEW;
END $$ LANGUAGE plpgsql;
#
★★★

17. 如何在 INFORMATION_SCHEMA 中查询所有触发器?

如何在 INFORMATION_SCHEMA 中查询所有触发器?各数据库的差异是什么?

  • information_schema.triggers 的结构
  • 各库查询与字段差异
  • 原生目录查询(pg_trigger)

标准视图:INFORMATION_SCHEMA.TRIGGERS——每行一个触发器,关键字段:TRIGGER_CATALOG/SCHEMA(库与 schema)、TRIGGER_NAME、EVENT_MANIPULATION(INSERT/UPDATE/DELETE)、EVENT_OBJECT_SCHEMA/TABLE(触发表)、ACTION_ORIENTATION(ROW/STATEMENT)、ACTION_TIMING(BEFORE/AFTER)、ACTION_STATEMENT(函数体文本,MySQL 中是触发器体、PG 中是函数名引用?PG 的 ACTION_STATEMENT 为 null(函数分离)、MySQL 为函数体)。查询:SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING FROM information_schema.triggers WHERE TRIGGER_SCHEMA = 'app';按表过滤:WHERE EVENT_OBJECT_TABLE = 'orders';按事件过滤:WHERE EVENT_MANIPULATION = 'UPDATE'。

各库差异:MySQL——information_schema.triggers 完整(含 ACTION_STATEMENT 全文、DEFINER、SQL_MODE),也可 SHOW TRIGGERS [FROM db] [LIKE '表'](更快捷,但只显示当前库且字段有限);PostgreSQL——information_schema.triggers 字段较全但 ACTION_STATEMENT 为空(触发器与函数分离),更常用 pg_trigger JOIN pg_class/pg_proc 查询(tgname、tgtype 位掩码(1=ROW、2=BEFORE、4=INSERT 等位组合)、tgfoid 函数、tgisinternal);SQL Server——information_schema.triggers 存在(但触发器与表关联用 sys.triggers/sys.trigger_events 更全),OBJECT_DEFINITION 取文本;Oracle——ALL_TRIGGERS/USER_TRIGGERS(无 information_schema.triggers)。工程实践:跨库审计用 information_schema.triggers 的公共字段(名称/表/时机/事件);需要函数体(PG)查 pg_trigger JOIN pg_proc 的 prosrc 或 \d+ 表;MySQL 用 SHOW TRIGGERS 快速浏览。注意 information_schema 只显示"当前用户有权限"的触发器。

答题先讲标准视图的字段(名称/事件/表/时机/级别/函数体)与三类典型查询(全部、按表、按事件),再列各库差异(MySQL 含函数体与 SHOW TRIGGERS、PG 用 pg_trigger 位掩码、SQL Server sys.triggers、Oracle ALL_TRIGGERS),最后给跨库审计与 PG 取函数体的实践。

SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING
FROM information_schema.triggers
WHERE TRIGGER_SCHEMA = 'app';
-- PostgreSQL 原生目录(含函数)
SELECT t.tgname, t.tgtype, p.proname
FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_proc p ON p.oid = t.tgfoid
WHERE c.relname = 'orders' AND NOT t.tgisinternal;
-- MySQL 快捷方式
SHOW TRIGGERS FROM app LIKE 'orders%';
#
★★★

18. 触发器与 CDC(Change Data Capture)的取舍?

触发器与 CDC(Change Data Capture)的取舍是什么?各自适用场景是什么?

  • 触发器做变更捕获的问题
  • CDC 的机制(binlog/WAL/日志解析)
  • 取舍维度与选型

触发器做 CDC:在表上建 AFTER 触发器把 NEW/OLD 写入审计/变更表(应用内建 CDC),优点——实现简单(纯 SQL)、与业务同事务(不丢变更)、可捕获"应用层看不到的变更";缺点——侵入业务库(每个表挂触发器)、同步开销在写入路径、只捕获"数据库内变更"(跨系统/文件变更捕获不了)、变更表膨胀需清理、复制/迁移时触发器配置复杂、无法捕获 DDL(结构变更)与 TRUNCATE(MySQL 触发器不触发 TRUNCATE)。CDC 工具(Debezium + Kafka、Maxwell、pglogical、AWS DMS、MySQL binlog 解析):机制——解析数据库事务日志(MySQL binlog、PG WAL、SQL Server CDC/LSN),以"变更流"形式输出(增删改、DDL、时间戳、before/after 镜像),应用异步消费。优点——无侵入(不写业务库)、低开销(读日志)、实时、可捕获 DDL/TRUNCATE、支持多下游(Kafka/数仓/搜索索引同步);缺点——需要日志保留策略配合(binlog 过期丢变更)、消费端需处理顺序与幂等(至少一次语义)、数据同步有延迟(毫秒-秒级)、配置运维复杂度高。

取舍维度:一致性要求——同事务强一致(审计必须随主事务回滚)用触发器;最终一致异步同步(数仓、缓存、搜索)用 CDC;侵入容忍度——不能动业务库用 CDC;变更捕获范围——需要 DDL/TRUNCATE/全量镜像用 CDC;复杂度与成本——小规模简单审计用触发器,规模化用 CDC 平台。工程结论:审计/同库联动(须同事务)用触发器;跨系统数据同步(数仓、ES、缓存、微服务事件)用 CDC(Debezium 等);两者可共存(触发器做库内强一致审计,CDC 做对外同步);注意 CDC 依赖日志格式(MySQL 需 binlog_format=ROW 且 binlog_row_image=FULL 才能拿 before 镜像)。

答题先讲触发器做变更捕获的实现与优缺点(简单同事务 vs 侵入、写路径开销、无 DDL/TRUNCATE),再讲 CDC 的机制(解析 binlog/WAL、变更流、异步消费)与优缺点(无侵入、实时、需日志保留、幂等),最后按一致性/侵入性/范围/复杂度四维给选型结论与共存建议。

#
★★★

19. 触发器与约束(CHECK)的取舍准则是什么?

触发器与约束(CHECK)的取舍准则是什么?什么时候用约束、什么时候用触发器?

  • 约束的表达边界
  • 触发器的补充能力
  • 取舍准则与最佳实践

取舍准则核心:"能声明式表达就用约束,约束表达不了才用触发器"。约束(CHECK/UNIQUE/FK/NOT NULL/DOMAIN)的边界:单行表达式(CHECK 只能引用本行列、不能跨行/跨表、不能聚合、不能用 volatile 函数)、声明式(数据库保证、优化器可推导、迁移工具识别、失败信息明确);触发器可表达的:跨行/跨表校验(库存合计、父子一致性)、复杂的动态规则(查其他表、调用函数、时间窗判断)、写入时转换与联动(清洗、审计、级联衍生)、无法用约束表达的业务规则(如"金额不能超过该客户信用额度的 1.5 倍"需查客户表)。因此:约束负责"数据完整性的基础层"(类型、非空、唯一、引用、简单范围),触发器负责"跨实体/动态/过程化的完整性"。

选择建议:其一,优先约束——简单值域(CHECK)、唯一、外键、非空用原生约束(性能好(优化器参与)、语义清晰、可 NOT VALID 渐进校验);其二,CHECK 表达不了(跨行/跨表/聚合/复杂条件)再上触发器——触发器内做校验失败 RAISE;其三,触发器内尽量"用约束兜底"(触发器负责查跨表条件、约束负责基础规则,双层防御);其四,性能——约束检查随行内联、触发器是额外函数调用(高频写入慎用);其五,可移植性——约束跨库语义较一致、触发器过程语言不可移植;其六,替代品——跨行不变式可用"物化视图 + 约束"或"延迟约束"部分替代触发器;断言未实现故触发器是跨行校验的主要手段。结论:先约束后触发器;约束是"第一道防线",触发器是"补充的完整性工具",能用约束绝不用触发器。

答题先给总准则(声明式优先),再列约束的边界(单行表达式、不可跨表聚合)与触发器的补充能力(跨行跨表、动态规则、写入转换),最后给选择清单(性能、可移植、双层防御、物化视图替代)与结论。

#
★★★

20. 序列在分布式环境下的失效,分库分表后如何保证全局唯一?雪花 ID、Leaf、UUID 的对比?

序列在分布式环境下的失效:分库分表后如何保证全局唯一?雪花 ID、Leaf、UUID 的对比?

  • 单库序列的分布式失效
  • 雪花 ID/Leaf/UUID 的原理
  • 选型对比

失效场景:自增序列(MySQL AUTO_INCREMENT、PG SERIAL)的连续性只存在于"单库单实例"——分库分表后每个分片各自维护序列,各片生成的 ID 会重复(都从 1 开始),无法作为全局唯一主键;即使集中式序列服务(号段)也有性能与单点问题。分布式全局唯一方案对比:雪花 ID(Snowflake)——64 位 = 1 位符号 + 41 位毫秒时间戳 + 10 位机器/工作节点 ID + 12 位序列号:趋势递增(时间有序)、无需中心协调(每节点独立生成)、生成快(本地计算)、可解析时间与节点(调试友好);局限——依赖时钟(时钟回拨会重复,需处理:等待/备用位)、机器 ID 需分配(<1024 节点)、趋势递增而非严格有序(跨节点乱序)。Leaf(美团)——号段模式(DB 集中分配区间:一次取 1000 个号段缓存在本地,用完再取)与雪花模式(Leaf-snowflake 带时钟校正):号段模式简单可靠、吞吐高、有中心依赖(DB 单点可做高可用)、号码连续区间可预测;雪花模式同雪花 ID 思路。UUID——128 位(v4 随机 / v7 时间有序):全局唯一无需任何协调、离线可用;v4 随机无序(索引页分裂、缓存差)、无业务含义、存储 16 字节(文本 36 字符);v7 时间有序(2022 标准)弥补无序问题。

选型对比(四维):唯一性保证——四者都全局唯一(实现不同);顺序性——雪花/Leaf 号段/ UUID v7 趋势递增 > UUID v4 随机;性能——本地生成(雪花/UUID)最高、Leaf 号段依赖 DB 区间分配(网络往返但批量摊销);可调试——雪花含时间与节点、UUID 无含义;存储——BIGINT(雪花/Leaf)8 字节最优、UUID 16 字节。结论:分布式主键主流选雪花 ID(或 Leaf-snowflake),需要离线生成选 UUID v7,简单分片场景也可用"库表前缀 + 自增"(如 分片号+序列)复合键;必须处理时钟回拨与机器 ID 管理。

答题先讲序列在分库分表后的重复问题(各片独立自增),再分别讲雪花(位结构、趋势递增、时钟回拨风险)、Leaf(号段与雪花两种模式)、UUID(v4 随机/v7 有序)的原理与优劣,最后给四维对比与主流选型结论。

-- 雪花 ID 位结构:1 符号 + 41 时间戳 + 10 机器 + 12 序列
-- 号段模式(Leaf):一次从 DB 取一段
SELECT * FROM id_alloc WHERE tag='order' FOR UPDATE;
UPDATE id_alloc SET max_id = max_id + 1000 WHERE tag='order';
-- 本地发放 1000 个号后再次申请
#
★★★

21. 序列的 currval、nextval、setval 函数调用语义与可见性规则?

PostgreSQL 中序列的 currval、nextval、setval 函数调用语义与可见性规则是什么?

  • nextval/currval/setval 的语义
  • currval 的会话级可见性
  • setval 的使用与风险

三个函数的语义:nextval(seq)——推进序列并返回新值(每次调用原子递增,返回递增后的值;首次调用从 START 开始);currval(seq)——返回"当前会话中最近一次 nextval 返回的值"(不推进序列、可重复调用);setval(seq, n [, is_called])——把序列的"当前值"设置为 n(is_called 默认 true:下一次 nextval 返回 n+1;is_called=false:下一次 nextval 返回 n),用于"对齐/修复"序列值。可见性规则:nextval 返回值不依赖会话(任何人调用都拿到全局单调递增值,且不回滚);currval 是"会话级"的——只返回本会话最近 nextval 的值:新会话未调用过 nextval 时调用 currval 报错("currval of sequence is not yet defined in this session"),其他会话调用 nextval 不影响本会话的 currval;setval 是全局立即生效(影响所有会话的后续 nextval)。

使用场景:currval——插入后立即获取刚生成的主键(INSERT ... RETURNING 是现代替代,currval 是旧式做法,注意"本会话"约束与并发下多个序列混用的坑);setval——数据迁移后把序列对齐到现有最大 id(SELECT setval('seq', (SELECT MAX(id) FROM t)))、修复误操作(把序列回拨)、重置测试环境;注意 setval 不受事务回滚影响(非事务性)、回拨可能导致主键冲突(已有值被重复生成)、并发下 setval 与 nextval 交错会打乱单调性。工程规范:取新值一律 nextval(或 DEFAULT nextval('seq') / IDENTITY 列);"刚插入行的 id"用 RETURNING 或 currval(同会话);迁移后必须 setval 到 MAX(id) 防冲突;序列操作权限(USAGE/SELECT/UPDATE 对应 nextval/currval/setval)。

答题先分别定义三个函数(推进返回/会话内最近值/显式设置),再重点讲可见性规则(nextval 全局单调不回滚、currval 会话级且未调用报错、setval 全局立即生效),最后给使用场景(RETURNING 替代 currval、迁移后 setval 对齐)与风险(回拨冲突、非事务性)。

SELECT nextval('orders_seq');      -- 5(推进并返回)
SELECT currval('orders_seq');      -- 5(本会话最近值,未 nextval 会报错)
SELECT setval('orders_seq', 100);  -- 下一次 nextval 返回 101
-- 迁移后对齐
SELECT setval('orders_seq', (SELECT MAX(id) FROM orders));
-- 使用默认值
INSERT INTO orders (id, ...) VALUES (nextval('orders_seq'), ...);
#
★★★

22. 序列的并发安全机制,nextval() 的原子性、CACHE 1/100 的差异、重启时的空洞如何避免?

序列的并发安全机制是什么?nextval() 的原子性、CACHE 1/100 的差异、重启时的空洞如何避免?

  • nextval 的原子性(锁/无锁实现)
  • CACHE 参数的内存预分配
  • 空洞的来源与是否需避免

并发安全机制:nextval() 必须保证并发调用不重复——PostgreSQL 序列实现为"无锁的原子递增"(序列值存于内存缓存并定期写盘,递增用原子操作(非阻塞,PostgreSQL 8.2+ 用原子加(fetch-and-add),无锁),并发会话同时 nextval 拿到严格递增且不重复的值;Oracle 序列同样原子;MySQL 的 AUTO_INCREMENT 用表锁/互斥(InnoDB 自增锁:传统模式语句级锁、8.0 默认 innodb_autoinc_lock_mode=2 交错模式,批量插入仍保证唯一)。CACHE 参数:CREATE SEQUENCE ... CACHE 100 表示"内存预分配 100 个值"——每次访问缓存中的值无需写盘/无锁竞争(性能提升),用尽后再申请下一段(写盘);CACHE 1(默认)每次 nextval 都持久化(崩溃后从持久值继续,最安全但慢);CACHE 100 崩溃时"未使用的预分配值"丢失(重启后从已持久化段尾继续,产生空洞)——CACHE 越大性能越好、空洞风险越大)。

空洞(gap)的来源:其一,事务回滚(nextval 无事务语义);其二,崩溃/重启(CACHE 预分配值丢失、PostgreSQL 重启后序列文件回退到最近持久点(WAL 中序列段更新按 cache 粒度));其三,删除行/批量导入预留;其四,CACHE 段边界(多节点缓存分配)。是否需避免:主键"唯一且尽量小"即可,业务一般不需要连续——连续 ID 反而是信息泄露(暴露业务量)与并发热点(尾部插入争用);只有"对外票据号、流水号必须连续"的场景需要连续,此时——不要用序列的 CACHE(用 1 并接受回滚空洞仍存在)、把"分配"与"提交"绑定(应用层占号表 + 事务内确认/回填)、或用唯一约束+重试;正确认知:序列保证"唯一且递增",不保证"连续";追求连续需额外机制(占号回填、号段事务)。

答题先讲并发安全(PG 无锁原子递增、Oracle 原子、MySQL 自增锁/交错模式),再讲 CACHE 机制(内存预分配提性能、崩溃丢预分配值)与 CACHE 1/100 的取舍,最后分析空洞来源(回滚、崩溃、CACHE)并论证"唯一递增即可、连续需占号机制"的工程认知。

CREATE SEQUENCE orders_seq START 1 INCREMENT 1 CACHE 100;
-- 崩溃后可能从持久化段尾继续(空洞)
CREATE SEQUENCE strict_seq CACHE 1;  -- 最安全但仍不防回滚空洞
-- 连续需求:占号回填(应用层)
BEGIN;
INSERT INTO ticket_no (seq, used) VALUES (nextval('t_seq'), false) RETURNING seq;
-- 事务提交后 UPDATE ticket_no SET used = true;
#
★★★

23. 序列(Sequence)的本质是什么?它是表的对象还是独立的 schema 对象?PostgreSQL 的 CREATE SEQUENCE 与 MySQL 的 AUTO_INCREMENT 的根本差异?

序列(Sequence)的本质是什么?它是表的对象还是独立的 schema 对象?PostgreSQL 的 CREATE SEQUENCE 与 MySQL 的 AUTO_INCREMENT 的根本差异是什么?

  • 序列的独立对象本质
  • PG 的显式序列与 MySQL 的隐式计数器
  • 两种模型的差异与迁移

本质:序列是"独立的 schema 级对象"(不是表的附属物)——一个可独立创建、授权、查询(nextval/currval)、跨表共享的计数器对象;PG 中 CREATE SEQUENCE orders_seq 创建独立对象(可被多列/多表引用:DEFAULT nextval('orders_seq')),表只是"引用"它,DROP 表不删序列(需单独 DROP SEQUENCE);Oracle 的 SEQUENCE 同理(独立 schema 对象);MySQL 的 AUTO_INCREMENT 不是独立对象——是"列属性/表内计数器"(存于表定义,跟随表存亡,无独立名称、不可跨表共享、不可独立查询(LAST_INSERT_ID() 是会话函数非序列对象))。差异本质:PG/Oracle 的序列是"一等公民对象"(显式、可复用、可管理),MySQL 的自增是"表内隐式计数器"(隐式、表绑定、简单)。

根本差异清单:其一,对象模型——PG 序列独立(可跨表、可授权、可 setval、可 CACHE/INCREMENT 参数),MySQL 自增表内绑定(AUTO_INCREMENT 属性);其二,获取方式——PG DEFAULT nextval() 或显式调用、MySQL 省略该列自动分配(或 LAST_INSERT_ID() 取回);其三,并发与参数——PG 的序列参数丰富(START/INCREMENT/MINVALUE/CACHE/CYCLE),MySQL 只有起点/步长(8.0 无 CACHE 概念,步长由系统变量控制(auto_increment_increment/offset 用于多主复制));其四,复制——MySQL 自增在复制中依赖"值随 binlog 复制"(语句级复制要 auto_increment_increment 防冲突、行级复制值直接复制),PG 序列不随逻辑复制(复制表的数据不含序列状态,从库需单独 setval,物理复制则含);其五,迁移——MySQL 迁 PG:AUTO_INCREMENT → GENERATED AS IDENTITY/SERIAL(PG 内部仍是序列);PG 迁 MySQL:独立序列需改造为自增列或应用层分配。工程要点:需要"跨表共享号段/统一计数器"用独立序列(PG/Oracle 直接支持、MySQL 用专门分配表模拟);"每个表各自自增"用 AUTO_INCREMENT/IDENTITY;多主复制(MySQL 多源)用 auto_increment_offset 错开避免冲突。

答题先定义序列本质(独立 schema 计数器对象)与 PG/Oracle 的显式模型,再对比 MySQL 的 AUTO_INCREMENT(列属性、表内计数器、无独立对象),列根本差异(对象模型、获取方式、参数、复制、迁移),最后给跨表共享与迁移的工程建议。

-- PostgreSQL:独立序列对象
CREATE SEQUENCE global_seq START 1 CACHE 50;
CREATE TABLE a (id BIGINT DEFAULT nextval('global_seq') PRIMARY KEY);
CREATE TABLE b (id BIGINT DEFAULT nextval('global_seq') PRIMARY KEY);  -- 共享
DROP SEQUENCE global_seq;   -- 独立删除
-- MySQL:列属性(表内计数器)
CREATE TABLE a (id BIGINT AUTO_INCREMENT PRIMARY KEY);
#
★★★

24. 生成列(Generated Column)在 MySQL 5.7+、PostgreSQL 12+、SQL Server 中的实现差异,VIRTUAL vs STORED 的取舍?

生成列(Generated Column)在 MySQL 5.7+、PostgreSQL 12+、SQL Server 中的实现差异是什么?VIRTUAL vs STORED 的取舍是什么?

  • 生成列的语法与语义
  • VIRTUAL/STORED 的实现差异
  • 各库能力对比

生成列(Generated Column/计算列):列值由表达式自动计算(GENERATED ALWAYS AS (expr)),不能手工赋值;MySQL 5.7+:GENERATED ALWAYS AS (expr) VIRTUAL|STORED——VIRTUAL(默认)不存储、查询时计算(可用于索引(8.0 起 VIRTUAL 列可建二级索引)、不能做分区键/外键引用(8.0 部分放开))、STORED 物理存储(写入时计算落盘、可索引/分区键/外键引用、占用存储);PostgreSQL 12+:GENERATED ALWAYS AS (expr) STORED——只有 STORED 语义(无 VIRTUAL),表达式要求 IMMUTABLE、可建索引、不能作为分区键;SQL Server:computed column 语法 AS (expr) [PERSISTED]——默认计算(不存储,非持久化计算列不能建索引(需确定性+精确类型)、PERSISTED 存储后可索引(需确定性与索引条件)),SQL Server 的 computed column 可加 PERSISTED。

VIRTUAL vs STORED 取舍:VIRTUAL——不占存储、写入更快(省计算落盘?写入仍计算(用于索引/查询),但表不膨胀)、适合"频繁读、计算轻、想省空间"与"仅作查询便捷"的场景(MySQL 的 VIRTUAL + 索引是 8.0 常用技巧,如 JSON 提取列索引);STORED——写入时物化(读快、写慢、占空间)、适合"计算昂贵(避免每次查询重算)、需要外键/分区键/唯一约束引用(MySQL 中 STORED 支持更全)、频繁过滤/排序的表达式列";PG 只有 STORED 说明其设计取向(物化计算列)。共性限制:表达式必须确定性(IMMUTABLE——PG 强制、MySQL/SQL Server 需确定性)、不能引用其他生成列(MySQL 中 STORED 可引用 VIRTUAL?MySQL 限制:VIRTUAL 可引用 STORED?规则:生成列可引用普通列与其他生成列(有顺序限制:引用的生成列必须"先定义";MySQL 中 VIRTUAL 不能引用 STORED?实际上 MySQL 允许引用先定义的生成列);更新基列时生成列自动重算、DEFAULT 不能作用于生成列。应用:JSON 字段提取(JSON_EXTRACT 生成列索引)、规范化字段(全小写 email 生成列 + 唯一索引——软删除唯一技巧)、计算属性(总价 = 单价×数量))。

答题先定义生成列(表达式自动计算、不可手写),再分别讲三库语法与 VIRTUAL/STORED 支持(MySQL 双模式、PG 仅 STORED、SQL Server PERSISTED),然后对比 VIRTUAL(省存储、写入快、查询时算)与 STORED(物化、读快写慢、约束引用全)的取舍,最后列通用限制(确定性、引用顺序)与应用场景。

-- MySQL:VIRTUAL / STORED
ALTER TABLE t ADD total NUMERIC(12,2)
  GENERATED ALWAYS AS (price * qty) STORED;
ALTER TABLE t ADD email_lower VARCHAR(255)
  GENERATED ALWAYS AS (LOWER(email)) VIRTUAL;
CREATE INDEX idx_lower ON t(email_lower);
-- PostgreSQL:仅 STORED
ALTER TABLE t ADD total NUMERIC(12,2)
  GENERATED ALWAYS AS (price * qty) STORED;
-- SQL Server
ALTER TABLE t ADD total AS (price * qty) PERSISTED;
#
★★★

25. PostgreSQL 中序列关联列的实现(DEFAULT nextval('seq'))与 MySQL AUTO_INCREMENT 的等价写法?

PostgreSQL 中序列关联列的实现(DEFAULT nextval('seq'))与 MySQL 的 AUTO_INCREMENT 等价写法是什么?

  • PG 的 DEFAULT nextval 与 IDENTITY
  • MySQL 的 AUTO_INCREMENT
  • 等价映射与差异

PostgreSQL 实现"自增列"的两种方式:其一,显式序列 + 默认值——CREATE SEQUENCE users_id_seq; CREATE TABLE users (id BIGINT PRIMARY KEY DEFAULT nextval('users_id_seq'));(或建表后 ALTER SEQUENCE OWNED BY users.id 绑定);其二,SERIAL 简写——id SERIAL PRIMARY KEY(自动创建序列 users_id_seq 并挂默认值,bigserial/smallserial 对应 BIGINT/SMALLINT);其三,标准 IDENTITY——id BIGINT GENERATED ALWAYS AS IDENTITY(PG 10+,自动创建内部序列,GENERATED ALWAYS 禁止手工赋值、BY DEFAULT 允许覆盖),官方推荐 IDENTITY。MySQL 等价:id BIGINT AUTO_INCREMENT PRIMARY KEY——列属性由 InnoDB 维护表内计数器,省略该列插入时自动分配。

等价映射:MySQL AUTO_INCREMENT → PG 的 GENERATED ALWAYS AS IDENTITY(或 SERIAL);获取刚插入的 id——MySQL 用 LAST_INSERT_ID()(会话级),PG 用 INSERT ... RETURNING id(推荐)或 currval('seq')(会话级);重置——MySQL ALTER TABLE t AUTO_INCREMENT = n,PG ALTER SEQUENCE seq RESTART WITH n 或 setval。差异点:其一,对象性——PG 序列是独立对象(可共享、可查看(\ds)、可授权),MySQL 自增是表内属性(无独立对象);其二,赋值控制——IDENTITY ALWAYS 禁止显式插入 id(需 OVERRIDING SYSTEM VALUE 或改 BY DEFAULT),MySQL 默认允许显式插入(插入后计数器自动跳到 max+1);其三,多列/共享——PG 序列可被多表共享(DEFAULT nextval 同一序列),MySQL 每表独立;其四,复制——PG 物理复制含序列状态、逻辑复制不含(从库需 setval),MySQL 自增值随 binlog(行级复制直接复制值);其五,迁移——MySQL→PG 用 IDENTITY/SERIAL,PG→MySQL 用 AUTO_INCREMENT,注意 PG 的 IDENTITY 序列名与表绑定(OWNED BY)。

答题先讲 PG 的三种实现(显式序列+DEFAULT、SERIAL、IDENTITY,推荐 IDENTITY),再给 MySQL 的 AUTO_INCREMENT 与等价格式(类型映射、获取方式 LAST_INSERT_ID vs RETURNING、重置语法),最后列对象性、赋值控制、共享、复制四类差异与迁移映射。

-- PostgreSQL:三种等价写法
CREATE TABLE users (id BIGINT DEFAULT nextval('users_id_seq') PRIMARY KEY);
CREATE TABLE users (id BIGSERIAL PRIMARY KEY);
CREATE TABLE users (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
INSERT INTO users (name) VALUES ('a') RETURNING id;   -- 取回新 id
-- MySQL
CREATE TABLE users (id BIGINT AUTO_INCREMENT PRIMARY KEY);
INSERT INTO users (name) VALUES ('a');
SELECT LAST_INSERT_ID();
#
★★★

26. 为什么 MySQL 的 AUTO_INCREMENT 在某些场景下会“空洞”(自增不连续)?

为什么 MySQL 的 AUTO_INCREMENT 在某些场景下会"空洞"(自增不连续)?

  • 自增空洞的来源
  • 事务回滚与删除的影响
  • 批量插入与自增锁

空洞来源:其一,事务回滚——InnoDB 的自增分配"不回滚":INSERT 语句先取自增值(即使事务后来 ROLLBACK,已分配的值作废不复用),这是空洞的最大来源(并发事务大量回滚时空洞明显);其二,行删除——DELETE 删除的行占过的自增值不会复用(AUTO_INCREMENT 只增不减),删除后后续插入继续用更大的值(空洞 = 已删除的区间);其三,批量插入与自增锁——INSERT ... SELECT、LOAD DATA 等批量插入按"批量分配"(传统模式一次分配整批区间),语句失败/部分回滚时整段作废;8.0 的 innodb_autoinc_lock_mode=2(交错模式)下"简单插入"与"批量插入"交错分配,批量段可能整体跳跃(若批量插入被中断);其四,显式插入——插入指定 id 后计数器跳到 max(id)+1,与之前的序列之间有空洞;其五,重启与回滚段——MySQL 8.0 重启后自增值可能回退(8.0 之前的持久化行为:8.0 中 AUTO_INCREMENT 计数器持久化到 redo log(8.0.0+ 每变更写 redo?8.0 把自增计数器持久化避免重启回退(8.0 改进),但 8.0 早期版本重启后仍可能回退到 max 计算值——MySQL 8.0 中计数器随 redo 持久化,重启不回退;之前版本重启后取 max+1 可能与分配过的值重叠?早期版本(5.7)重启后取当前最大行值+1,若删除最大行则回退产生"复用"风险(非空洞而是回退,8.0 修复)))。

是否可避免:不能完全避免——自增的语义是"唯一且递增",不承诺连续;需要连续(票据号)时应改用"占号回填/号段表"或接受"每成功事务一个号"的口径(用 COUNT 而非 max(id) 统计);空洞无害性——主键连续与否不影响索引性能与正确性(B 树只要求唯一有序);工程认知:把"自增值连续"当业务需求是反模式(并发下成本高),除非合规要求(流水号)才引入额外机制。注意 MySQL 8.0 中 AUTO_INCREMENT 计数器随 redo log 持久化、崩溃恢复不回退(5.7 及以前回退行为),升级后可减少"重启导致的复用/回退"问题。

答题先列四类空洞来源(回滚、删除、批量自增锁、显式插入)并解释各自机制(分配不回滚、只增不减、批量区间、max+1 跳变),再讲 8.0 的持久化改进(计数器随 redo、重启不回退)与 5.7 的差异,最后给"自增不承诺连续、连续需求用占号机制"的工程结论。

#
★★★

27. PostgreSQL 中 serial、bigserial、smallserial 的差异?

PostgreSQL 中 serial、bigserial、smallserial 的差异是什么?各自的使用场景是什么?

  • 三种 serial 的底层类型
  • 序列创建与绑定行为
  • 与 IDENTITY 的对比

三者是"自增列简写":serial——INTEGER 类型(4 字节,上限约 21 亿),smallserial——SMALLINT(2 字节,±32767),bigserial——BIGINT(8 字节,±9.2 万亿);共同行为:CREATE TABLE t (id serial PRIMARY KEY) 等价于"建整数列 + 自动创建序列 t_id_seq + 列默认值 nextval('t_id_seq') + 序列 OWNED BY t.id(随表删除)",序列名规则"表名_列名_seq"。选择依据:按主键数量级——中小型表 serial、大表/预留增长 bigserial(现代默认推荐 bigserial,避免 INT 上限事故)、小枚举/序号 smallserial(极少用,自增语义下 32767 太小);注意"serial 不是真正的类型"——PG 中 serial 是语法糖(展开为 INTEGER + 序列),\d 显示 integer 与默认值 nextval;序列参数默认 START 1 INCREMENT 1、无 CACHE(默认 CACHE 1)。

与 IDENTITY 对比:GENERATED ALWAYS/BY DEFAULT AS IDENTITY(PG 10+)是标准语法——内部同样是"整数列+序列",差异在:IDENTITY 序列名自动生成、GENERATED ALWAYS 阻止手工插入、DROP 列/表时序列自动管理(OWNED)、pg_dump 还原行为更标准(备份恢复后序列状态正确);官方推荐新代码用 IDENTITY 而非 serial(serial 是历史简写,仍有大量存量)。工程注意:serial 列 insert 时省略该列;显式插入 id 后需 setval 对齐(否则后续冲突);查看序列:\ds 或 pg_sequences;重置:ALTER SEQUENCE t_id_seq RESTART;类型转换(serial 不够用)——ALTER COLUMN TYPE BIGINT 并重建序列/默认值(或直接初始就用 bigserial)。

答题先给三者底层类型与上限(INT/SMALLINT/BIGINT + 自动序列绑定),再讲"serial 是语法糖"(展开为列+序列+默认值+OWNED BY)与序列命名规则,最后对比 IDENTITY(标准、ALWAYS 控制、官方推荐)并给选型(默认 bigserial/IDENTITY)与 setval 对齐注意。

CREATE TABLE a (id SERIAL PRIMARY KEY);        -- 等价 INTEGER + seq + nextval
CREATE TABLE b (id BIGSERIAL PRIMARY KEY);     -- 推荐:预留增长
CREATE TABLE c (id SMALLSERIAL PRIMARY KEY);   -- 上限 32767,极少用
-- 等价展开
CREATE SEQUENCE a_id_seq;
CREATE TABLE a (id INTEGER DEFAULT nextval('a_id_seq') PRIMARY KEY);
ALTER SEQUENCE a_id_seq OWNED BY a.id;
-- 推荐标准写法
CREATE TABLE d (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
#
★★★

28. PostgreSQL 中如何获取当前序列值?

PostgreSQL 中如何获取当前序列值?有哪些方法与注意事项?

  • currval 的会话语义
  • 查询序列状态的系统视图
  • last_value 的读取限制

方法:其一,currval('seq')——返回"本会话最近一次 nextval 返回的值"(不推进序列),注意:未在本会话调用过 nextval 时调用 currval 报错(currval of sequence ... is not yet defined in this session);这是"当前值"的会话级读取,跨会话读取不到别人的 nextval 值;其二,pg_sequences 视图——SELECT last_value, is_called FROM pg_sequences WHERE schemaname='public' AND sequencename='orders_seq':last_value 是"序列文件中的持久值"(is_called 表示 last_value 是否已被使用),注意 last_value 与"下一个将要返回的值"的关系:is_called=true 时下一个 nextval 返回 last_value+1(无 CACHE 时),CACHE >1 时 last_value 是"已持久化段的末尾"而非实际内存中的下一个值(内存中可能已预分配更多);其三,直接读序列表——SELECT last_value FROM orders_seq(序列内部是表,可直接 SELECT,但只反映持久状态,CACHE 下不等于内存值)。

注意事项:其一,"当前值"的两种口径——会话已分配值(currval)与序列持久值(last_value),业务上要区分;其二,CACHE 影响——有 CACHE 时 last_value 可能落后于内存中已分配的值(并发 nextval 后);其三,并发下"读当前值"本身无意义(下一个 nextval 可能已被别的会话取走),设计上不要依赖"当前值+1"预测;其四,取"刚插入行的 id"正确做法是 INSERT ... RETURNING id(推荐,事务安全)或 currval(同会话、紧邻插入);其五,setval 修改后 last_value 立即变化(全局生效)。工程建议:需要"刚生成 id"用 RETURNING;需要查看序列状态(诊断、对齐)用 pg_sequences(含 last_value/is_called/数据缓存信息);避免用 last_value 做业务预测。

答题先给三种读取方法(currval 会话级、pg_sequences 视图、直接查序列表)与各自语义,再重点讲注意事项(currval 未调用报错、last_value 与内存值在 CACHE 下的差异、并发下预测无意义),最后给"取刚插入 id 用 RETURNING"的工程建议。

SELECT currval('orders_seq');          -- 本会话最近 nextval 值(未调用报错)
SELECT last_value, is_called FROM pg_sequences
WHERE schemaname = 'public' AND sequencename = 'orders_seq';
INSERT INTO orders (...) VALUES (...) RETURNING id;  -- 推荐
#
★★★

29. UUID 与自增 ID 在主键场景下的取舍?

UUID 与自增 ID 在主键场景下的取舍是什么?各自的优缺点与适用场景是什么?

  • 自增 ID 的优缺点
  • UUID 的优缺点(v4 随机/v7 有序)
  • 选型依据与折中方案

自增 ID(BIGINT + AUTO_INCREMENT/IDENTITY/序列)优点:存储小(8 字节)、索引紧凑(B+ 树页内键多、缓存命中高)、插入有序(尾部追加、页分裂少)、可读(订单号 10001)、可预测(用于分页/增量同步);缺点:分布式/分库分表下不唯一(各片重复)、暴露业务量(可被爬取统计)、合并/迁移时冲突、离线生成不便(需中心分配)。UUID(128 位)优点:全局唯一(无需中心协调)、离线/客户端可生成、不可枚举(安全)、分库分表天然适用;缺点:v4 随机无序(索引页分裂、插入热点分散、缓存命中差)、存储大(16 字节二进制/36 字符文本,索引膨胀)、不可读;UUID v7(时间有序)弥补无序性(前 48 位时间戳、后随机),保留唯一与离线生成,索引友好度接近自增。其他方案:雪花 ID/号段在"分布式+有序+紧凑"上折中。

选型依据:单机/单库传统业务——自增 BIGINT(简单、性能最优);分布式/分库分表/多系统合并——UUID v7 或雪花 ID;需要隐藏规模与防枚举——UUID/雪花(雪花时间戳可解析出时间,防枚举性弱于随机 UUID);离线生成(客户端离线插入、数据采集)——UUID;跨系统主键合并(多源导入去重)——UUID/雪花;迁移兼容——已有自增业务可保持并加"业务唯一键(UUID/自然键)"双轨。折中方案:BIGINT 自增作内部主键 + UUID 作对外业务键(两列,索引各一);或 UUID 排序规范化为"有序 UUID"(时间前缀);或雪花 ID 一举多得(分布式+8 字节+趋势有序)。结论:无分布式需求用自增 BIGINT;有分布式/合并需求用 UUID v7 或雪花,避免 UUID v4 随机(索引与缓存代价)。

答题先分别列自增(小、有序、可读但分布式失效、暴露量)与 UUID(全局唯一、离线可生成但 v4 无序、大、不可读)的优缺点,再引入 UUID v7 与雪花折中,最后按场景给选型矩阵(单库自增、分布式雪花/UUID v7、防枚举随机、离线 UUID)与双轨方案。

#
★★★

30. 为什么序列的 nextval 不随事务回滚回退(无事务语义),及其对自增 ID 空洞的影响

为什么序列的 nextval 不随事务回滚回退(无事务语义)?这对自增 ID 空洞有什么影响?

  • nextval 非事务性的原因
  • 并发正确性要求
  • 空洞的必然性与影响

原因:序列设计为"并发分配器"而非"事务数据"——nextval 需要在并发会话间严格不重复,若实现为"事务提交才生效",则:其一,并发事务同时 nextval 会拿到相同值(A 未提交时 B 拿到 A 的值)破坏唯一性;其二,实现"提交前锁定值"需要分布式锁/等待(事务提交前值被占用),并发吞吐崩溃(分配器不能阻塞于事务生命周期);其三,序列与 MVCC 无关(不参与回滚版本链),它的状态是"单一全局计数器"。因此 nextval 采用"分配即生效、永不回退"的语义:值一旦分配就永久作废(即使事务回滚、语句失败),这是"唯一且递增"与"高并发"的必然折中。setval 同样非事务性(立即全局生效,回滚不影响)。

对空洞的影响:事务回滚(或语句失败)后,已分配的自增值不会复用——后续 nextval 继续递增,中间出现空洞(gap);空洞的其他来源(删除、CACHE、批量中断)。影响评估:空洞对"主键唯一性与索引性能"无任何影响(B 树按值有序,空洞只是键值区间空缺);对业务的影响仅在"需要连续编号"的场景(票据号、流水号)——此时序列/自增不适合,需"占号+提交确认"机制(占号表:事务内申请号码、提交时标记已用、回滚释放/作废,牺牲并发与复杂度换连续性);统计行数用 COUNT(*) 而非 max(id)(有空洞时 max(id) 不等于行数)。结论:非事务 nextval 是"并发唯一分配器"的必然设计,接受空洞(默认)或换占号机制(强连续需求);不要试图"回拨序列填洞"(会与已存在行冲突、破坏单调性)。

答题先解释非事务性的原因(并发分配需立即生效、避免锁阻塞与重复分配、序列是全局计数器),再分析对空洞的影响(回滚作废不复用、空洞必然)与影响评估(主键正确性无关、连续需求需占号机制),最后给结论与反模式警示(回拨填洞)。

#
★★

31. 为什么主键不建议使用 UUID v1 而推荐 v7?

为什么主键不建议使用 UUID v1 而推荐 v7?

  • UUID v1 的结构与缺陷
  • UUID v7 的设计
  • 版本演进与选型

UUID v1(基于时间+节点)结构:时间戳(60 位,100ns 粒度)+ 时钟序列 + 节点(48 位 MAC 地址);缺陷:其一,隐私泄露——节点字段是 MAC 地址(暴露机器身份),被 RFC 4122 后来版本明确不建议使用;其二,时间戳顺序问题——v1 的时间戳是"低序在前"(时间戳字段内低 32 位在前),字典序与时间序不一致(看起来随机、无序),索引页分裂问题与随机 UUID 类似(不如 v7 有序);其三,时钟序列与单调性——依赖系统时钟,时钟回拨/重置时可能产生重复或乱序;其四,MAC 获取在虚拟化/容器环境不可靠;其五,可预测性——时间戳+固定节点使 ID 可被推测(安全角度弱于随机 v4)。因此 RFC 9562(2024 更新)把 v1/v4 标记为"不建议新使用"。

UUID v7(时间有序):结构 = 48 位 Unix 毫秒时间戳 + 12 位版本/变体 + 62 位随机数:时间戳在前且"大端有序"——按字典序排序即按时间排序,插入接近有序(索引友好、页分裂少、缓存命中高),同时保留随机位(唯一性、防枚举、无需中心协调、离线可生成)。与 v1 对比:v7 无 MAC(隐私)、有序(索引)、随机部分保证唯一性;与 v4 对比:v7 有序(v4 随机导致索引分裂);与雪花 ID 对比:v7 128 位(大)但标准通用(跨系统互操作、数据库原生函数 uuidv7()(PG 18+、MySQL 9 等支持)),雪花 64 位紧凑但需自实现与机器 ID 管理。结论:新系统主键若选 UUID——用 v7(有序+标准+无隐私问题),不用 v1/v4(v1 隐私与无序、v4 无序);数据库支持(PG 18 的 uuidv7()、MySQL 9.0 的 UUIDv7())使 v7 落地简单;需要 8 字节紧凑且可自控时用雪花 ID。

答题先讲 v1 的四类缺陷(MAC 隐私、时间戳低序导致的乱序、时钟依赖、可预测),再讲 v7 的结构(毫秒时间戳大端在前+随机位:有序、无隐私、唯一)与优势,最后对比 v4/雪花并给"新系统用 v7"的结论与数据库原生函数支持。

#
★★

32. 序列在复制场景下的行为差异(PG 物理复制、MySQL 主从)?

序列在复制场景下的行为差异是什么?PostgreSQL 物理复制与 MySQL 主从复制中的序列/自增如何处理?

  • PG 物理复制(WAL 重放)与序列
  • PG 逻辑复制与序列
  • MySQL 主从与自增(binlog 模式)

PostgreSQL 物理复制(流复制,WAL 重放):序列的变更也写入 WAL(序列递增是 WAL 日志的一部分)——从库通过重放 WAL 获得与主库一致的序列状态(序列值、is_called 均复制),故障切换后从库升主无需处理序列;但注意:从库上的 nextval 不被允许(从库只读,序列状态只跟随主库)。PostgreSQL 逻辑复制(发布-订阅,行级变更流):序列状态"不复制"——逻辑复制只复制表数据(行事件),序列是独立对象不在发布内;订阅端若使用相同序列名,其序列状态停留在初始值,插入订阅端表可能主键冲突(订阅端应使用自己的序列并 setval 对齐,或主键用非序列来源);因此逻辑复制拓扑中序列需单独管理(初始化对齐、升级时同步)。MySQL 主从:binlog 复制下自增值处理取决于 binlog_format——行级复制(ROW,默认):插入语句的最终自增值直接写入 binlog 行事件,从库应用时"使用 binlog 中的值"(从库计数器同步推进、两端一致);语句级复制(STATEMENT):复制 SQL 语句,从库执行语句时自行分配自增值——多主/级联场景需 auto_increment_increment/offset 错开避免冲突(同一时刻两端可能分配相同值),且语句中的非确定性(如并发顺序)可能使两端值不同;MySQL 8.0 默认 ROW + 自增计数器持久化(redo)后行为更一致。级联(主→从→从)下自增按 binlog 传递;切换(failover)后新主继续从持久化计数器分配(8.0 无回退)。对比总结:PG 物理复制自动同步序列(故障切换无忧)、PG 逻辑复制不管序列(需手工对齐);MySQL ROW 复制值随 binlog(从库一致)、STATEMENT 复制需 offset 防冲突;工程上——PG 逻辑复制订阅端务必 setval 对齐或使用独立序列,MySQL 多写拓扑配置 auto_increment_offset。

答题先讲 PG 物理复制(WAL 含序列、从库跟随、切换无忧)与逻辑复制(序列不复制、订阅端需对齐),再讲 MySQL 的 ROW(值入 binlog、两端一致)与 STATEMENT(语句重放、需 increment/offset 错开)模式,最后给工程对策(逻辑复制对齐、多主 offset)。

-- PG 逻辑复制:订阅端手动对齐序列
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
-- MySQL 多主/级联:错开自增
SET GLOBAL auto_increment_increment = 2;  -- 节点 1
SET GLOBAL auto_increment_offset = 1;
SET GLOBAL auto_increment_increment = 2;  -- 节点 2
SET GLOBAL auto_increment_offset = 2;
#
★★

33. 域(DOMAIN)在 PostgreSQL 中的实现,CREATE DOMAIN 与 CREATE TYPE 的差异,DOMAIN 可携带约束吗?

域(DOMAIN)在 PostgreSQL 中的实现是什么?CREATE DOMAIN 与 CREATE TYPE 的差异、DOMAIN 可否携带约束?

  • DOMAIN 的定义与实现
  • DOMAIN 携带的约束(CHECK/DEFAULT/NOT NULL)
  • 与 CREATE TYPE 的差异

DOMAIN 实现:CREATE DOMAIN 名 AS 底层类型 [DEFAULT 默认值] [NOT NULL] [CHECK (表达式引用 VALUE)]——基于现有类型的"受限包装",规则绑定在域上、列声明用域类型即自动获得规则(规则一处定义、多处复用);域不是新存储类型(存储与底层类型相同,无 IO 函数、无类型系统改动),是"类型+约束"的元数据层。可携带的约束:CHECK(表达式内用 VALUE 表示列值,如 CHECK (VALUE >= 0)、可多个 CHECK)、NOT NULL(域级非空)、DEFAULT(列的默认值,列级 DEFAULT 覆盖域级);约束在插入/更新时强制执行,同列级约束一致(CHECK 对 NULL 放行语义一致)。修改:ALTER DOMAIN ADD CONSTRAINT/ DROP CONSTRAINT/SET DEFAULT/SET NOT NULL——存量数据校验(ADD CONSTRAINT 会扫描校验,PG 无 NOT VALID 选项于 DOMAIN?ALTER DOMAIN ADD CONSTRAINT 校验存量)。DROP DOMAIN 需先处理依赖它的列(CASCADE 或先改列类型)。

CREATE DOMAIN vs CREATE TYPE:CREATE TYPE(基础类型)定义"新存储类型"——需要输入/输出函数(I/O functions)、内部表示、类型名(如复合类型、枚举、范围、自定义 base type),存储与运算由类型自身定义;DOMAIN 不新建存储(底层类型决定存储与运算),只叠加约束与默认值;差异总结:DOMAIN 是"约束复用层"(轻量、可带 CHECK/NOT NULL/DEFAULT、底层类型不可见)、TYPE 是"新数据表示"(重量级、需 IO 函数、影响存储)。使用场景:统一金额/状态/电话等列规则(多个表共享同一约束定义)、约束可维护性(改域即改所有使用处);注意:域与底层类型在函数参数匹配/运算符解析上不完全等价(部分场景需显式 CAST)、域上的约束会影响 COPY 与批量导入(违反即报错)、不能对域列直接用底层类型的部分函数(视函数签名)。对比:MySQL 无 DOMAIN(用 ENUM/CHECK 近似)、Oracle 支持 DOMAIN?Oracle 无 CREATE DOMAIN(用约束与类型子类型近似)、SQL Server 有 CREATE TYPE ... FROM base WITH 规则(别名类型,类似域)。

答题先讲 CREATE DOMAIN 的语法与实现(底层类型包装、规则绑定、ALTER/DROP 管理),再列可携带的约束(CHECK 用 VALUE、NOT NULL、DEFAULT)与执行语义,最后对比 CREATE TYPE(新存储+IO 函数 vs 约束复用层)并给使用场景与限制(函数匹配、MySQL 无 DOMAIN)。

CREATE DOMAIN positive_money AS NUMERIC(12,2)
  DEFAULT 0
  NOT NULL
  CHECK (VALUE >= 0);
CREATE TABLE orders (total positive_money);   -- 自动获得规则
ALTER DOMAIN positive_money ADD CONSTRAINT ck_max CHECK (VALUE <= 1000000);
DROP DOMAIN positive_money CASCADE;
#
★★

34. 复合类型(Composite Type)在 PostgreSQL 中的用法,声明、构造(ROW())、访问(.field)、比较语义。

PostgreSQL 中复合类型(Composite Type)的用法是什么?声明、构造(ROW())、访问(.field)、比较语义各如何?

  • CREATE TYPE 复合类型声明
  • ROW() 构造与字面量
  • 字段访问与比较

声明:CREATE TYPE address AS (city TEXT, street TEXT, zip TEXT);——自定义复合类型(一组字段);也可"表自动成为复合类型"(表名即类型名,RETURNS 表名 返回该表行结构)。构造:ROW('北京','中关村','100080') 或显式类型转换 '("北京","中关村","100080")'::address(逗号分隔的括号字面量,字符串内引号转义);也可从表行构造(SELECT ROW(city, street) FROM ...)。访问:复合列/变量用点号——a.city、r.street(记录/复合类型字段访问),表查询中 SELECT (addr).city FROM t(复合列需括号再取字段,避免歧义);函数返回复合类型时 RETURN ROW(...)。比较语义:复合类型支持 =、<>(整体相等:所有字段逐字段相等)、排序(<、>、BETWEEN——按"字段顺序逐字段比较",类似元组比较:先比第一个字段、相同再比第二个);NULL 语义:字段中任一为 NULL 时整体比较返回 NULL(= 为 UNKNOWN);可用于索引吗——复合类型整体不能直接建普通 B 树索引(可用表达式(字段提取)或对复合列建 GiST?可对复合列建 B-tree 索引(按复合排序规则)——PG 支持复合类型的 B-tree 索引(rare);哈希索引需 hash 函数(复合类型有默认 hash?可建))。

使用场景:函数返回多值(RETURNS 复合类型/表名)、表的复合列(表嵌套表:父表存子结构的复合列,替代 1:1 子表——简化但失约束与查询能力)、批量参数(数组 of 复合类型传递)。注意:复合列"作为整体"更新(UPDATE t SET addr = ROW(...))、复合列上的约束只能整体(无法对 addr.city 建 NOT NULL/CHECK——这是复合列的局限,复杂约束需拆表或触发器);与 JSONB 对比:复合类型强类型(字段静态)、JSONB 灵活(动态)——静态结构用复合类型、动态用 JSONB。

答题先讲声明(CREATE TYPE 与表即类型)、构造(ROW() 与字面量)、访问(点号与括号),再重点讲比较语义(逐字段字典序、NULL 传播、索引可行性),最后给使用场景(函数多值返回、复合列、批量参数)与局限(字段级约束缺失、整体更新)及与 JSONB 的取舍。

CREATE TYPE address AS (city TEXT, street TEXT, zip TEXT);
CREATE TABLE users (id INT PRIMARY KEY, addr address);
INSERT INTO users VALUES (1, ROW('北京','中关村','100080'));
SELECT (addr).city, addr.zip FROM users;              -- 字段访问
SELECT * FROM users WHERE addr = ROW('北京','中关村','100080');
-- 排序:逐字段比较(city 相同再比 street)
SELECT * FROM users ORDER BY addr;
#
★★

35. 数组类型(Array)在 PostgreSQL 中的原生支持,构造(ARRAY[])、访问([i])、数组操作符(&&、@>、<@)。

PostgreSQL 数组类型(Array)的原生支持是什么?构造(ARRAY[])、访问([i])、数组操作符(&&、@>、<@)如何使用?

  • 数组类型声明与构造
  • 下标访问与维度
  • 数组操作符语义与索引

声明与构造:CREATE TABLE t (tags TEXT[]);——任何类型可加 [](含多维 int[][]、复合类型数组);构造:ARRAY['a','b'] 或字面量 '{a,b}'(字符串内逗号、引号转义)、ARRAY(SELECT ...)(子查询构造数组)、ARRAY_AGG 聚合生成;下标访问:tags[1](下标从 1 开始,访问越界返回 NULL 而非报错)、array_lower/array_upper/array_length/array_dims 取维度信息、多维访问 tags[1][2];切片:tags[1:2]。修改:整体更新(UPDATE t SET tags = ARRAY['x'])、array_append/array_prepend/array_cat/array_remove/array_replace 函数、|| 拼接(数组||元素、数组||数组)。

操作符与语义:=、<>(数组相等:同长度且逐元素相等)、<、>(逐元素字典序比较);包含与交集:@>(左包含右:左边数组包含右边所有元素,顺序无关)、<@(右包含左)、&&(重叠:两数组有公共元素);其他:||(拼接)、-(差?数组差集无原生操作符(array 差用 EXCEPT?没有,需自定义或 array_remove))、IS NULL/IS DISTINCT FROM;元素级判断:value = ANY(arr)(元素是否在数组中)、value <> ALL(arr)。索引:GIN 索引(CREATE INDEX ON t USING GIN (tags))加速 @>、<@、&& 与 = ANY(数组列上的包含/重叠查询)、btree_gin 扩展让标量数组可复合索引;普通 B 树索引支持数组整体排序(较少用)。NULL 语义:数组元素可为 NULL('{a,NULL}')、数组整体可为 NULL;@> 对 NULL 元素按"存在性"处理(NULL 元素比较返回 NULL→不匹配?GIN 索引对 NULL 元素不建条目,查询含 NULL 元素需注意);空数组 '{}' 与 NULL 数组区分。 使用场景:标签/枚举多选(tags TEXT[])、批量 ID 参数(WHERE id = ANY($1))、聚合收集(array_agg)。注意:数组不适合"频繁按元素更新/查询引用"(整体重写、无元素唯一约束、跨表引用不便)——需要元素级约束/关联时拆表(多值属性);与 JSONB 对比:数组强类型有序、JSONB 灵活动态,静态列表用数组。

答题先讲声明构造(ARRAY[]/字面量/子查询)与下标访问(1 起始、越界 NULL、维度函数、切片),再重点讲操作符(= 逐元素、@>/<@ 包含、&& 重叠、= ANY)与 GIN 索引加速,最后给使用场景与边界(元素级约束缺失、整体更新)及与拆表/JSONB 的取舍。

CREATE TABLE t (tags TEXT[]);
INSERT INTO t VALUES (ARRAY['pg','sql']);
INSERT INTO t VALUES ('{redis,mq}');
SELECT tags[1] FROM t;                        -- 'pg'
SELECT * FROM t WHERE tags @> ARRAY['sql'];   -- 包含查询(GIN 索引)
SELECT * FROM t WHERE tags && ARRAY['redis']; -- 重叠
SELECT * FROM t WHERE 'pg' = ANY(tags);       -- 元素存在
CREATE INDEX ON t USING GIN (tags);
#
★★

36. 枚举类型(ENUM)在 PostgreSQL、MySQL 中的实现差异,枚举值的存储大小、修改成本、索引支持。

枚举类型(ENUM)在 PostgreSQL 与 MySQL 中的实现差异是什么?存储大小、修改成本、索引支持如何?

  • 两库 ENUM 的定义与存储
  • 加值/改值的成本
  • 索引与查询行为

定义与存储:PostgreSQL——CREATE TYPE mood AS ENUM ('sad','ok','happy')(类型级对象,schema 内可复用);存储为 4 字节(内部 OID 引用枚举值,不是字符串),比较按"声明顺序"(枚举排序 = 定义顺序,sad < ok < happy);MySQL——列级内联 ENUM('sad','ok','happy')(不是独立类型,每次声明各自列表);存储为 1-2 字节(内部序号:最多 255 个值时 1 字节、更多 2 字节),比较按"列表顺序"(索引位置即序号),与 PG 相同的"定义序排序"。修改成本:PostgreSQL——ALTER TYPE mood ADD VALUE 'ecstatic';(PG 12+ 可在事务内/外添加,只能"追加到末尾"(不能插入中间或重排——重排需重建类型),正在使用的枚举值不能删除/改名(需重建类型+迁移列);PG 14+ 支持 ALTER TYPE ... ADD VALUE 后新值立即可用(14 前需事务提交后);修改成本中等(catalog 更新、无数据重写——追加不改存量存储);MySQL——ALTER TABLE t MODIFY col ENUM('sad','ok','happy','ecstatic')(重建表(8.0 前阻塞、8.0 在线 DDL 部分支持?ENUM 修改 8.0 中 ALTER 需 COPY(ALGORITHM=COPY,8.0 无 INPLACE 支持 for ENUM 变更——8.0 修改 ENUM 列表仍为 COPY 算法,锁表+重建),大表成本高;删除/重排值会改变存量行的序号映射(错误风险)。索引支持:两者都支持 ENUM 列建 B 树索引(按枚举序排序:PG 按类型定义序、MySQL 按列表序),索引体积小(4 字节/1-2 字节键)、范围查询(WHERE mood >= 'ok')按定义序工作;PG 的枚举支持 btree 与 hash 索引;MySQL ENUM 的排序/比较按序号(注意字符串排序与序号排序的一致性由列表顺序决定))))。

工程取舍:枚举值"稳定、少、静态"用 ENUM(存储小、语义强、排序定义序);"频繁增删改"(动态字典)用字典表(外键引用:ALTER TABLE 加行即可,无需 DDL)或 CHECK 约束 + TEXT;PG 的枚举适合域级复用,MySQL 的列级枚举在"多表同枚举"时重复定义易漂移(统一用字典表);注意 MySQL ENUM 的"非法值静默存空串"(严格模式报错、非严格存 '')与排序规则(默认按序号排序,ORDER BY col 得到定义序而非字典序)是经典坑。

答题先分别讲两库的定义与存储(PG 类型级 4 字节 OID、MySQL 列级 1-2 字节序号,均按定义序排序),再对比修改成本(PG ADD VALUE 追加、删除重建;MySQL ALTER 重建表 COPY),再讲索引支持(B 树按定义序、小键值),最后给"静态小枚举用 ENUM、动态字典用字典表"的选型与 MySQL 排序/非法值坑。

-- PostgreSQL
CREATE TYPE mood AS ENUM ('sad','ok','happy');
CREATE TABLE t (m mood);
ALTER TYPE mood ADD VALUE 'ecstatic';          -- 追加(PG 12+)
-- MySQL
CREATE TABLE t (m ENUM('sad','ok','happy'));
ALTER TABLE t MODIFY m ENUM('sad','ok','happy','ecstatic');  -- 重建表
-- 字典表方案(动态)
CREATE TABLE dict_status (code TEXT PRIMARY KEY);
CREATE TABLE t (status TEXT REFERENCES dict_status(code));
#
★★

37. 范围类型(Range Type)在 PostgreSQL 中的原生支持,int4range、numrange、tsrange、daterange 的边界(inclusive/exclusive)。

PostgreSQL 范围类型(Range Type)的原生支持是什么?int4range、numrange、tsrange、daterange 的边界(inclusive/exclusive)如何使用?

  • 范围类型的种类与声明
  • 边界包含性([ ] 闭、( ) 开)
  • 范围操作符与索引

范围类型(Range):表示"值的连续区间"(含边界语义),原生类型:int4range(整数)、int8range、numrange(numeric)、tsrange/tstzrange(时间戳)、daterange(日期);声明列:CREATE TABLE t (booking tsrange, age int4range)。构造与边界:'[1,10)'::int4range 或 int4range(1, 10, '[]')——方括号 [ 或 ] 表示"包含边界"(闭),圆括号 ( 或 ) 表示"不包含边界"(开);如 [1,10) 含 1 不含 10;daterange 用日期、tsrange 用时间戳;特殊值:empty(空范围)、无限边界('[1,)' 右无穷)。标准形式:PG 自动把范围归一为标准形式(如 [1,10) 整数范围规范化为 [1,9]?int4range 的规范形式是 [1,9]?int4range 存储时统一为"左闭右开"?不——int4range 规范化为 [lower, upper) 的形式(如 int4range(1,10) 显示 [1,10)),离散类型规范化为左闭右开、连续类型(numrange/tsrange)保留用户边界)。操作符:包含/被包含/重叠/邻接/相交——@>(区间包含值或区间)、<@、&&(重叠)、-|-(邻接,无空隙相接)、<<(严格小于)、>>(严格大于)、+(并集)、-(差集)、*(交集);函数:lower(r)/upper(r)(边界值)、isempty(r)、lower_inc/upper_inc(边界包含性)、range_merge 等。索引:GiST 索引(CREATE INDEX ON t USING GIST (booking))加速 @>、&& 等区间查询(区间重叠/包含是核心场景),exclusion 约束(EXCLUDE USING gist (booking WITH &&))实现"区间不重叠"约束(会议室预定防冲突)。NULL 与空:空区间 isempty、NULL 值合法(范围列可 NULL)。

使用场景:预定/排程冲突检测(会议室、酒店房间时间窗)、价格区间/年龄区间查询、有效期限建模([start, end))。注意:边界包含性错误导致"端点值查不到/重复计"是经典坑(如 daterange '[2024-01-01,2024-01-31)' 不含 1 月 31 日);离散类型建议左闭右开规范避免双计数。

答题先列范围类型种类与构造语法(括号边界 [ 闭 ( 开、示例、无限/空),再讲操作符(@> 包含、&& 重叠、-|- 邻接、并差交)与 GiST 索引/EXCLUDE 约束,最后给使用场景(排程冲突、区间查询)与边界包含性陷阱。

CREATE TABLE room_booking (room_id INT, period tsrange);
INSERT INTO room_booking VALUES (1, '[2024-01-01 09:00, 2024-01-01 10:00)');
SELECT * FROM room_booking WHERE period && tsrange('2024-01-01 09:30','2024-01-01 11:00');
CREATE INDEX ON room_booking USING GIST (period);
-- 区间不重叠约束
ALTER TABLE room_booking ADD EXCLUDE USING gist (room_id WITH =, period WITH &&);
#
★★

38. PostgreSQL 中如何删除自定义类型?DROP TYPE 的级联影响?

PostgreSQL 中如何删除自定义类型?DROP TYPE 的级联影响是什么?

  • DROP TYPE 语法(IF EXISTS、CASCADE/RESTRICT)
  • 依赖类型列的处理
  • 级联删除的风险

语法:DROP TYPE [IF EXISTS] 类型名 [CASCADE|RESTRICT]——删除自定义类型(枚举、复合、范围、基础类型、DOMAIN 用 DROP DOMAIN);IF EXISTS 幂等(不存在时 NOTICE);RESTRICT(默认)——存在依赖时拒绝删除并报错("cannot drop type ... because other objects depend on it"),CASCADE——级联删除依赖对象:使用该类型的表列(列随之删除!CASCADE 会删除引用该类型的列或整表(若表只有该类型列?删除列;若整表依赖(表结构包含)则删列,若表本身作为类型被引用则删表)、依赖的函数/运算符/域等。级联影响示例:CREATE TYPE mood AS ENUM(...); CREATE TABLE t (m mood); DROP TYPE mood CASCADE;——t.m 列被直接删除(数据随列丢失!这是 CASCADE 的最大风险:你以为删类型,实际删了表列与数据);若类型被函数返回类型引用(函数返回 mood),函数也被删除。安全实践:其一,删除前查依赖——pg_depend 查询引用该类型的对象(列:pg_attribute 的 atttypid)、information_schema.columns 的 data_type 与 udt_name;其二,先解除依赖——把列改类型(ALTER TABLE t ALTER COLUMN m TYPE text)或先删列/表,再 DROP TYPE(不带 CASCADE);其三,确需 CASCADE 时先备份(导出数据与 DDL)并在低峰执行;其四,删除类型不可逆(PG 的 DROP 在事务内可回滚——DROP TYPE 是事务性 DDL,事务内可 ROLLBACK 恢复,但提交后不可);其五,枚举/复合类型被"表即类型"引用(每个表都是复合类型)——DROP TABLE 不要求删类型(表类型随表);删除前检查是否有视图/函数依赖。注意 DROP TYPE 与 DROP DOMAIN 的区别(域删除用 DROP DOMAIN,级联规则相同))。

答题先给语法(IF EXISTS、CASCADE/RESTRICT)与 RESTRICT 报错语义,再重点讲 CASCADE 的级联影响(删除使用该类型的表列→数据丢失、删除依赖函数),最后给安全实践(pg_depend 查依赖、先改列类型再删、备份低峰、事务可回滚)。

-- 查依赖该类型的列
SELECT c.relname AS table, a.attname AS column
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE a.atttypid = 'mood'::regtype AND a.attnum > 0;
-- 安全删除:先解除依赖
ALTER TABLE t ALTER COLUMN m TYPE text;
DROP TYPE mood;
-- 或级联(风险:删列)
DROP TYPE mood CASCADE;
#
★★

39. 自定义类型的输入/输出函数(IO function)的实现步骤是什么?

PostgreSQL 自定义类型的输入/输出函数(IO function)的实现步骤是什么?

  • 自定义基础类型需要 IO 函数
  • 输入/输出函数的签名与语言
  • CREATE TYPE 的组装步骤

背景:CREATE TYPE 创建"基础类型(base type)"时需要提供输入/输出函数(把"外部文本表示"与"内部存储表示"互转)——PG 存储与显示都经过这对函数;枚举/复合/范围类型无需自写 IO(系统生成)。实现步骤:第一步,选择内部表示——如用一个 C 结构/整数/定长字节序列(简单类型可用整数实现:内部表示 = int4);第二步,写输入函数(cstring → 内部表示):输入函数接收"文本形式"(如 '1.5')解析并返回内部表示,非法输入报错(elog(ERROR));输出函数(内部表示 → cstring):把内部表示格式化为文本(可 NULL 返回表示 NULL)。语言选择——C(性能、需要编译为共享库 .so 并 CREATE FUNCTION ... AS 'lib' 'symbol')、或用 SQL/plpgsql 实现"简单类型"(PG 支持用 SQL/plpgsql 写 IO 函数:输入函数可做文本解析返回内部类型(如 int4),输出函数返回 text——适合内部表示即内置类型的场景,如把"自定义校验的文本"包装为类型);第三步,CREATE TYPE 组装:CREATE TYPE mytype (INPUT = mytype_in, OUTPUT = mytype_out, INTERNALLENGTH = 4, PASSEDBYVALUE, ALIGNMENT = int4, STORAGE = plain);——关键选项:INPUT/OUTPUT(函数名)、INTERNALLENGTH(内部字节数:定长填数字、变长填 VARIABLE)、PASSEDBYVALUE(按值传递,小类型)、ALIGNMENT(对齐:char/int2/int4/double)、STORAGE(plain/external/extended/main,变长时)、CATEGORY/SUBTYPE 等;第四步,测试:文本转换(SELECT '1.5'::mytype)、创建表插入查询;第五步(可选)——运算符(CREATE OPERATOR)、索引支持(btree 操作符类 opclass)、类型转换(CAST)等配套,让类型"可用"。

注意:IO 函数必须 IMMUTABLE;输入函数要严格校验非法文本;C 函数需正确管理内存(palloc);开发调试用简单语言(SQL/plpgsql)先行,复杂再 C;官方建议多数场景优先用"域/复合/枚举"而非自定义基础类型(IO 与 opclass 成本高)。

答题按四步讲实现:选内部表示 → 写输入/输出函数(签名 cstring 互转、错误处理、IMMUTABLE)→ CREATE TYPE 组装(INPUT/OUTPUT/INTERNALLENGTH/PASSEDBYVALUE/ALIGNMENT 等关键选项)→ 配套运算符与索引类,最后提醒"能用域/复合/枚举就别自定义基础类型"。

CREATE FUNCTION mytype_in(cstring) RETURNS mytype LANGUAGE plpgsql IMMUTABLE AS $$ ... $$;
CREATE FUNCTION mytype_out(mytype) RETURNS cstring LANGUAGE plpgsql IMMUTABLE AS $$ ... $$;
CREATE TYPE mytype (
  INPUT = mytype_in, OUTPUT = mytype_out,
  INTERNALLENGTH = 4, PASSEDBYVALUE = true, ALIGNMENT = int4
);
-- 配套操作符类(支持排序/索引)
CREATE OPERATOR CLASS mytype_ops DEFAULT FOR TYPE mytype USING btree AS
  OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 =, OPERATOR 4 >=, OPERATOR 5 >;
#
★★

40. JSON 与 JSONB 的查询性能差异?

PostgreSQL 中 JSON 与 JSONB 的查询性能差异是什么?各自的存储与索引行为如何?

  • JSON 文本存储 vs JSONB 二进制
  • 查询速度与写入速度
  • 索引与操作符差异

存储差异:JSON 类型"原样保存文本"(保留空白、键顺序、重复键,存储即输入文本,每次解析开销在"查询/访问时");JSONB 是"解析后的二进制格式"(键排序、去重、无空白,规范化存储,写入时解析)。性能差异:写入——JSON 更快(不做解析直接存文本),JSONB 较慢(写入需解析+规范化+键排序);查询/访问——JSONB 远快于 JSON:JSON 每次访问(如 j->>'key'、函数提取)都要实时解析文本(且无法索引),JSONB 数据已结构化(键值可直接查找、无需重复解析);大文档上 JSON 的重复解析开销显著(查询慢数倍)。索引:JSONB 支持 GIN 索引(@> 包含、->> 键值、路径查询、全文等:jsonb_path_ops 与默认 ops 两种 GIN 类),B 树索引(整体比较);JSON 不支持 GIN(无操作符类,只能全表扫描或表达式索引(对 JSON 列做函数提取后建索引:CREATE INDEX ON t ((j ->> 'key'))——表达式索引可建,但查询需相同表达式);JSONB 的 @> 等操作符可直接走 GIN。功能差异:JSONB 支持更多操作符与函数(@>、?、?|、?& 存在性查询、jsonb_path_* 路径查询、全文检索(GIN 的 tsvector 集成));JSON 保留原始文本(需要"原样回显"、保留键序/重复键/精确格式的场景用 JSON——但一般业务无此需求))。

选型结论:默认 JSONB(查询、索引、功能全面占优),JSON 仅用于"必须保留原始文本/极少查询/导入解析成本敏感"的场景;注意 JSONB 的键排序与去重会改变文本外观(回显非原样);大小差异:JSONB 通常更小(去空白)或相近;两者可互转(::jsonb/::json)。工程建议:分析/检索型 JSON 数据用 JSONB + GIN;仅存储透传(日志原始体)用 JSON 或直接 TEXT。

答题先对比存储(JSON 原样文本 vs JSONB 规范化二进制),再分别讲查询/写入性能差异(JSON 访问时解析 vs JSONB 结构化直取、JSONB 写入慢)、索引(JSONB 的 GIN @> 与存在性、JSON 只能表达式索引),最后给选型结论与转换注意(文本外观变化)。

CREATE TABLE t (doc JSONB);
CREATE INDEX ON t USING GIN (doc);
SELECT * FROM t WHERE doc @> '{"status": "paid"}';       -- 走 GIN
SELECT * FROM t WHERE doc ->> 'status' = 'paid';
-- JSON(文本):只能表达式索引
CREATE INDEX ON t ((doc ->> 'status'));
#
★★

41. LTREE 类型在 PostgreSQL 中的用途?

PostgreSQL 中 LTREE 类型(ltree 扩展)的用途是什么?如何表达与查询树形数据?

  • LTREE 的标签路径表示
  • 祖先/后代/匹配查询
  • 与递归 CTE/物化路径的对比

LTREE(ltree 扩展,CREATE EXTENSION ltree)是"物化路径(Materialized Path)"的专用类型:用"点分标签路径"表示树节点位置——如 'Top.Science.Astronomy'(每段是标签,标识从根到该节点的路径),比"parent_id 邻接表 + 递归 CTE"更直接地支持层级查询。能力:祖先/后代判断——path <@ 'Top.Science'(path 是其后代)、path @> 'Top.Science'(path 是祖先)、~(匹配正则/lquery)、?(lquery 匹配:'Top.{2}' 两层通配);层级访问——nlevel(path)(深度)、subpath(path, 1, 2)(取子路径)、subltree、索引——GiST 与 B-tree 索引支持 <@、@>、~ 等操作符(祖先/后代查询走索引,避免逐层递归);排序——按路径字典序(同父节点连续、深度优先顺序)。lquery 语法:'Top.Science.'(任意一层)、'*.Astronomy'(任意祖先下的 Astronomy)、'{2}'(重复次数)、& 与 |(逻辑组合)。

对比:邻接表(parent_id)——规范化、更新只动一行(移动子树改 parent_id 一行)、但"查询整棵子树/祖先"需递归 CTE(深度大时慢);LTREE/物化路径——查询子树/祖先 O(路径长度) 且走索引(快)、但"移动子树"要更新所有后代路径(前缀批量更新)、插入需维护路径字符串、无外键引用(标签非表引用);LTREE 提供了"路径的类型安全与操作符/索引"(优于手写 TEXT 物化路径)。使用场景:分类树/组织架构/评论树(层级固定、读多写少)、快速"查某节点全部子孙/祖先"(权限树、导航菜单);写多(频繁移动)用邻接表+递归 CTE,读多(频繁查子树)用 ltree。注意:ltree 是扩展(需 CREATE EXTENSION)、标签长度限制(255 字节/段?标签限长)、与 ltree 的 lquery 语法学习成本;对照:PostgreSQL 16+ 的 pg_tree?(无原生树类型,ltree 仍是标准选择)、MySQL 无 ltree(物化路径用 VARCHAR 手写或闭包表)。

答题先定义 LTREE(物化路径专用类型:点分标签、扩展提供)与核心查询能力(<@/@> 祖先后代、~ lquery 匹配、nlevel/subpath、GiST 索引),再对比邻接表+递归 CTE(更新优/查询慢)与手动物化路径(无类型支持),最后给场景选型(读多查子树用 ltree、写多移动用邻接表)。

CREATE EXTENSION ltree;
CREATE TABLE category (id INT PRIMARY KEY, path LTREE);
INSERT INTO category VALUES (1, 'Top.Science.Astronomy');
SELECT * FROM category WHERE path <@ 'Top.Science';        -- 后代(走索引)
SELECT * FROM category WHERE path @> 'Top.Science.Astronomy'; -- 祖先
SELECT * FROM category WHERE path ~ 'Top.*{1,2}';          -- lquery 匹配
CREATE INDEX ON category USING GIST (path);
#
★★

42. PostgreSQL 中如何创建复合类型?

PostgreSQL 中如何创建复合类型?语法与用法是什么?

  • CREATE TYPE ... AS (字段列表)
  • 复合类型的使用(列、函数返回、ROW 构造)
  • 修改与删除

创建:CREATE TYPE 名 AS (字段1 类型1, 字段2 类型2, ...);——复合类型是"命名的字段结构"(类似"行结构模板"),如 CREATE TYPE address AS (city TEXT, street TEXT, zip VARCHAR(10));。使用场景:其一,表列——CREATE TABLE users (addr address);(复合列:整行存于列,字段访问 (addr).city);其二,函数返回——RETURNS address 或 RETURNS TABLE 中复用复合类型、函数内 RETURN ROW('北京',...)/RETURN (SELECT ...);其三,批量参数——复合类型数组(address[])作函数参数(SQLAlchemy 等可映射);其四,过程语言变量——PL/pgSQL 中声明 record 变量按复合类型赋值。构造与访问:ROW(...) 构造(或 '(...)'::type 字面量)、点号访问(复合列需括号)、整体比较(逐字段)、可作为表类型(CREATE TABLE t OF address?(typed table,可继承类型结构))。修改:ALTER TYPE address ADD ATTRIBUTE/DROP ATTRIBUTE/ALTER ATTRIBUTE 字段类型(PG 12+)——修改属性影响使用该类型的列(表列自动获得新结构:加属性后已有行新字段为 NULL);改名 ALTER TYPE RENAME。删除:DROP TYPE address [CASCADE](级联删列风险)。注意事项:复合类型与"表的行类型"等价(每个表自动是同名复合类型,可用 RETURNS 表名 或 %ROWTYPE 引用);复合类型参与 JSON/行转换(row_to_json、to_jsonb);复合列上不能直接建字段级约束/索引(需拆表或用表达式索引((addr).city)建表达式索引);比较与排序按字段顺序字典序。与 JSONB 取舍:结构固定、强类型用复合类型;动态结构用 JSONB。

答题先给 CREATE TYPE ... AS 语法与构造/访问方法(ROW、点号、字面量),再列四类使用场景(复合列、函数返回、数组参数、typed table),最后讲修改(ALTER TYPE ADD/DROP ATTRIBUTE)与删除(CASCADE 风险)及与表行类型/JSONB 的关系。

CREATE TYPE address AS (city TEXT, street TEXT, zip VARCHAR(10));
CREATE TABLE users (id INT PRIMARY KEY, addr address);
INSERT INTO users VALUES (1, ROW('北京','中关村','100080'));
SELECT (addr).city FROM users;
CREATE FUNCTION get_addr() RETURNS address LANGUAGE sql AS
  $$ SELECT ROW('上海','南京路','200000')::address $$;
ALTER TYPE address ADD ATTRIBUTE country TEXT DEFAULT 'CN';
#
★★

43. PostgreSQL 的 citext 类型的作用是什么?

PostgreSQL 的 citext 类型的作用是什么?如何实现大小写不敏感比较?

  • citext 的定义与原理
  • 与 lower() 函数的对比
  • 索引与注意事项

citext(CREATE EXTENSION citext)是"大小写不敏感文本类型":存储与 TEXT 相同(原样保留大小写),但"比较时按小写化处理"(内部比较用 lower() 语义)——因此 =、<、LIKE、GROUP BY、DISTINCT、唯一约束都大小写不敏感('Foo' = 'foo' 为真、UNIQUE 把 'Foo' 与 'foo' 视为重复)。原理:citext 的 IO/比较函数内部把值转小写后再比较(类型本身仍是文本存储,比较操作符经过 citext 的实现重载)。用法:CREATE TABLE users (email CITEXT UNIQUE);——邮箱唯一不区分大小写;WHERE name = 'ABC' 匹配 'abc'。

与 lower() 方案对比:手动写法是"存储/比较统一 lower"(列存 lower 值或 WHERE lower(col) = lower('x') + 表达式索引)——显式但易漏(忘记 lower 就大小写敏感、GROUP BY/唯一约束难统一);citext 把"不敏感"内建到类型比较(所有操作符自动生效、唯一约束/GROUP BY/DISTINCT/连接键天然一致),代码简洁且不易出错;代价——比较时每次调用 lower 逻辑(函数调用开销,比原生 text 比较略慢)、索引(B 树索引按"小写键"组织,支持 = 与范围查询,可用);性能:citext 索引与普通 text 索引性能相当(比较路径多一层 lower),高频等值查询可接受;注意点:其一,citext 的比较基于 lower(),多字节与排序规则(collation)行为跟随数据库排序规则(大小写折叠规则因 locale 而异——如土耳其语 i 问题);其二,citext 列不改变"显示/存储"(查询返回原大小写);其三,LIKE 前缀匹配走 citext 索引需保证查询模式也按不敏感语义(citext 的 ~~ 操作符已处理);其四,扩展类型(需要 CREATE EXTENSION,托管环境可能受限);其五,排序顺序按小写值(ORDER BY 大小写不敏感排序)。选型:需要"不敏感但不改变存储"用 citext;需要精确控制(如仅唯一不敏感、查询大小写敏感)用"存储小写+普通 text"+CHECK/触发器(软删除唯一场景常用生成列 lower 方案)。

答题先定义 citext(存储原样、比较按小写:=、唯一、GROUP BY 均不敏感)与原理(比较函数内部 lower),再对比 lower() 手动方案(易漏 vs 内建一致)与性能/索引,最后列注意事项(collation 与 locale、显示原样、扩展依赖)与选型。

CREATE EXTENSION citext;
CREATE TABLE users (email CITEXT UNIQUE);   -- 邮箱唯一且大小写不敏感
INSERT INTO users (email) VALUES ('A@B.COM');
SELECT * FROM users WHERE email = 'a@b.com';   -- 命中
-- 对比:手动 lower 方案
CREATE TABLE u2 (email TEXT UNIQUE);
CREATE UNIQUE INDEX u2_lower ON u2 (LOWER(email));
#
★★

44. PostgreSQL 的 hstore 类型(键值对)的使用场景?

PostgreSQL 的 hstore 类型(键值对)的使用场景是什么?与 JSONB 有何差异?

  • hstore 的定义与操作
  • 适用场景
  • 与 JSONB 的对比与取舍

hstore(CREATE EXTENSION hstore)是"键值对集合"类型:单列存多组"字符串键→字符串值"(如 'name=>Tom, age=>30'——键值都是文本,无类型、无嵌套),支持:键值访问(data->'key'、data->>'key')、存在性(data ? 'key'、?&、?|)、包含(@>、<@:data @> 'role=>admin')、键集/值集(akeys/avals、each() 展开成行)、更新(|| 合并、delete 函数删除键)、GIN/GiST 索引(@>、? 等操作符走索引);hstore 无嵌套与数组(扁平键值对)。使用场景:其一,"稀疏动态属性"——实体的附加自定义字段(每行不同的键集合:商品参数、用户扩展属性),用 hstore 避免"加列/宽表 NULL"(对比 EAV 表:hstore 单列查询与索引更高效);其二,快速 KV 检索——键值存在性与等值过滤(? 'key'、@>)走 GIN;其三,迁移场景的中间格式——从键值存储(Redis Hash 等)迁移、多源异构数据的暂存。与 JSONB 对比:JSONB 支持嵌套、数组、类型化值(数字/布尔/null)、路径查询(jsonb_path)、更多函数(jsonb_each、jsonb_agg)、更广生态(标准 SQL/JSON、ORM 支持),hstore 只有扁平字符串 KV(简单、存储略小、操作符直观);选型——简单扁平 KV 且追求轻量:hstore;需要嵌套/类型/标准 JSON:JSONB;两者可互转(hstore→jsonb::jsonb?(hstore 可 ::jsonb,反之 jsonb 不能直接 ::hstore(有嵌套)))。

注意:hstore 值是文本(数字需 ::int 转换比较)、无排序定义(可用键排序函数)、键空字符串与 NULL 语义(hstore 的 NULL 值:'k=>NULL' 表示 SQL NULL 值存在、'k=>""' 空串)容易混淆;现代新项目一般直接 JSONB(功能超集),hstore 更多出现在存量系统与"极简 KV 需求"。

答题先定义 hstore(扁平字符串键值对)与核心操作(-> 访问、? 存在、@> 包含、each 展开、GIN 索引),再列适用场景(稀疏动态属性、KV 检索、迁移中间格式),最后与 JSONB 全面对比(嵌套/类型/路径/生态)并给选型结论(新项目默认 JSONB)。

CREATE EXTENSION hstore;
CREATE TABLE products (id INT PRIMARY KEY, attrs hstore);
INSERT INTO products VALUES (1, 'color=>red, size=>M');
SELECT attrs->'color' FROM products;                    -- 'red'
SELECT * FROM products WHERE attrs ? 'color';           -- 存在性(GIN)
SELECT * FROM products WHERE attrs @> 'color=>red';     -- 包含(GIN)
CREATE INDEX ON products USING GIN (attrs);
#
★★

45. PostgreSQL 的 tsvector 类型与全文检索的关系?

PostgreSQL 的 tsvector 类型与全文检索的关系是什么?如何实现全文搜索?

  • tsvector/tsquery 的定义
  • 全文检索的流程(to_tsvector/to_tsquery)
  • GIN 索引与排名

关系:tsvector 是"文本向量"类型——把文档预处理后的"词项(lexeme)+ 位置/权重"存储结构(分词、词干化(English stemming)、去停用词后的结果),是 PostgreSQL 全文检索(Full Text Search, FTS)的核心数据类型;配合 tsquery(查询向量)与 @@ 匹配操作符完成全文匹配。检索流程:其一,索引侧——把文本转 tsvector(to_tsvector('english', body))并建 GIN 索引(CREATE INDEX ON docs USING GIN (tsv) 或表达式索引 to_tsvector('english', body));分词按配置(parser + 词典:中文用 zhparser/pg_jieba 扩展或 simple 配置(中文无原生分词,按空格/标点切分));权重(A/B/C/D 标注词项重要性);其二,查询侧——用户输入转 tsquery:to_tsquery('english', '查询词')(自动分词+词干化,& | ! 逻辑组合:'cat & dog' 且、'cat | dog' 或、'!cat' 非、短语 '<->' 邻近)、plainto_tsquery(把整句当 AND 短语)、phraseto_tsquery(精确短语);匹配:tsv @@ tsq(返回布尔,走 GIN 索引);其三,排名——ts_rank(tsv, tsq) 按词频/位置/权重打分排序(ORDER BY ts_rank DESC)、ts_headline 生成高亮摘要;其四,语言配置——text search configuration('english'/'russian'/自定义),决定分词器、停用词、同义词词典(CREATE TEXT SEARCH DICTIONARY/SYNONYM)。与 LIKE '%x%' 对比:LIKE 无法走索引(全扫描、无词干化、无排序)、FTS 有 GIN 索引+相关性排序+语言处理,是"文档搜索"的正道(量级中等场景;海量/复杂检索用 Elasticsearch 等专用引擎)。注意:表达式索引 vs 独立 tsvector 列(生成列/触发器维护)的选择——表达式索引简单但每次写入计算、独立列便于权重调整与更新控制;中文需扩展(zhparser)或"ngram 配置";排名函数对结果截断(LIMIT)。

答题先定义 tsvector(词项+位置权重的预处理向量)与 tsquery、@@ 匹配,再讲完整流程(to_tsvector 索引侧、to_tsquery 查询侧、ts_rank 排名、配置与词典、中文扩展),最后对比 LIKE 并给"中等规模 FTS 用 PG、海量用 ES"的结论。

CREATE TABLE docs (id INT PRIMARY KEY, body TEXT);
CREATE INDEX ON docs USING GIN (to_tsvector('english', body));
SELECT id FROM docs
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'cat & dog')
ORDER BY ts_rank(to_tsvector('english', body), to_tsquery('english', 'cat & dog')) DESC
LIMIT 10;
-- 或独立 tsvector 列
ALTER TABLE docs ADD tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('english', body)) STORED;
#
★★

46. UUID 类型与 TEXT 存储 UUID 的性能对比?

PostgreSQL 中 UUID 类型与 TEXT 存储 UUID 的性能对比是什么?

  • UUID 与 TEXT 的存储大小
  • 比较与索引性能
  • 迁移与转换

存储:UUID 类型 16 字节(内部二进制表示),TEXT 存 UUID 文本 36 字符(含连字符,UTF-8 下 36 字节)——UUID 类型省一半以上存储;索引:UUID 列的 B 树索引键 16 字节(页内键多、缓存命中高),TEXT 键 36 字节(页内键少、索引更大、扫描更慢)——等值/范围查询 UUID 类型性能更好(索引体积与比较代价);比较:UUID 按 16 字节二进制比较、TEXT 按字节比较 36 字符(比较路径略长,差异微小但存在);内存与缓存:UUID 类型行更窄(页内行多、shared_buffers 命中率更高),全表扫描更快。其他差异:校验——UUID 类型强制合法格式('not-a-uuid' 报错),TEXT 任意文本(脏数据可入);函数——UUID 支持 gen_random_uuid() 生成、无内置字符串函数需求(::text 转换显示)、TEXT 有字符串函数(substring 等)但对 UUID 无意义;排序——UUID 类型按二进制值排序(等价十六进制字典序),TEXT 按文本字典序(连字符位置相同,两者一致?'xxxxxxxx-...' 文本序与二进制序一致(同长度同字符集),基本等价);存储对齐——UUID 类型 16 字节自然对齐(行内无填充问题)。工程结论:存储 UUID 一律用 UUID 类型(校验、体积、索引性能全面占优),TEXT 存 UUID 是反模式(仅历史/迁移遗留场景,或需要"宽松格式容忍");迁移——ALTER TABLE t ALTER COLUMN id TYPE UUID USING id::uuid(需数据全合法,非法值先清理);ORM 映射(Hibernate 的 UUID 映射到 PG uuid、SQLAlchemy 的 Uuid 类型)。注意:UUID 类型的主键"随机性"带来的插入页分裂问题(v4)与类型本身无关(v7 可缓解);索引性能差异在百万行级才明显,但类型正确性(校验)从小规模就重要。

答题先对比存储(16 vs 36 字节)与索引/缓存影响(页内键数与命中率),再讲校验与函数差异(强制格式 vs 任意文本)、排序等价性,最后给"一律用 UUID 类型、TEXT 是反模式"的结论与迁移写法。

CREATE TABLE t (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ...);
-- 迁移:TEXT → UUID
ALTER TABLE t ALTER COLUMN id TYPE UUID USING id::uuid;
-- 先清理非法值
SELECT id FROM t WHERE id !~ '^[0-9a-f]{8}-...$';
#
★★

47. 自定义类型如何与 ORM 框架(SQLAlchemy、Hibernate)集成?

自定义类型如何与 ORM 框架(SQLAlchemy、Hibernate)集成?

  • 自定义类型在 ORM 中的映射问题
  • SQLAlchemy 的 TypeDecorator/自定义类型
  • Hibernate 的 UserType/AttributeConverter

问题背景:ORM 需要把"数据库自定义类型"映射为"语言类型"——枚举(PG ENUM)、复合类型、数组、JSONB、citext、DOMAIN 等默认映射可能失败或失真,需要显式集成。SQLAlchemy(Python):其一,TypeDecorator——包装现有类型做"处理逻辑定制"(如把列值加解密、自定义校验、把 DOMAIN 语义用 CheckConstraint 表达):class EncryptedType(TypeDecorator): impl = LargeBinary ...(处理 init/process_bind_param/process_result_value);其二,原生类型映射——SQLAlchemy 的 sqlalchemy.dialects.postgresql 提供 JSONB、ARRAY、ENUM、UUID、INET 等方言类型(from sqlalchemy.dialects.postgresql import JSONB, ARRAY, UUID, ENUM——可直接映射 PG 自定义枚举(ENUM('a','b', name='mood') 对应 CREATE TYPE mood);其三,复合类型——用 from_text/composite 或自定义 TypeDecorator 把 ROW(...) 与 Python 元组互转;其四,TypeEngine 子类——最底层自定义(实现 get_col_spec/bind_processor/result_processor)。Hibernate(Java):其一,AttributeConverter<X,Y>——"属性转换器":JPA 标准方式,把 Java 类型与"数据库可存类型"互转(如把枚举转字符串、把 PG 枚举映射为字符串列(配合 @Enumerated(STRING))、把 JSON 对象与 String 互转(Jackson 序列化));其二,UserType(org.hibernate.usertype.UserType)——Hibernate 原生接口(实现 nullSafeGet/nullSafeSet/assemble/disassemble),用于映射 PG 自定义类型(复合类型、数组、UUID(6.2+ 内置)、枚举:@Type(type="pgsql_enum") 或自定义 UserType 处理 PG 原生 ENUM 的 OID 读写);其三,方言与 hibernate-types 库——vladmihalcea/hibernate-types 提供 JsonType、PostgreSQLEnumType 等现成实现(处理 JSONB、数组、枚举);其四,@JdbcTypeCode 与 @Column(columnDefinition)(显式 DDL 类型)。集成要点:其一,DDL 一致性——ORM 建表(ddl-auto)需能生成自定义类型(或用 migration 工具预建类型、ORM 只映射列);其二,读写路径对称——bind 与 result 转换必须可逆;其三,NULL 处理——自定义类型处理器要正确传播 NULL(UserType 的 nullSafe*、TypeDecorator 的 process_null);其四,查询——自定义类型的 WHERE 参数(如数组参数)需按方言绑定(ARRAY 绑定列表、JSONB 绑定字符串);其五,测试——集成测试覆盖类型往返(插入-查询-比较))。

答题先讲问题(ORM 默认映射自定义类型失败/失真),再分别给 SQLAlchemy(TypeDecorator/方言类型 ENUM/ARRAY/JSONB/TypeEngine)与 Hibernate(AttributeConverter、UserType、hibernate-types 库)的集成方式,最后列要点(DDL 一致性、读写可逆、NULL、参数绑定、集成测试)。

# SQLAlchemy:方言类型直接映射
from sqlalchemy.dialects.postgresql import JSONB, ARRAY, UUID, ENUM
class Product(Base):
    __tablename__ = "products"
    id = mapped_column(UUID(as_uuid=True), primary_key=True)
    tags = mapped_column(ARRAY(String))
    attrs = mapped_column(JSONB)
# TypeDecorator 自定义处理
class EncryptedString(TypeDecorator):
    impl = Text
    def process_bind_param(self, value, dialect):
        return encrypt(value) if value else None
// Hibernate:AttributeConverter(JSON ↔ String)
@Converter
public class JsonConverter implements AttributeConverter<Map<String,Object>, String> {
    public String convertToDatabaseColumn(Map m) { return toJson(m); }
    public Map convertToEntityAttribute(String s) { return fromJson(s); }
}
#
★★

48. MySQL InnoDB 在线加索引(ALGORITHM=INPLACE、LOCK=NONE)的实现细节。

MySQL InnoDB 在线加索引(ALGORITHM=INPLACE、LOCK=NONE)的实现细节是什么?

  • 在线 DDL 的算法与锁级别
  • INPLACE 的实现机制(重建 vs 就地)
  • 限制与监控

背景:MySQL 5.6+ 在线 DDL(ALGORITHM 与 LOCK 选项):ALGORITHM=INPLACE(就地构建,不复制整表数据)与 ALGORITHM=COPY(旧方式:复制表数据到新表,锁表且空间翻倍);LOCK=NONE(允许并发 DML)、LOCK=SHARED(只读并发)、LOCK=EXCLUSIVE(全锁)。加索引(CREATE INDEX/ALTER TABLE ADD INDEX)是"支持 INPLACE 的典型操作":INPLACE 下分三个阶段——准备(Prepared):创建索引的内存结构并加元数据锁(共享);执行(Execute):扫描主表构建索引(B 树构建,页内填充);提交(Commit):索引元数据替换;期间 DML 被允许(LOCK=NONE)——InnoDB 通过"在线日志(online log/redo 记录 DML 变更)"机制:构建索引期间的并发 INSERT/UPDATE/DELETE 被记录,索引构建完成后"回放"应用到新索引(增量合并),保证最终一致。实现细节:其一,二级索引 INPLACE 不需要重建主表(只扫主键构建索引树,通过"排序构建"(sort-merge 或临时文件)减少随机 IO);主键变更/表重建(OPTIMIZE、加列)才需"重建表"(仍是 INPLACE(8.0 的 INSTANT 除外)——重建在后台新表空间进行、期间 DML 记录、完成后切换);其二,LOCK=NONE 的可行性——加二级索引支持;主键删除/修改、加列(部分)需要更高锁级别(LOCK=EXCLUSIVE 或不允许 NONE);其三,8.0 的改进——INSTANT 算法(加列等元数据级操作 O(1) 不重建)、ALGORITHM=INSTANT 优先级(8.0.12+ 自动)、inplace 加索引支持并行构建(8.0.14+ innodb_parallel_build_index);其四,监控——performance_schema 的 statements 显示 ALGORITHM/LOCK 实际值(无法按请求级别执行时自动降级:如 INPLACE 不支持的操作回退 COPY、LOCK=NONE 不满足回退 SHARED/EXCLUSIVE,需检查"实际执行计划"(SHOW 语句状态或 performance_schema.events_statements_current 的 SQL_TEXT 中 ALGORITHM 提示?实际用 EXPLAIN/status 观察);其五,风险——INPLACE 仍占用临时空间(索引构建需要磁盘空间:重建表约 1 倍表大小、构建索引按索引大小)、长事务下 online log 增长(DML 记录量大时失败回滚)、大表加索引期间 IO 与 CPU 占用(限速可用 8.0 的 INNODB 参数)。工程实践:大表加索引优先 ALGORITHM=INPLACE, LOCK=NONE(8.0 默认自动选);执行前评估临时空间、低峰执行、用 pt-osc/gh-ost 作为补充(超大表场景避免原生 DDL 的锁/IO 尖峰);验证实际算法(information_schema.innodb_online_alter_log 或监控语句状态))。

答题先讲 ALGORITHM(INPLACE/COPY)与 LOCK(NONE/SHARED/EXCLUSIVE)语义,再重点讲加索引 INPLACE 的三阶段与"online log 记录并发 DML 后回放"机制、二级索引不需重建表,然后列限制(主键变更需更高锁、8.0 INSTANT、并行构建)与监控/风险(空间、日志增长、降级),最后给工程实践。

ALTER TABLE big_table ADD INDEX idx_col (col),
  ALGORITHM=INPLACE, LOCK=NONE;
-- 查看实际算法与锁(performance_schema)
SELECT SQL_TEXT, ALGORITHM, LOCK FROM performance_schema.events_statements_current;
-- 8.0 INSTANT 加列
ALTER TABLE t ADD COLUMN c INT, ALGORITHM=INSTANT;
#
★★

49. PostgreSQL CREATE INDEX CONCURRENTLY 的工作原理与失败回滚机制?

PostgreSQL CREATE INDEX CONCURRENTLY 的工作原理是什么?失败时的回滚机制如何?

  • 并发建索引的阶段与锁
  • 与普通建索引的差异
  • 失败与清理机制

背景:普通 CREATE INDEX 持有 SHARE 锁(阻止写入,允许读),大表建索引长时间阻塞 DML;CREATE INDEX CONCURRENTLY(CIC)把建索引改为"不阻塞写"的多阶段流程(写继续、索引最终一致)。工作原理(两阶段/三扫描):第一阶段——开启事务,对表做全表扫描构建索引(期间持有低级别锁(SHARE UPDATE EXCLUSIVE,与写兼容)),同时记录"并发变更"(增量变更会补入);第二阶段——等待"并发事务完成"(等待所有可能影响索引的事务提交/回滚:两次等待快照),再做一次"合并扫描"(把第一阶段之后的变更合并进索引(将新插入的元组加入索引));第三阶段——提交(索引可见)。整体:CIC 通过"快照 + 变更合并 + 等待并发事务"保证最终索引包含所有已提交数据。细节:其一,锁——CIC 分阶段持有 SHARE UPDATE EXCLUSIVE(不阻塞读写),仅在提交瞬间短暂升级;比普通 SHARE 锁更温和;其二,唯一索引并发建——CIC 可用于 UNIQUE 索引,但并发插入的冲突检查机制更复杂(通过"等待冲突事务"处理,重复值错误可能延迟出现);其三,失败与回滚——CIC 失败时"索引不删除、标记为 INVALID"(pg_index.indisvalid=false):建索引的会话崩溃/报错后,无效索引残留在目录中(名字可见、查询不使用),需要手工 DROP INDEX 清理(或再次 CONCURRENTLY 重建前必须删掉无效索引——直接重建会报"已存在");DROP INDEX CONCURRENTLY 同理失败会残留 INVALID 索引(名字带 _ccnew 之类);其四,副作用——CIC 不能在一个事务块内执行(必须 autocommit)、不能与同一表的其他 CIC 并行(表级排他)、索引构建期间崩溃会留下无效索引与"死元组"(vacuum 可清理);其五,性能——CIC 比普通建索引慢(多一次合并扫描与等待),且消耗更多临时空间。

工程实践:大表(生产环境)用 CIC 加索引;先评估磁盘空间(构建索引需要空间)与 IO 负载;失败后 DROP INDEX 清理再重试;监控 pg_stat_progress_create_index(建索引进度视图)观察进度;注意 CIC 期间 autovacuum 与长事务会影响等待阶段。

答题先讲 CIC 的目标(不阻塞 DML)与多阶段流程(扫描构建→等待并发事务→合并变更→提交),再讲锁的温和性(SHARE UPDATE EXCLUSIVE)与唯一索引的特殊性,然后重点讲失败机制(残留下 INVALID 索引、需 DROP 清理、不能事务内执行),最后给工程实践(空间评估、进度监控、失败重试)。

CREATE INDEX CONCURRENTLY idx_orders_uid ON orders (user_id);
-- 失败后清理无效索引
DROP INDEX CONCURRENTLY idx_orders_uid;
-- 监控进度
SELECT phase, blocks_done, blocks_total FROM pg_stat_progress_create_index;
#
★★

50. 唯一索引(UNIQUE INDEX)与唯一约束(UNIQUE CONSTRAINT)的实现差异,本质相同但约束名与索引名分离。

唯一索引(UNIQUE INDEX)与唯一约束(UNIQUE CONSTRAINT)的实现差异是什么?约束名与索引名分离如何理解?

  • 两者的语义等价与实现关系
  • 约束与索引的元数据分离
  • 各库的行为差异

语义等价:唯一约束与唯一索引都保证"列值唯一"(NULL 处理按库规则),底层都用唯一 B 树索引实现;差异在"元数据身份":唯一约束是"约束对象"(出现在 INFORMATION_SCHEMA.TABLE_CONSTRAINTS、pg_constraint,可被外键引用、参与约束管理),唯一索引是"索引对象"(pg_index、无约束语义,外键不能引用它?外键要求引用列上有"唯一约束或主键"——PG 中外键可以引用唯一索引吗:PG 允许外键引用"唯一索引"覆盖的列?PG 要求"非部分唯一索引"即可(PG 允许外键引用唯一索引的列,不要求必须是约束)——严格说 PG 的外键可基于唯一索引;MySQL 外键要求索引(含唯一索引)即可;SQL Server 外键要求唯一索引或主键)。实现关系:创建唯一约束时数据库"自动创建同名的唯一索引"(PG:约束名与索引名相同(可用 USING INDEX 指定已有索引);MySQL:约束名即索引名;SQL Server:约束与索引同名(可改名));创建唯一索引"不会产生约束对象"(无约束元数据、不能被当作约束管理)。

差异维度:其一,约束名与索引名分离——PG 中 ALTER TABLE ADD CONSTRAINT ... UNIQUE 自动建索引(可用 USING INDEX 复用已有唯一索引:ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX idx——约束挂在已有索引上、索引名与约束名可不同);DROP 约束自动删伴随索引(DROP CONSTRAINT 删索引),DROP INDEX 直接删索引(若被约束使用则报错需先删约束);MySQL 中 DROP INDEX 即删约束(一体)、SQL Server 类似(DROP CONSTRAINT 删索引或反向需处理)。其二,行为差异——唯一约束可延迟(DEFERRABLE,PG 中约束可声明延迟、索引不能延迟);唯一约束出现在约束目录(供外键/工具识别)、唯一索引不在;MySQL 中两者几乎无差别(约束即索引);其三,用途——约束语义(外键引用目标、延迟检查、约束命名管理)用 UNIQUE CONSTRAINT;仅"性能/去重工具"(不关心约束语义、需要部分索引/表达式索引变体——约束不支持部分/表达式,唯一索引支持(PG 的部分唯一索引/表达式唯一索引:软删除唯一方案))用 UNIQUE INDEX。结论:需要"约束语义"(可被外键引用、可延迟、约束管理)用 UNIQUE CONSTRAINT(自动建索引);需要"部分/表达式唯一"或"纯索引"用 UNIQUE INDEX;两库迁移注意约束/索引命名联动。

答题先讲语义等价(底层都是唯一 B 树)与身份差异(约束对象 vs 索引对象),再重点讲约束名与索引名的关系(自动伴随创建、PG 的 USING INDEX 复用、DROP 联动、MySQL 一体),最后给选型(约束语义用 CONSTRAINT、部分/表达式唯一用 INDEX)。

-- 唯一约束:自动建同名索引
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);
-- PG:约束挂在已有唯一索引上(名可不同)
CREATE UNIQUE INDEX idx_email ON users (email);
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE USING INDEX idx_email;
-- 部分唯一索引(约束做不到)
CREATE UNIQUE INDEX ux_active ON users (username) WHERE deleted_at IS NULL;
#
★★

51. 索引在 PostgreSQL、MySQL、SQL Server 中的创建语法差异,CREATE INDEX 的并行选项、ONLINE、CONCURRENTLY。

索引创建语法在 PostgreSQL、MySQL、SQL Server 中的差异是什么?并行、ONLINE、CONCURRENTLY 选项如何对应?

  • 三库 CREATE INDEX 语法
  • 在线/并发选项的对应
  • 并行构建选项

基础语法差异:PostgreSQL——CREATE [UNIQUE] INDEX [CONCURRENTLY] 名 ON 表 [USING 方法 (列/表达式)] [WITH (参数)] [INCLUDE (列)] [WHERE 条件];(USING btree/hash/gist/gin/brin、部分索引 WHERE、表达式索引、INCLUDE、TABLESPACE);MySQL——CREATE [UNIQUE] INDEX 名 ON 表 (列 [长度], ...) [ALGORITHM=...] [LOCK=...] [USING BTREE|HASH](索引键可指定前缀长度(col(20))、无表达式索引(8.0 有函数索引语法((expr) 或 col 表达式,8.0.13+))、无部分索引);SQL Server——CREATE [UNIQUE] [CLUSTERED|NONCLUSTERED] INDEX 名 ON 表 (列 [ASC|DESC]) [INCLUDE (列)] [WHERE 过滤] [WITH (ONLINE=ON, SORT_IN_TEMPDB=ON, ...)] [ON 文件组](过滤索引 WHERE、INCLUDE、ONLINE 选项、文件组)。

在线/并发选项:PostgreSQL——CONCURRENTLY(不阻塞 DML;PG 无 ONLINE 关键字);MySQL——ALGORITHM=INPLACE(8.0 加索引默认在线)+ LOCK=NONE(MySQL 无 CONCURRENTLY 关键字);SQL Server——WITH (ONLINE = ON)(2014+ 可重建索引时并发 DML(加新列时有限制));三库在线机制不同但目标一致(构建期间允许并发 DML)。并行选项:PostgreSQL——8.0 无索引并行构建(PG 的并行只用于查询,索引构建单进程;PG 15+ 开始支持并行构建 B 树?PG 目前 CREATE INDEX 不支持并行(社区有讨论,未并入)——严格说 PG 无并行索引构建参数);MySQL——8.0.14+ innodb_parallel_build_index=ON(并行构建二级索引,通过多个线程排序);SQL Server——WITH (MAXDOP = N)(并行索引构建(多 CPU 排序/扫描));对应关系:性能参数 SQL Server 的 MAXDOP ≈ MySQL 的 innodb_parallel_build_index,PG 无并行选项(用更多内存/磁盘)。 其他差异:MySQL 索引键长度限制(767/3072 字节,utf8mb4 前缀)、PG 索引键长度(页大小 1/3 等)、SQL Server 索引键 900 字节(可 1700 8.0+)——大字段索引需前缀/哈希方案;删除语法:DROP INDEX 名(PG/SQL Server)/ DROP INDEX 名 ON 表(MySQL)。

答题先给三库的基础语法骨架(PG 的 USING/部分/INCLUDE、MySQL 的前缀/算法、SQL Server 的聚簇/过滤/INCLUDE),再重点对比在线选项(CONCURRENTLY vs ALGORITHM/LOCK vs ONLINE)与并行选项(无 vs innodb_parallel_build_index vs MAXDOP),最后提索引键长度限制与删除语法差异。

-- PostgreSQL
CREATE INDEX CONCURRENTLY idx ON t (col) WHERE active;
-- MySQL
ALTER TABLE t ADD INDEX idx (col(20)), ALGORITHM=INPLACE, LOCK=NONE;
-- SQL Server
CREATE NONCLUSTERED INDEX idx ON t (col) INCLUDE (other) WHERE active
  WITH (ONLINE = ON, MAXDOP = 4);
#
★★

52. 索引膨胀(Index Bloat)的原因与检测,pgstat、pgstattuple 在 PostgreSQL 中的应用。

索引膨胀(Index Bloat)的原因是什么?pg_stat 与 pgstattuple 在 PostgreSQL 中如何检测?

  • 膨胀的成因(更新、删除、fillfactor)
  • pg_stat_user_indexes 视图与估算
  • pgstattuple 精确检测与重建

成因:PostgreSQL 的 MVCC 使"更新=插入新版本+标记旧版本",索引页内留下死元组(索引不立即回收空间);频繁 UPDATE/DELETE(尤其更新索引列)、未及时 VACUUM、fillfactor 预留不足、批量导入后未重建——导致索引"膨胀"(索引文件大小 >> 实际键数量,页内利用率低、扫描 IO 放大)。检测方法:其一,估算(pgstat 系)——pg_stat_user_indexes 的 idx_scan(使用次数)、pg_index 的 indisvalid;膨胀估算靠 pgstatindex?估算常用查询:索引大小(pg_relation_size(index)) 与"理想大小"(基于 reltuples × 键宽)比较;常用脚本(如 btree_bloat 查询)用 pg_statistic/pg_class 估算每页平均键数;其二,精确(pgstattuple 扩展)——SELECT * FROM pgstattuple('idx_name'):返回 tuple_count、dead_tuple_count(死元组)、free_space、avg_leaf_density(叶子页密度:接近 1 健康、低值膨胀)——精确统计需要扫描索引(大索引慢,但准确);pgstatindex('idx') 给出叶/内部分层统计(leaf_frac、avg_leaf_density 等);其三,直观比较——索引大小 vs 表大小比例、vacuum 后索引大小不缩(VACUUM 不回收索引空间?普通 VACUUM 会清理索引死元组并部分合并页,但空间不归还文件(可复用);REINDEX/VACUUM FULL 才压缩文件)。

处理:REINDEX INDEX 名(重建索引:新文件、压缩)、REINDEX TABLE、REINDEX INDEX CONCURRENTLY(PG 12+ 不阻塞读写)、VACUUM FULL(重建表+索引,锁表)、CLUSTER(按索引重组表);预防——合理 fillfactor(更新频繁的表/索引设 70-90 预留空间)、及时 autovacuum、批量导入后统一 REINDEX、监控膨胀率(定期 pgstattuple 或估算脚本、警报阈值如密度 < 0.7)。注意:pg_stat_user_indexes 不直接给膨胀率(给使用统计),膨胀检测需要"估算脚本或 pgstattuple";监控自动化(pg_stat_statements 结合慢查询、膨胀脚本进巡检)。

答题先讲膨胀成因(MVCC 更新产生死元组、未及时清理、fillfactor 不足),再给两级检测(pg_stat 估算脚本对比大小、pgstattuple/pgstatindex 精确密度与死元组),最后讲处理(REINDEX/CONCURRENTLY、VACUUM FULL、CLUSTER)与预防(fillfactor、autovacuum、监控阈值)。

CREATE EXTENSION pgstattuple;
SELECT * FROM pgstattuple('idx_orders_uid');
-- 密度低说明膨胀
SELECT indexrelid::regclass, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan;
-- 重建(不阻塞读写)
REINDEX INDEX CONCURRENTLY idx_orders_uid;
#
★★

53. 组合索引(Composite Index)的列顺序对查询效率的影响,最左前缀原则在不同数据库中的实现细节。

组合索引(Composite Index)的列顺序对查询效率的影响是什么?最左前缀原则在不同数据库中的实现细节如何?

  • 最左前缀原则
  • 列顺序的选择准则
  • 各库实现差异(索引跳跃扫描等)

组合索引(多列 B 树)按"列顺序逐级排序"(第一列优先、同值再按第二列……),因此"最左前缀"可用性:索引 (a, b, c) 支持查询条件覆盖 a、(a,b)、(a,b,c)(前缀子集)走索引;条件不含 a(如只 b 或 b,c)无法利用索引(需要扫描整树)——除非数据库支持"索引跳跃扫描(skip scan)"。列顺序选择准则:其一,等值条件列放前——等值(=)过滤性强且顺序无关,习惯把"等值列"放前(等值条件下第二列也可做范围/排序);其二,范围条件放后——范围列(>、BETWEEN)放等值列之后(范围列之后的列无法用于过滤,只能过滤等值列);其三,按过滤选择性——高选择性(区分度高)列在前,尽早缩小扫描范围;其四,ORDER BY 利用——列顺序与排序需求一致时可避免排序((a,b) 满足 ORDER BY a,b 与 a 的排序);其五,覆盖查询——需要的列尽量包含在索引中(覆盖索引免回表)。细节:范围条件后的列"不能用于索引过滤但可在索引中用于覆盖";IN 条件按等值处理(多个 IN 是范围扩展)。

各库实现差异:MySQL——最左前缀严格(8.0 的索引跳跃扫描(Skip Scan,8.0.13+)允许"第一列选择性低、第二列等值"时跳跃扫描(如索引 (gender, id),查 WHERE id=100——优化器可跳过 gender 分组扫描,代价是多次范围扫描(性能通常仍不如前缀查询));ICP(索引条件下推,5.6+)在回表前用索引内非前缀列过滤(减少回表);MySQL 没有位图等。PostgreSQL——同样要求最左前缀(无 skip scan,社区版本无此优化(13+ 有提升但非 skip scan));但 PG 有"多列统计(extended statistics,10+)"改善组合列相关性估算;部分索引可"补充"缺失前缀(为 (b) 建部分索引);PG 的索引也可用"表达式/包含列"缓解。SQL Server——有索引跳跃(index skip scan?SQL Server 无 skip scan;但其"索引交集(index intersection)"可组合多个单列索引查询(优化器合并索引结果),等效替代部分组合场景;包含列 INCLUDE 支持覆盖。工程结论:组合索引设计遵循"等值在前、范围在后、高选择性在前、配合 ORDER BY";无法满足前缀的查询——MySQL 试 skip scan/ICP、SQL Server 用索引交集、PG 补建部分索引/单列索引,并用 EXPLAIN 验证实际计划))。

答题先讲组合索引的排序结构与最左前缀可用性(前缀子集、范围列后失效),再列列顺序五条准则(等值前、范围后、选择性、ORDER BY、覆盖),最后讲各库实现细节(MySQL skip scan/ICP、PG 多列统计/部分索引、SQL Server 索引交集/INCLUDE)与 EXPLAIN 验证。

CREATE INDEX idx ON t (a, b, c);
-- 可用:a、(a,b)、(a,b,c);不可用:b、b&c
SELECT * FROM t WHERE a = 1 AND b > 10;   -- a 等值 + b 范围(a 前)
SELECT * FROM t WHERE b = 1;              -- 无法走 idx(无 a)
-- MySQL skip scan 尝试(8.0.13+)
SELECT * FROM t WHERE c = 5;              -- 可能跳过 a 分组扫描
#
★★

54. 表达式索引(Expression Index)的使用场景,LOWER(col)、date_trunc('day', ts)作为索引键。

表达式索引(Expression Index)的使用场景是什么?以 LOWER(col)、date_trunc('day', ts) 为例说明?

  • 表达式索引的语法与原理
  • 函数包裹列导致索引失效的解决
  • 使用限制与注意

背景:普通索引对"函数包裹的列"失效(WHERE LOWER(email) = 'x' 中列被函数包裹,B 树无法按原始列匹配)——表达式索引"把表达式本身作为索引键":CREATE INDEX ON users (LOWER(email));——索引按 LOWER(email) 排序,查询 WHERE LOWER(email) = 'x' 直接命中(优化器识别表达式匹配)。原理:索引键 = 表达式求值结果(存储表达式值、按值排序),查询中的相同表达式被匹配(表达式字面形式需一致:LOWER(email) 与 lower(email) 等价(函数大小写不敏感?函数名解析不区分大小写,等价));实现为"虚拟生成列"(PG 把表达式索引等价于生成列索引)。使用场景:其一,大小写不敏感查询——LOWER(col)(邮箱、用户名唯一:配合唯一表达式索引实现"大小写不敏感唯一");其二,时间粒度——date_trunc('day', ts)(按天分组/过滤的报表查询:WHERE date_trunc('day', ts) = '2024-01-01' 或 ts >= ... AND ts < ...(范围写法通常更优,但既有查询用截断函数时表达式索引兜底);date(ts)(PG 中 date(ts) 取日期)、to_char 前缀;其三,JSON 字段提取——doc->>'key'(JSONB 中常用(但 JSONB 有 GIN 更优),doc->>'name' 等值查询建表达式索引;其四,规范化存储——UPPER(col)、自定义规范化函数(清洗空格);其五,数值转换——CAST 表达式(col::int 的混合类型列)。限制:其一,表达式必须 IMMUTABLE(确定性:LOWER 依赖排序规则?PG 中 LOWER 是 stable?LOWER 是 immutable(按数据库排序规则)?PG 允许 LOWER 建表达式索引(标 IMMUTABLE?LOWER(text) 是 immutable(除非 collation 不确定)——表达式索引要求 IMMUTABLE,volatile(now()、random())不能建);其二,查询必须写"相同表达式"(无法自动匹配语义等价的不同写法:LOWER(email) 与 LOWER(trim(email)) 是不同索引);其三,索引维护成本——每个插入/更新都要计算表达式(与普通列索引同量级);其四,统计与大小——表达式索引的统计基于表达式值(ANALYZE 支持)。工程建议:高频"函数查询"先考虑改写为"原始列范围查询"(ts >= 边界 AND ts < 边界 优于 date_trunc 表达式);确实需要函数匹配时建表达式索引并保证查询写法一致(应用层统一 SQL 模板);配合唯一表达式索引实现不敏感唯一)))。

答题先讲背景(函数包裹列索引失效)与表达式索引原理(表达式作索引键、查询同形匹配),再给四类场景(LOWER 不敏感、date_trunc 日报、JSON 提取、规范化)示例,最后列限制(IMMUTABLE、表达式同形、维护成本)与"优先改写范围查询"的建议。

CREATE INDEX idx_users_lower_email ON users (LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';   -- 走表达式索引
CREATE INDEX idx_orders_day ON orders (date_trunc('day', created_at));
-- 范围写法通常更优(无需表达式索引)
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- 大小写不敏感唯一
CREATE UNIQUE INDEX ux_lower ON users (LOWER(username));
#
★★

55. 部分索引(Partial Index)的价值,WHERE 子句过滤的小集合数据如何用部分索引加速?

部分索引(Partial Index)的价值是什么?WHERE 子句过滤的小集合数据如何用部分索引加速?

  • 部分索引的定义(WHERE 条件)
  • 小集合索引的体积与命中率
  • 典型场景与限制

部分索引(Partial Index,PG/SQL Server 的过滤索引;MySQL 无):CREATE INDEX ... WHERE 条件——只为"满足条件的行"建索引(索引内只有条件子集的行),核心价值:其一,体积小——全表只有少量行满足条件时(如未删除 1% 行、活跃用户 5%),索引只含这些行(体积为全量索引的百分之几),缓存命中率高、扫描快、维护成本低;其二,专注高频查询——"条件集合"正是高频查询的过滤谓词,部分索引 = "为特定谓词定制的小索引";其三,唯一性控制——部分唯一索引(WHERE 条件内唯一:软删除唯一);其四,加速"多数行不满足条件"的过滤——WHERE status='PENDING' AND ... 配 CREATE INDEX ON t (col) WHERE status='PENDING':查询中条件与索引 WHERE 一致时优化器匹配(谓词包含索引条件)。典型场景:软删除(WHERE deleted_at IS NULL 索引——查询"未删除"记录走小索引)、状态机(只索引活跃/待处理状态行)、热点子集(VIP 用户)、时间窗(近期数据)。

匹配与限制:其一,查询谓词必须"包含索引的 WHERE 条件"才被使用(优化器做条件蕴涵判断:查询 WHERE status='PENDING' AND user_id=5 可用该部分索引;查询 WHERE user_id=5(无 status 条件)不一定用——需要查询条件蕴含索引条件?实际是"索引条件蕴含查询谓词的某部分":查询谓词集合需包含索引 WHERE(status='PENDING' 在查询条件中出现)时匹配);其二,PG 中部分索引与普通索引可共存(优化器按代价选择);其三,限制——部分索引不能用于"不满足条件的行"的查询(WHERE status='DONE' 不走它)、外键引用部分唯一索引受限制(PG 外键要求"非部分"?PG 外键可以引用部分唯一索引吗——PG 不允许外键引用部分索引(文档:外键需要非部分唯一索引);其四,维护——插入/更新时判断条件(条件不满足的行不写入索引)、ANALYZE 统计按子集;其五,MySQL 无部分索引(8.0 用生成列技巧模拟)。工程建议:识别"高频谓词 + 小基数集合"组合建部分索引;把常用过滤列纳入部分索引条件;用 EXPLAIN 验证匹配(Index Cond 与 Filter 差异);注意部分索引与统计信息(小集合的统计更准确——ANALYZE 后优化器对子集查询估算更好))。

答题先定义部分索引(带 WHERE 的索引、只含条件行)与三点价值(体积小、专注高频谓词、部分唯一),再给典型场景(软删除、状态机、热点子集),最后讲匹配规则(查询需包含索引条件、外键限制、MySQL 无、EXPLAIN 验证)与工程建议。

-- 软删除场景:只为未删除行建小索引
CREATE INDEX idx_users_active ON users (email) WHERE deleted_at IS NULL;
SELECT * FROM users WHERE deleted_at IS NULL AND email = 'a@b.com';  -- 命中
-- 状态子集
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'PENDING';
-- 部分唯一索引
CREATE UNIQUE INDEX ux_username ON users (username) WHERE deleted_at IS NULL;
#
★★

56. 索引的存储参数(fillfactor)在 B-Tree 上的写入性能影响?

索引的存储参数(fillfactor)在 B-Tree 上的写入性能影响是什么?

  • 索引 fillfactor 的语义
  • 页分裂与随机插入
  • 参数选择与重建

索引 fillfactor:CREATE INDEX ... WITH (fillfactor = 80)——B 树索引页的"初始填充率":80 表示建索引时每页只填 80%(预留 20% 空间给后续插入)。影响机制:B 树插入时若目标页已满则"页分裂"(把键分配到两个页,更新父指针)——页分裂是随机 IO + 索引膨胀的源头,且分裂后页利用率下降;预留空间(fillfactor < 100)让"后续插入的键"直接放入预留位(无需立即分裂),延迟分裂、减少分裂次数。适用场景:其一,随机插入(UUID 主键、随机键)——键分布无规律、插入位置随机,低 fillfactor(60-80)显著减少页分裂(每页有空间容纳新键),代价是索引更大(页数更多、扫描略慢);其二,顺序插入(自增主键)——键追加到页尾,页分裂少(每页填满才分裂、分裂出的新页继续顺序填),fillfactor 100 最优(不浪费空间);其三,混合/更新频繁——UPDATE 索引列产生新版本插入(同页可用 HOT?索引更新不走 HOT),预留空间减少分裂与膨胀(70-90);堆表的 fillfactor 同理(预留空间支持 HOT 原地更新)。影响权衡:fillfactor 低 → 索引体积大(页数多,全扫描与缓存开销略增)、插入分裂少(写入快);fillfactor 高 → 索引紧凑(读快)、随机插入分裂多(写慢、膨胀)。选择建议:主键自增/顺序场景 100(默认);随机键/高频更新场景 70-85(B 树);设置后"只影响新写入的页"(存量页保持原填充率——需要 REINDEX 才按新 fillfactor 重建);监控页分裂与膨胀(pgstattuple 的 avg_leaf_density)调整;8.0 的 autovacuum 与 REINDEX CONCURRENTLY 配合维护。其他参数:PG 索引还有 deduplicate_items(B 树去重重复键,11+ 默认开)、fastupdate(GIN)等。注意:fillfactor 是"空间-写入速度"的折中,不是越大越好也不是越小越好;MySQL 的索引 fillfactor 不可直接设置(InnoDB 的 MERGE_THRESHOLD 控制页合并阈值,8.0 可设)、SQL Server 有 FILLFACTOR 选项(含在线重建 WITH (FILLFACTOR=80, ONLINE=ON))。

答题先定义索引 fillfactor(页初始填充率)与页分裂机制(满页插入分裂的代价),再按场景分析(随机插入低 fillfactor 减少分裂、顺序插入 100 最优、更新频繁预留空间)与权衡(体积 vs 写入速度),最后给选择建议(按写读比与键分布)、REINDEX 重建时机与各库对应(SQL Server FILLFACTOR、MySQL 无直接参数)。

-- 随机键/高频写:预留空间
CREATE INDEX idx_uuid ON t (uuid_col) WITH (fillfactor = 80);
-- 顺序键:默认 100
CREATE INDEX idx_serial ON t (id);
-- 重建按新参数
REINDEX INDEX idx_uuid;  -- 或 ALTER INDEX ... SET (fillfactor)
#
★★

57. BRIN 索引的适用场景,在天然有序的大表上如何用最小元数据加速范围查询,其选择性不足的代价与索引体积收益如何衡量?

BRIN 索引的适用场景是什么?在天然有序的大表上如何用最小元数据加速范围查询?选择性不足的代价与体积收益如何衡量?

  • BRIN 的原理(块范围摘要)
  • 适用条件(物理顺序与查询对齐)
  • 体积收益与选择性代价的权衡

BRIN(Block Range INdex)原理:把表按"物理块范围"(默认每 128 页一组,pages_per_range 参数)分组,每组只存"该块范围内列的最小值与最大值"(页级摘要元数据,每个块范围一行)——索引体积极小(几千页的表只有几十行元数据),查询时先看块范围摘要:范围与查询条件"可能相交"才扫描该块范围内的数据页(条件落在范围外则整块跳过);本质是"按物理顺序的区间过滤"。适用条件:其一,列与物理顺序"强相关"(天然有序:时间戳/自增 ID/日志序号)——插入顺序即值顺序(块范围内值连续,摘要有效);若随机插入(值乱序),每个块范围 min/max 跨度大(几乎覆盖全表),摘要无过滤力(退化全扫);其二,查询模式——大范围扫描(范围查询/聚合按时间窗、>= 或 BETWEEN)、低选择性过滤(过滤掉大部分块):BRIN 适合"宽范围"(块范围粒度粗);点查/高选择性查命中块太少不划算(直接 B 树);其三,超大表——数十亿行,B 树体积与维护成本高,BRIN 体积是 B 树的 1/1000 级(几 MB),适合"时间序日志/流水/传感器数据"。性能:范围查询扫描"候选块范围"(摘要匹配的块)而非全表,过滤度取决于"数据相关性与范围粒度"(pages_per_range 调小更精细但元数据多);写入——BRIN 维护开销极小(只在块范围边界更新摘要,且可延迟合并(autosummarize)),写入吞吐优于 B 树索引(无逐行索引维护);代价:选择性不足——点查(WHERE id = 12345)仍需扫描包含该值的块范围(128 页 ≈ 1MB),若表大且散列则退化全扫;随机键/乱序列上 BRIN 完全无效;无唯一/排序支持(BRIN 不支持唯一约束、不适合 ORDER BY 精确排序(可提供顺序但无精确键))。衡量:收益 = 体积/维护成本节省(超大表、写密集);代价 = 查询必须"范围化"(点查改 B 树或组合);选择矩阵——有序大表 + 范围查询 → BRIN;点查/高选择性 → B 树;两者可共存(BRIN 覆盖范围查询、B 树覆盖点查——双索引)。调优:pages_per_range(默认 128,调小提高精度、调大减元数据)、autosummarize(8.0 自动摘要合并)、与部分索引组合。

答题先讲 BRIN 原理(块范围存 min/max 摘要、体积极小、按摘要跳过块),再讲适用条件(列与物理顺序强相关、宽范围查询、超大表)与不适场景(随机键、点查),然后量化权衡(体积收益 vs 选择性不足代价、写维护轻),最后给调优(pages_per_range/autosummarize)与"BRIN+B 树共存"的建议。

CREATE INDEX idx_log_ts ON logs USING BRIN (created_at);
-- 时间范围查询:按块摘要跳过无关块
SELECT * FROM logs WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- 调优:更细粒度(每 64 页一组)
CREATE INDEX idx_log_ts2 ON logs USING BRIN (created_at) WITH (pages_per_range = 64);
-- 自动摘要
ALTER INDEX idx_log_ts SET (autosummarize = on);
#
★★

58. GIN 索引的典型应用场景?

GIN 索引的典型应用场景是什么?其原理与限制是什么?

  • GIN 的倒排原理
  • 典型场景(数组、JSONB、全文、hstore)
  • 限制与调优(fastupdate、pending list)

GIN(Generalized Inverted Index,倒排索引)原理:为"复合值列"的每个元素建条目(元素 → 包含它的行),适合"列内包含多个值、查询按元素匹配"的场景(等值/包含/重叠)。典型应用:其一,数组列——tags TEXT[]:@>(包含)、&&(重叠)、= ANY 查询走 GIN(CREATE INDEX ON t USING GIN (tags));其二,JSONB——doc @> '{"a":1}'(包含)、? 存在性键、?|、?&、jsonb_path_ops(更小的 GIN 变体,专注路径包含)、全文检索组合;其三,全文检索——tsvector 列的 @@ tsquery(to_tsvector 表达式索引);其四,hstore——@>、? 键值查询;其五,模糊/相似——pg_trgm 扩展的 gin_trgm_ops(LIKE '%x%'、相似度(similarity)——用 trigram 倒排实现模糊匹配);其六,ltree 的 GiST/GIN(路径匹配)。性能特征:查询——元素等值/包含走 GIN 索引查找(元素哈希/树定位),比全表扫描快数量级;写入——GIN 默认"延迟插入"(fastupdate):新元素先进"pending list"(内存/磁盘缓冲),达到阈值(gin_pending_list_limit)或 VACUUM 时批量合并进主索引——批量写友好(比每行更新倒排快),但 pending list 中数据"不走索引"(查询需扫 pending list 合并——小量无感,大量未合并时查询变慢);调优——关闭 fastupdate(写入变慢但查询稳定)、调大 pending list、vacuum 合并;维护——VACUUM 清理死元素、REINDEX 压缩;多键列——GIN 对"多个可索引列"也支持(多列 GIN)。限制:其一,无排序支持(GIN 不提供有序扫描:ORDER BY 列值无法利用(除非用 btree_gin 扩展给标量类型加排序操作符——btree_gin 使 GIN 支持 = 与排序);其二,唯一约束不可用 GIN(GIN 不支持唯一性——唯一需 B 树);其三,B 树比较(范围查询 < > BETWEEN)不适合 GIN(元素匹配语义);其四,NULL——GIN 不索引 NULL(NULL 元素需额外处理)。选型:元素级匹配(包含/存在/重叠)用 GIN;范围/排序/唯一用 B 树;全文用 GIN(tsvector)+ trigram 扩展)。

答题先讲 GIN 的倒排原理(元素→行条目、元素匹配语义),再列五类典型场景(数组 @>、JSONB 包含/存在、全文 tsvector、hstore、pg_trgm 模糊),然后重点讲写入特性(fastupdate/pending list 的延迟合并与查询影响)与限制(无排序/唯一、NULL 不索引、btree_gin 扩展),最后给选型矩阵。

CREATE INDEX idx_tags ON t USING GIN (tags);               -- 数组
CREATE INDEX idx_doc ON t USING GIN (doc);                 -- JSONB
CREATE INDEX idx_tsv ON docs USING GIN (tsv);              -- 全文
CREATE INDEX idx_trgm ON t USING GIN (name gin_trgm_ops);  -- 模糊 LIKE
-- 调优:禁用 fastupdate 换取查询稳定
ALTER INDEX idx_tags SET (fastupdate = off);
#
★★

59. INCLUDE 子句(PostgreSQL 11+)的用途?

PostgreSQL 11+ 的 INCLUDE 子句(包含列索引)的用途是什么?

  • INCLUDE 的语法与语义
  • 覆盖索引与免回表
  • 与普通组合索引的差异

INCLUDE 子句:CREATE INDEX idx ON t (a, b) INCLUDE (c, d);——索引的"键列"是 (a, b)(参与排序与过滤),(c, d) 是"包含列"(只存值、不参与排序/搜索,供覆盖扫描使用)。用途:实现"覆盖索引(covering index)"——查询只需 (a, b, c, d) 时(WHERE a=? AND b=? SELECT c, d),全部数据从索引叶子页取(Index Only Scan),免回表(PostgreSQL 中"索引扫描"→"仅索引扫描"节省堆表 IO,且可见性需 VM(visibility map)支持(否则仍要检查堆表,但通常大部分行可见))。与普通组合索引 (a, b, c, d) 的差异:组合索引的每个列都参与排序结构(影响索引体积与插入维护——键越长每页键越少、比较开销越大),INCLUDE 列"不进排序键"(B 树结构只按键列组织、包含列附加在叶子条目上)——因此 INCLUDE 的收益:其一,覆盖查询能力与 (a, b, c, d) 组合索引相同,但索引更小(键比较只发生在键列)、插入/更新维护成本低(包含列不参与节点分裂比较——实际分裂时包含列跟随键走,但比较只按键);其二,可包含"不能作键的列"(大字段 TEXT/JSONB——B 树键长度限制(页大小 1/3)下大列不能作键,但可作 INCLUDE(如索引 (id) INCLUDE (payload) 实现"按 id 取 payload 免回表",payload 大但只附加);其三,与唯一索引组合(CREATE UNIQUE INDEX ON t (a) INCLUDE (b)——唯一只按 a、b 覆盖附加)。适用场景:高频"窄键 + 固定返回列"查询(列表页 SELECT 固定列 WHERE 索引列)、外键/关联查询的免回表、大字段随小键覆盖。注意:INCLUDE 列不参与 WHERE 过滤/排序(无法用 INCLUDE 列做范围条件——需过滤时它得是键列);PG 11+ 支持、SQL Server 的 INCLUDE 类似(早期支持)、MySQL 无 INCLUDE 语法(用"普通组合索引把列放最后"模拟,仍参与键结构);查看覆盖效果:EXPLAIN 显示 "Index Only Scan"(PG);INCLUDE 列同样占用索引存储(每行一份))。

答题先定义 INCLUDE(键列+包含列:包含列只存值不参与排序搜索),再讲核心用途(覆盖索引免回表 Index Only Scan、可包含大字段、与唯一索引组合),然后对比普通组合索引(体积/维护成本差异、键长度限制),最后列限制(INCLUDE 列不可过滤排序、各库支持差异、EXPLAIN 验证)。

CREATE INDEX idx_orders ON orders (user_id) INCLUDE (amount, status);
-- 覆盖查询:仅索引扫描免回表
SELECT amount, status FROM orders WHERE user_id = 100;
-- 大字段随键覆盖
CREATE INDEX idx_docs ON docs (owner_id) INCLUDE (body);
#
★★

60. MySQL 的 FORCE INDEX、USE INDEX、IGNORE INDEX 的语义差异?

MySQL 的 FORCE INDEX、USE INDEX、IGNORE INDEX 的语义差异是什么?各自的使用场景是什么?

  • 三个索引提示的语义
  • 强制索引 vs 建议索引
  • 使用场景与风险

三个索引提示(Index Hints,写在 FROM 表名后):USE INDEX (idx)——"建议"优化器使用指定索引(优化器可忽略:若估算认为全表扫描或其他索引更优则不用),只是"候选列表";FORCE INDEX (idx)——"强制"优化器使用指定索引(即使估算全表扫描更优,也强制走该索引——比 USE 更强,除非索引无法使用(如条件不匹配)才退化);IGNORE INDEX (idx)——"禁止"使用指定索引(优化器从候选中去掉该索引,允许全表扫描或其他索引)。差异核心:USE 是软建议(候选)、FORCE 是硬要求(代价比较被跳过/加权)、IGNORE 是排除。其他语法:FORCE INDEX FOR ORDER BY/JOIN 等细分(限定用途);8.0 也支持 optimizer hints(/*+ INDEX(t idx) */ 注释式,更精细(可指定扫描方式))。

使用场景:其一,FORCE——优化器"选错索引"时的临时干预(统计信息滞后/索引选择性被低估、列相关性导致错误估算):WHERE 条件本应走组合索引却全表扫描,FORCE 强制修正(配合 EXPLAIN 验证);其二,USE——多索引候选时提示"优先考虑"(优化器仍可自主);其三,IGNORE——排除"误导优化器"的索引(如低选择性索引干扰计划、特定索引引发性能问题)或测试索引影响;其四,优化器提示(8.0 hint)用于更精细控制(JOIN 顺序、扫描方式)。风险与注意:其一,索引提示是"硬编码的捷径"——数据分布/统计变化后提示可能过时(索引被删、选择性变化),提示失效(报错?索引不存在则报错)或计划退化,维护成本高;其二,正确做法——先找根因(ANALYZE 更新统计、优化查询结构、重建索引/调整组合索引),提示只是临时缓解;其三,FORCE 可能导致"本应全表扫描更快"的查询被强制走低效索引(提示掩盖了真实最优);其四,EXPLAIN 验证(提示后的计划是否符合预期);其五,跨库不可移植(PG 无对应(PG 用 enable_* 参数或外部优化)、SQL Server 有 FORCESEEK/FORCESCAN 提示);8.0 的 optimizer hints 覆盖更全。工程规范:生产代码避免长期依赖 FORCE/IGNORE(用统计维护与索引设计解决),提示仅用于"已知统计缺陷"的临时修复并在注释中记录原因与失效条件;每次 DDL(加索引/删索引)后检查提示有效性。

答题先分别定义三个提示(USE 建议可忽略、FORCE 强制除非不可用、IGNORE 排除)与语法位置,再讲使用场景(选错索引时 FORCE 临时干预、多候选 USE、排除干扰 IGNORE)与 8.0 optimizer hints,最后列风险(统计变化后过时、掩盖真实问题、EXPLAIN 验证、不可移植)与规范。

SELECT * FROM orders FORCE INDEX (idx_user) WHERE user_id = 100 AND status = 'PAID';
SELECT * FROM orders USE INDEX (idx_user) WHERE user_id = 100;
SELECT * FROM orders IGNORE INDEX (idx_status) WHERE status = 'PAID';
-- 8.0 optimizer hint
SELECT /*+ INDEX(orders idx_user) */ * FROM orders WHERE user_id = 100;
#
★★

61. PostgreSQL 中哈希索引为何曾被标记为“实验性”?

PostgreSQL 中哈希索引为何曾被标记为"实验性"?其现状如何?

  • 哈希索引的早期缺陷
  • 崩溃安全与 WAL 问题
  • 10+ 的重写与现状

历史:PostgreSQL 的哈希索引(CREATE INDEX ... USING hash)从早期版本就存在,但长期(到 9.x)被文档标注为"实验性/不推荐"("not recommended for production"),原因:其一,崩溃安全缺陷——旧版哈希索引"不记录 WAL"(索引变更不写预写日志):崩溃/崩溃恢复后哈希索引可能损坏(索引内容与表不一致),需要 REINDEX 才能修复(普通 B 树索引有 WAL 支持、崩溃安全);其二,元数据与操作符不完整——没有 pg_opclass 的完整支持、无 ALTER INDEX 完整支持、哈希索引的相关操作(等值查询)在优化器中估算不佳(代价模型不完整);其三,功能局限——只支持等值查询(=),不支持范围与排序;无唯一哈希(旧版无 UNIQUE 支持?9.x 哈希不支持 UNIQUE);其四,维护工具缺失(VACUUM 对哈希的清理不完善)。因此官方建议用 B 树(等值查询性能相当、功能全、崩溃安全)。

现状(10+ 重写):PostgreSQL 10 重写哈希索引——"完整 WAL 支持"(索引变更写 WAL、崩溃安全、参与复制)、支持 UNIQUE、ALTER INDEX、VACUUM 清理、并行构建(10+);文档移除"实验性"标注,哈希索引成为"生产可用"选项。当前定位:等值查询专用——哈希索引对"大键等值查询"有体积优势(哈希键固定大小(4/8 字节),大文本/复合键的哈希索引比 B 树小)、查找复杂度 O(1) 平均(B 树 O(log n));但哈希索引"不支持范围查询/排序/最左前缀"(组合哈希索引无前缀利用)、不支持索引排序扫描(ORDER BY 无法用)、扫描顺序不保证——实际生产仍以 B 树为主(B 树等值查找足够快且功能全),哈希索引用于"极少数等值密集、键很大"的场景(或教学/对比)。结论:实验性标签是历史遗留(WAL 缺失),10+ 已修复;面试点在于"为什么曾经不推荐"(崩溃安全)与"现在为何可用"(WAL 重写)。

答题先讲历史缺陷(无 WAL 崩溃损坏、元数据/维护不完整、仅等值),再讲 10 的重写(WAL 支持、UNIQUE、并行构建、文档去实验性),最后给现状定位(等值专用、体积优势但范围/排序受限,生产仍以 B 树为主)。

#
★★

62. PostgreSQL 中如何查看表的索引信息?\d t 的输出包含哪些内容?

PostgreSQL 中如何查看表的索引信息?\d t 的输出包含哪些内容?

  • \d t 的输出结构
  • 其他查询方式(pg_indexes、pg_index)
  • 索引信息的解读

查看方式:psql 的 \d t(\d 表名)——输出三段:表头(列名、类型、约束/默认值(含 NOT NULL、DEFAULT、生成列))、索引段(Indexes:索引名、UNIQUE/PRIMARY 标记、USING 方法、键列(如 btree (user_id))、部分索引 WHERE 条件、表达式)、外键/约束段(Foreign-key constraints、Check constraints、Triggers);\d+ t 增加大小、描述等。索引段示例:"idx_orders_uid" btree (user_id)、"pk_orders" PRIMARY KEY, btree (id)(主键约束自动索引)、UNIQUE 约束索引带 UNIQUE CONSTRAINT 标记、部分索引显示 WHERE (status = 'PENDING')、表达式索引显示表达式(LOWER((email)::text))。其他查询:SELECT * FROM pg_indexes WHERE tablename='t'(indexname、indexdef(完整 CREATE INDEX 文本——最直接));pg_index 底层(indisunique/indisprimary/indkey 列 OID 数组);information_schema.statistics?PG 用 pg_indexes 即可;查看索引大小 pg_relation_size('idx')、使用统计 pg_stat_user_indexes(idx_scan 等)。解读要点:其一,主键/唯一约束自动伴随索引(\d 中 PRIMARY KEY/UNIQUE 标记);其二,USING 方法(btree/hash/gist/gin/brin)与键列顺序(组合索引顺序即声明顺序——最左前缀依据);其三,部分/表达式/INCLUDE 特征(WHERE/表达式/INCLUDE 显示);其四,无效索引(CIC 失败残留)显示 INVALID(\d 或 pg_index.indisvalid)。工程场景:开发/排查"某列为何没走索引"先 \d 确认索引定义与列序;迁移审计导出所有索引(pg_indexes 的 indexdef 可直接重放)。

答题先讲 \d t 的三段输出(列定义、索引段含 UNIQUE/PRIMARY/USING/键/WHERE/表达式、约束触发器段),再给 pg_indexes(indexdef 完整 DDL)与 pg_index/pg_stat_user_indexes 的补充查询,最后讲解读要点(约束伴随索引、列顺序、部分/表达式特征、INVALID 标记)。

\d orders
-- 或
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';
SELECT indexrelid::regclass, indisunique, indisprimary, indkey
FROM pg_index WHERE indrelid = 'orders'::regclass;
-- 使用统计
SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE relname = 'orders';
#
★★

63. 唯一索引的 NULL 处理(PG 多 NULL、Oracle 多 NULL、SQL Server 单 NULL)?

唯一索引的 NULL 处理在 PostgreSQL、Oracle、SQL Server 中有何差异?如何处理"只允许一个 NULL"或"允许多个 NULL"的需求?

  • 各库唯一索引的 NULL 默认行为
  • 单 NULL 与多 NULL 的模拟
  • 软删除等应用

默认行为:PostgreSQL——唯一索引允许多个 NULL(NULL 互不相等,标准语义);Oracle——唯一索引允许多个 NULL(同标准);SQL Server——唯一索引(UNIQUE INDEX)默认"只允许一个 NULL"(NULL 被当作重复值?SQL Server 中唯一索引只允许一个 NULL(第二个 NULL 报重复——实际 SQL Server 把 NULL 视为一个值参与唯一性:唯一索引中只能有一个 NULL);但 SQL Server 的"唯一约束"(UNIQUE CONSTRAINT)允许多个 NULL(与索引行为不同));MySQL——与 PG 相同允许多个 NULL(唯一索引中 NULL 互不重复);SQLite 类似 SQL Server 单 NULL。差异根源:SQL 标准规定"唯一性中 NULL 互不相等"(多 NULL 合法),PG/Oracle/MySQL 遵循;SQL Server 的唯一索引实现"把 NULL 也当键值参与比较"(行为偏离标准),其唯一约束则遵循标准——同库内两套行为。

业务需求的模拟:需求"非空时唯一、NULL 无限制"(可选邮箱):PG/Oracle/MySQL 默认满足(多 NULL 合法);SQL Server 需"过滤索引"(CREATE UNIQUE INDEX ... WHERE col IS NOT NULL)实现多 NULL。需求"至多一个 NULL"(业务上 NULL 也是"占位"需唯一):PG/Oracle/MySQL 默认不满足(多 NULL 合法),需"表达式唯一索引"——PG:CREATE UNIQUE INDEX ON t (COALESCE(col, '哨兵'))(NULL 归一到哨兵值互相冲突);Oracle:基于函数的唯一索引(CREATE UNIQUE INDEX ON t (NVL(col, 哨兵)));MySQL 8.0:生成列技巧(CASE WHEN col IS NULL THEN 哨兵 END 生成列 + 唯一索引);SQL Server 默认就满足(单 NULL)。应用场景:软删除唯一(deleted_at NULL 唯一、非 NULL 多版本——部分索引 WHERE deleted_at IS NULL 或版本列);可选业务键(手机号/邮箱/税号"未设置时多行 NULL")。注意:唯一"索引"与唯一"约束"在 SQL Server 的行为差异是迁移高频坑;跨库迁移时把"单 NULL"需求显式化(过滤/表达式索引)。

答题先列各库默认行为(PG/Oracle/MySQL 多 NULL、SQL Server 唯一索引单 NULL 而唯一约束多 NULL),再分别给两种需求的模拟方案(多 NULL:SQL Server 过滤索引;单 NULL:PG/Oracle 表达式归一、MySQL 生成列),最后提应用场景与迁移注意。

-- SQL Server:实现"多 NULL"(非空唯一)
CREATE UNIQUE INDEX ux_email ON users(email) WHERE email IS NOT NULL;
-- PostgreSQL:实现"单 NULL"(表达式归一)
CREATE UNIQUE INDEX ux_code ON users (COALESCE(code, ''));
-- MySQL 8.0:生成列实现"单 NULL"
ALTER TABLE users ADD code_norm VARCHAR(20)
  GENERATED ALWAYS AS (CASE WHEN code IS NULL THEN 'NULL-MARK' END) STORED;
CREATE UNIQUE INDEX ux_code ON users (code_norm);
#
★★

64. 索引的 TABLESPACE 选项如何迁移?

索引的 TABLESPACE 选项如何迁移?PostgreSQL 中如何移动索引到其他表空间?

  • TABLESPACE 在索引上的作用
  • ALTER INDEX SET TABLESPACE
  • 迁移注意(锁、空间、重建)

背景:索引可以放在与表不同的表空间(磁盘/存储介质分离:热表 SSD、大索引放慢盘;或按 IO 特性分层),PostgreSQL 中 CREATE INDEX ... TABLESPACE ts、ALTER INDEX idx SET TABLESPACE ts2。迁移(移动索引到其他表空间):ALTER INDEX 名 SET TABLESPACE 新表空间;——物理移动索引文件(复制到新表空间目录并更新 relfilenode 映射,事务内完成);主键/唯一约束的伴随索引也可移动(ALTER TABLE ... ALTER INDEX?用 ALTER INDEX 直接移动(约束索引名可查)或用 ALTER TABLE t SET TABLESPACE 移动整表含索引)。注意事项:其一,锁——SET TABLESPACE 移动索引需要 ACCESS EXCLUSIVE 锁?索引移动持 ACCESS EXCLUSIVE(表级?移动索引本身锁索引与表(share 级?实际是 ACCESS EXCLUSIVE on table?PG 中 ALTER INDEX SET TABLESPACE 获取 ACCESS EXCLUSIVE 锁(阻止读写)——大索引移动期间业务中断(可用 REINDEX CONCURRENTLY 到新表空间替代:CREATE INDEX CONCURRENTLY ... TABLESPACE ts_new 后删旧索引);其二,空间——移动需要目标表空间有足够空间(复制+删除旧文件,峰值双份);其三,速度——大索引移动是 IO 密集操作(低峰执行);其四,默认表空间——未指定时索引与表同表空间(或数据库默认表空间);建表默认索引位置(CREATE TABLE ... TABLESPACE 时索引默认同表空间);其五,事务性——移动失败/回滚不会损坏(PG 事务性 DDL);其六,批量迁移——pg_dump 恢复时按 TABLESPACE 子句重建、或 ALTER TABLE ALL IN TABLESPACE(PG 15+ 支持 ALTER TABLE ALL IN TABLESPACE ... SET TABLESPACE(批量移动表与索引));其七,权限——需目标表空间 CREATE 权限。其他库:MySQL 索引跟随表(无法独立移动索引,表空间按表;8.0 通用表空间可选)、SQL Server 索引可独立指定文件组(CREATE INDEX ... ON [PRIMARY] / ALTER INDEX ... REBUILD WITH (DATA_COMPRESSION) 与文件组移动(DROP+CREATE 或 REBUILD ON 文件组))。工程实践:分层存储(冷热分离)用表空间 + 索引分离;迁移用"CONCURRENTLY 建新索引 + 删旧"避免阻塞;迁移前后核对索引清单(pg_indexes 的 tablespace 字段)))。

答题先讲 TABLESPACE 对索引的作用(存储分层)与 ALTER INDEX SET TABLESPACE 的语法语义(物理移动、事务性),再列注意点(ACCESS EXCLUSIVE 锁、空间双份、IO 密集、批量与权限),最后给"用 CONCURRENTLY 重建替代移动"的无阻塞实践与各库对应(MySQL 随表、SQL Server 文件组)。

CREATE INDEX idx ON t (col) TABLESPACE ssd_ts;
ALTER INDEX idx SET TABLESPACE hdd_ts;      -- 移动(锁表)
-- 无阻塞替代:新表空间并发重建 + 删旧
CREATE INDEX CONCURRENTLY idx_new ON t (col) TABLESPACE ssd_ts;
DROP INDEX CONCURRENTLY idx;
-- 查看索引表空间
SELECT indexname, tablespace FROM pg_indexes WHERE tablename = 't';
#
★★

65. 索引的 WHERE 子句与部分索引的关系?

索引的 WHERE 子句与部分索引的关系是什么?查询如何匹配部分索引?

  • 部分索引的定义(CREATE INDEX ... WHERE)
  • 查询谓词与索引条件的匹配规则
  • 使用注意

关系:部分索引 = "带 WHERE 子句的索引"(CREATE INDEX idx ON t (col) WHERE 条件;SQL Server 称"过滤索引(Filtered Index)";MySQL 无);索引只包含"满足 WHERE 条件的行",WHERE 条件同时定义"索引的适用范围"。匹配规则:查询能否使用部分索引取决于"查询谓词与索引 WHERE 条件的关系"——优化器做条件蕴涵判断:查询谓词集合"蕴含"索引的 WHERE 条件(即满足查询条件的行必然也满足索引条件)时可用;等价说法:索引条件"被查询条件覆盖"。例:索引 WHERE status='PENDING';查询 WHERE status='PENDING' AND user_id=5 → 可用(查询条件包含索引条件);查询 WHERE status IN ('PENDING','DONE') AND user_id=5 → 不可用(部分行不满足索引条件——优化器会拒绝);查询 WHERE user_id=5(无 status)→ 不可用(无法确定行在索引内)。匹配实现:优化器把索引的 WHERE 条件与查询谓词做逻辑比较(PG 通过 predtest(predicate test)模块判定蕴含),判定成功才生成"使用该部分索引"的计划(Index Cond 显示查询条件、Filter 可能仍显示)。使用注意:其一,条件写法需可比较(等值/范围/AND 组合易判定;OR 复杂条件判定困难);其二,部分索引与普通索引可共存(同一列两个索引:一个全量一个部分,优化器按代价与适用性选择);其三,部分索引上的"唯一"是部分唯一(软删除);其四,部分索引的统计(ANALYZE 按子集,统计更精准);其五,外键不能引用部分唯一索引;其六,EXPLAIN 验证(部分索引出现于 Index Scan/Index Only Scan);其七,维护——插入/更新时判断行是否满足索引条件(满足才写索引),条件表达式的计算成本随行数;更新使行"从满足变不满足"(删除索引条目)或反之(新增条目)都正确维护。设计要点:部分索引适合"高频谓词固定的小集合"(状态机、软删除、时间窗);写 WHERE 条件时保持"查询中最常见的过滤形式",使查询条件自然蕴含索引条件。

答题先定义部分索引(WHERE 子句限定索引行集)与匹配规则(查询谓词蕴含索引条件才可用、条件部分覆盖不可用、优化器判定蕴含),再给匹配示例(含可用/不可用情形)与 EXPLAIN 验证,最后列注意(统计、外键限制、维护成本、与普通索引共存)。

CREATE INDEX idx_pending ON orders (user_id) WHERE status = 'PENDING';
-- 可用(查询包含索引条件)
SELECT * FROM orders WHERE status = 'PENDING' AND user_id = 5;
-- 不可用(IN 超出索引条件)
SELECT * FROM orders WHERE status IN ('PENDING','DONE') AND user_id = 5;
-- EXPLAIN 验证
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING' AND user_id = 5;
#
★★

66. 索引的并行创建(PARALLEL)参数?

索引的并行创建(PARALLEL)参数是什么?各数据库如何实现并行建索引?

  • 并行建索引的机制与收益
  • 各库参数(MAXDOP、innodb_parallel_build_index、PG 无)
  • 并行构建的注意

并行建索引:用多个 CPU 并行扫描/排序构建索引,缩短大索引的创建时间(IO 与 CPU 并行化)。各库实现:SQL Server——WITH (MAXDOP = N)(CREATE INDEX ... WITH (ONLINE=ON, MAXDOP=4):并行扫描排序构建;MAXDOP=0 用全部可用度、也可在 server 级 max degree of parallelism 限制);PostgreSQL——目前"不支持并行索引构建"(CREATE INDEX 单进程;并行仅用于查询(并行扫描/聚合),索引构建虽有并行扫描的讨论但未实现(PG 15 仍无)——因此 PG 无 PARALLEL 参数(用更多 work_mem/磁盘排序优化);MySQL——8.0.14+ 的 innodb_parallel_build_index=ON(二级索引构建时并行(扫描+排序),主键/全文/空间索引不支持;参数会话/全局可设);Oracle——PARALLEL 子句(CREATE INDEX ... PARALLEL 4:并行 DDL,从库也可并行维护)。机制:扫描阶段并行(多线程读表数据)、排序阶段并行(多路归并)、写入阶段(建索引树);收益受限于表大小/CPU 数与 IO 带宽(IO 瓶颈时并行收益有限))。

注意与权衡:其一,并行构建消耗 CPU 与临时空间(排序临时文件多路)——大表建索引期间对在线业务有资源竞争(低峰执行、限制并行度);其二,与在线选项组合——SQL Server 的 ONLINE+MAXDOP、MySQL 的 INPLACE+并行(8.0.14+ 并行构建与在线 DDL 可同时)、PG 的 CONCURRENTLY 单进程(无并行但可并发读写);其三,并行度选择——按 CPU 核数与 IO 能力设置(MAXDOP 建议 ≤ 核数、MySQL 并行由参数控制),过高导致上下文切换与临时文件放大;其四,监控——SQL Server 的 sys.dm_exec_requests 并行度、MySQL 的 status、PG 的 pg_stat_progress_create_index(进度);其五,默认行为——MySQL 8.0 默认 OFF(需要显式开启)、SQL Server 默认 MAXDOP 由服务器配置(索引操作可用默认)、Oracle 默认串行(需 PARALLEL 提示);其六,从库/复制——并行构建在主库完成后复制正常(复制不并行)。工程实践:大表建索引优先"在线+并行"组合(SQL Server ONLINE+MAXDOP、MySQL INPLACE+innodb_parallel_build_index、PG CONCURRENTLY(无并行));评估资源(CPU/IO/临时空间);监控进度与失败重试。

答题先讲并行建索引的机制(并行扫描/排序/构建)与收益,再列四库支持(SQL Server MAXDOP、MySQL 8.0.14 innodb_parallel_build_index、Oracle PARALLEL、PG 不支持无参数),然后讲权衡(CPU/临时空间竞争、与在线选项组合、并行度选择、监控),最后给工程实践。

-- SQL Server
CREATE INDEX idx ON big (col) WITH (ONLINE = ON, MAXDOP = 4);
-- MySQL 8.0.14+
SET GLOBAL innodb_parallel_build_index = ON;
ALTER TABLE big ADD INDEX idx (col), ALGORITHM=INPLACE, LOCK=NONE;
-- Oracle
CREATE INDEX idx ON big (col) PARALLEL 4;
-- PostgreSQL:无并行参数(CONCURRENTLY 单进程)
CREATE INDEX CONCURRENTLY idx ON big (col);
#
★★

67. PostgreSQL 的 search_path 机制,未限定对象名时如何按顺序在多个 schema 中查找?

PostgreSQL 的 search_path 机制是什么?未限定对象名时如何按顺序在多个 schema 中查找?

  • search_path 的结构与默认值
  • 查找顺序与命中规则
  • 修改与影响范围

search_path 是"schema 搜索路径"(GUC 参数,会话级可改):值是逗号分隔的 schema 名列表,定义"未限定对象名(表/视图/函数/类型等)的解析顺序"。机制:解析未限定名时按 search_path 从左到右依次查找,第一个命中的 schema 即用(同名的后续 schema 不再考虑);全部找不到则报错(relation/function does not exist)。默认值 "$user", public:先找与当前用户名同名的 schema(存在才参与;不存在静默跳过),再找 public;pg_catalog 与 pg_temp 总是"隐式优先"(pg_catalog 在 search_path 之前被查(即使不在列表中)——实际语义:pg_catalog 永远排在查找最前;pg_temp 在会话有临时表时优先(临时表掩盖同名永久表))。影响范围:其一,对象解析(SELECT/DDL 的表名、函数名、类型名);其二,对象创建落点——CREATE TABLE 未限定时建在 search_path 第一个 schema;其三,函数体内执行语句的解析也走"函数创建时的 search_path 或函数内 SET search_path"。修改方式:SET search_path TO app, public;(会话级,当前事务内有效或会话内);ALTER ROLE ... SET search_path(用户级默认);ALTER DATABASE ... SET search_path(库级默认);SET LOCAL(事务内临时);pg_db_role_setting 存储库/角色级配置。查询:SHOW search_path / current_schemas(true)。常见场景:多 schema 应用切换默认 schema(SET search_path TO app)、恢复被污染路径(SET search_path TO pg_catalog, public)、安全加固(去掉 public 只留业务 schema)。注意:search_path 中写 "$user" 字面量(引号);不存在的 schema 名在路径中静默跳过;search_path 影响"同名对象劫持"(存在安全风险);跨会话默认继承库/角色设置。

答题先定义 search_path(schema 搜索路径 GUC、逗号列表)与解析机制(从左到右第一个命中、全失败报错、pg_catalog/pg_temp 隐式优先、默认 "$user", public),再讲影响范围(解析、创建落点、函数内)与修改方式(SET/ALTER ROLE/ALTER DATABASE/LOCAL),最后给查询与典型场景。

SHOW search_path;                 -- '"$user", public'
SET search_path TO app, public;   -- 会话级
ALTER ROLE app_user SET search_path TO app, public;  -- 用户级默认
-- 解析示例:search_path = app, public 时 users 优先 app.users
SELECT * FROM users;
#
★★

68. search_path 的安全风险,恶意用户创建同名的函数/视图污染搜索路径的攻击(search_path 攻击)。

search_path 的安全风险是什么?恶意用户创建同名函数/视图污染搜索路径的攻击(search_path 攻击)原理与防护是什么?

  • search_path 攻击的原理
  • 攻击面(SECURITY DEFINER、函数内解析)
  • 防护措施

原理:search_path 决定"未限定对象名"的解析——若攻击者能把自己 schema 插入目标的解析路径(或利用默认 public 的宽松权限),并创建"与合法对象同名"的恶意对象(函数/视图/表),当合法代码(尤其 SECURITY DEFINER 函数、高权进程)用未限定名引用时,解析到恶意对象——攻击者代码以"受害上下文权限"执行(提权/数据窃取/破坏)。典型攻击链:其一,public 污染——PostgreSQL 15 前 public schema 默认允许任意用户 CREATE:恶意用户建 public.func() 与系统函数同名(或与高权函数同名),受害者在 search_path 含 public 时调用 func() 解析到恶意版本(如把 pg_catalog 的运算函数屏蔽?pg_catalog 优先所以系统函数难劫持;更常见劫持"非系统"的常用函数/类型名(如自定义函数名)或未限定表引用(public.users 恶意表掩盖真实 users));其二,SECURITY DEFINER 放大——高权定义的函数以"定义者权限"执行,若其体内未限定引用被劫持(恶意函数在定义者权限下执行)→ 提权(可读写高权对象、甚至创建超级用户);SQL 函数/PL 函数体内"未限定对象"按"定义者会话的 search_path"解析(函数创建时的 search_path 快照?函数体内的解析发生在调用时、使用调用者的 search_path(或函数内 SET)——因此攻击者可通过设置自己会话的 search_path 操纵函数内解析);其三,类型/操作符劫持(同名类型导致隐式转换链改变)。

防护:其一,收紧 public——REVOKE CREATE ON SCHEMA public FROM PUBLIC(PG 15 默认已做);业务 schema 只授可信角色;其二,固定函数内的 search_path——SECURITY DEFINER 函数定义时用 SET search_path = pg_catalog, app(函数内执行解析固定、不受调用者污染):CREATE FUNCTION f() ... SECURITY DEFINER SET search_path = pg_catalog, app;;其三,全限定引用——代码与函数体内用 schema.table 显式限定(不依赖路径);其四,schema 隔离——应用专属 schema、无同名对象冲突面;其五,最小权限——函数定义者用低权专用角色而非超级用户;其六,审计——pg_depend/同名对象扫描、定期检查 public 与可疑对象(CREATE 权限与陌生函数)。最佳实践组合:public 只读 + 业务 schema + 函数内固定 search_path + 全限定名。

答题先讲攻击原理(未限定名按 search_path 解析、恶意同名对象被解析、SECURITY DEFINER 提权放大、public 宽松权限是温床),再讲两条攻击链(public 污染、函数体内解析被调用者路径操纵),最后给防护清单(REVOKE public CREATE、函数 SET search_path、全限定、低权定义者、审计)。

-- 防护一:收紧 public
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- 防护二:固定函数内解析路径(SECURITY DEFINER 函数)
CREATE FUNCTION read_secret() RETURNS text
LANGUAGE sql SECURITY DEFINER
SET search_path = pg_catalog, app AS $$
  SELECT secret FROM app.secret_table $$;
-- 防护三:全限定引用
SELECT * FROM app.users;
#
★★

69. 如何通过 pg_db_role_setting 设置默认 search_path?如何通过 ALTER ROLE/USER 设置?

如何通过 pg_db_role_setting 设置默认 search_path?如何通过 ALTER ROLE/ALTER USER 设置?

  • ALTER ROLE SET search_path
  • pg_db_role_setting 的结构
  • 设置优先级(库/角色/会话)

设置方式:其一,ALTER ROLE(ALTER USER 同义)——ALTER ROLE app_user SET search_path TO app, public;(角色级默认:该角色新建会话生效(GUC 默认值),不影响现有会话);ALTER ROLE ALL SET search_path ...(所有角色);其二,ALTER DATABASE——ALTER DATABASE db SET search_path TO ...(库级默认);其三,ALTER ROLE ... IN DATABASE——ALTER ROLE app_user IN DATABASE db SET search_path TO ...(角色+库交叉作用域:仅该角色连该库时生效);其四,会话内 SET search_path(当前会话/事务,覆盖默认)。存储机制:pg_db_role_setting 表存储"数据库/角色级别的 GUC 设置"(setdatabase/setrole 字段(0 表示"所有库/所有角色")与 setconfig 文本数组)——ALTER ROLE/DATABASE SET 命令实际写入该表;ALTER ROLE ALL SET 即 setrole=0 的行。优先级(从低到高):编译/配置默认 < ALTER DATABASE(库级)< ALTER ROLE(角色级)< ALTER ROLE IN DATABASE(交叉)< 会话 SET(含 ALTER ROLE SET 启动时应用?会话开始加载角色+库默认,然后用户可 SET 覆盖);实际加载顺序:启动时按"角色级/库级"合并加载到会话(pg_db_role_setting 中匹配(database, role)的配置)。

查询与清除:SELECT * FROM pg_db_role_setting;(查看)、SELECT rolconfig FROM pg_roles WHERE rolname='app_user'(角色配置);清除:ALTER ROLE app_user RESET search_path;(重置为默认)、ALTER DATABASE db RESET search_path;。应用场景:应用账号固定 schema 路径(SET search_path TO app, public 持久化)、安全(把 public 移出默认路径)、多租户(每租户角色不同路径)。注意:ALTER ROLE SET 只影响"未来会话";psql 中 \drds 查看库/角色设置;ALTER ROLE 的 SET 语句本身不受 search_path 影响(固定解析);函数内的 SET 子句(CREATE FUNCTION ... SET search_path)是函数级,与角色级独立。

答题先给四层设置语法(ALTER ROLE/ALTER DATABASE/IN DATABASE 交叉/会话 SET)与 pg_db_role_setting 的存储结构(setdatabase/setrole/setconfig),再讲优先级(库<角色<交叉<会话)与加载时机(新会话),最后给查询(pg_db_role_setting、\drds)、清除(RESET)与应用场景。

ALTER ROLE app_user SET search_path TO app, public;
ALTER DATABASE appdb SET search_path TO app;
ALTER ROLE app_user IN DATABASE appdb SET search_path TO app;  -- 交叉
-- 查看
SELECT * FROM pg_db_role_setting;
SELECT rolname, rolconfig FROM pg_roles WHERE rolname = 'app_user';
-- 清除
ALTER ROLE app_user RESET search_path;
#
★★

70. MySQL 的数据库(Database)与 PostgreSQL 的 Schema 切换语法差异?USE db vs SET search_path?

MySQL 的数据库(Database)与 PostgreSQL 的 Schema 切换语法差异是什么?USE db 与 SET search_path 有何不同?

  • 切换语法的差异
  • 未限定名的解析机制
  • 跨命名空间引用

切换语法:MySQL——USE dbname;(会话级切换"默认数据库",之后未限定表名解析到该库;跨库引用写 dbname.tablename);PostgreSQL——SET search_path TO schema1, schema2;(设置"schema 搜索路径列表",未限定名按列表顺序查找;跨 schema 引用写 schema.table)。差异本质:MySQL 的 USE 是"设置单一默认库"(一个当前库),PG 的 search_path 是"多 schema 有序列表"(可多个候选、按序回退);对应关系:USE app ≈ SET search_path TO app(单一);但 PG 可"app, public"多级回退(MySQL 无(只一个默认库,跨库必须全限定))。语义差异:其一,解析范围——MySQL 未限定名只在"当前库"找(找不到报错,不会自动去其他库),PG 在 search_path 列表内找(第一个命中);其二,对象创建——MySQL 未限定建表落在当前库,PG 落在 search_path 第一个 schema;其三,跨命名空间引用——MySQL db.t(跨库引用常见、同一实例可跨库 JOIN),PG schema.t(同库跨 schema 天然可引用;跨 Database 需 dblink/foreign data wrapper——Database 隔离更严格);其四,粒度——MySQL 的"库"即命名空间(无 schema 层),PG 的 schema 是库内分组(一个库多个 schema);其五,会话影响——USE 只切默认库、不改变其他库的权限/可见性,SET search_path 同理只影响解析顺序(权限按对象授权);其六,函数/过程内——MySQL 存储过程内 USE 影响会话(8.0 中过程内 USE 不允许?过程内可写 USE?MySQL 存储程序中不允许 USE(报错),需用 dbname.tablename 全限定;PG 函数内 SET search_path 可固定解析(安全));其七,连接串——客户端连接参数(MySQL db 参数、PG options='-c search_path=...')可预置。工程建议:MySQL 多库应用用"全限定 db.table"(避免 USE 切换的隐式状态);PG 多 schema 应用固定 search_path(SET search_path TO app)并安全加固;迁移(MySQL→PG)时"库"通常映射为"schema"(库名→schema 名、db.table → schema.table)。

答题先给两语法(USE db vs SET search_path 列表)与核心差异(单默认库 vs 有序列表回退),再从解析范围、建表落点、跨命名空间引用(db.t vs schema.t 与跨库限制)、粒度、过程内限制六点对比,最后给工程建议与迁移映射。

-- MySQL:切换默认库
USE app;
SELECT * FROM users;           -- app.users
SELECT * FROM other_db.users;  -- 跨库全限定
-- PostgreSQL:搜索路径
SET search_path TO app, public;
SELECT * FROM users;           -- 先找 app.users,没有再找 public.users
#
★★

71. Oracle 的同义词(Synonym)与 PostgreSQL 的 search_path 对比?

Oracle 的同义词(Synonym)与 PostgreSQL 的 search_path 有何对比?各自解决什么问题?

  • 同义词的定义与类型
  • 与 search_path 的机制差异
  • 各自适用场景

Oracle 同义词(Synonym):给对象(表/视图/序列/过程/类型)创建"别名"——CREATE [PUBLIC] SYNONYM app_users FOR scott.users;:私有同义词(默认,schema 内)+ 公共同义词(PUBLIC,全库可见);解析顺序:未限定的对象引用先查"自身 schema 的对象",再无则查"私有同义词",再查"公共同义词"(解析链);作用:解耦(应用只依赖同义词名,底层对象迁移/改名/换 schema 时改同义词定义即可)、跨 schema 简化(app_users 指向 scott.users 免写 schema 前缀)、多环境映射(开发/生产指向不同底层对象)。PostgreSQL 无同义词对象:等效能力由 search_path 提供——未限定名按 search_path 列表解析(多个 schema 依次查找),实现"同名的多 schema 回退/切换";但机制不同:同义词是"一对一别名映射"(任意名→任意目标对象,含重命名),search_path 是"目录顺序查找"(名字必须相同、靠 schema 顺序选中);同义词可把"名字不同"的对象隐藏(app_users 而非 users),search_path 做不到改名(要改名需视图/别名)。

对比:其一,命名自由度——Oracle 同义词可任意别名(含重命名语义),PG search_path 要求同名(不同名用视图模拟:CREATE VIEW app_users AS SELECT * FROM scott.users;或 foreign table);其二,作用域——同义词是对象(可授权、可 DROP、PUBLIC 全局),search_path 是会话配置(无对象身份);其三,解析链——Oracle 的固定链(本 schema→私有同义词→公共同义词)vs PG 的可配置列表(search_path 顺序可变、可插 pg_catalog 等);其四,权限——同义词使用需要底层对象权限(同义词本身不授权),search_path 解析后按对象授权,等效;其五,迁移——Oracle 迁移 PG:同义词改写为"视图/全限定名/统一 search_path 命名";PG 迁移 Oracle:search_path 语义用同义词或默认 schema 表达。工程结论:需要"别名/重命名解耦"用同义词(Oracle);需要"多 schema 切换回退"用 search_path(PG);PG 中模拟同义词的"改名"需求用视图(只读转发)或物化视图;跨库代码避免依赖同义词(用全限定名+视图层)。

答题先讲 Oracle 同义词(别名对象、私有/公共、解析链、解耦价值),再对比 PG 的 search_path(同名目录顺序查找、会话配置非对象),从命名自由度、作用域、解析链、权限、迁移五维度对比,最后给"解耦用同义词/视图、切换用 search_path"的结论与迁移映射。

-- Oracle:同义词
CREATE SYNONYM app_users FOR scott.users;
CREATE PUBLIC SYNONYM global_seq FOR scott.seq1;
SELECT * FROM app_users;    -- 解析到 scott.users
-- PostgreSQL 等效:视图转发(改名解耦)
CREATE VIEW app_users AS SELECT * FROM scott_users;
-- 或 search_path 同名切换
SET search_path TO scott;
SELECT * FROM users;
#
★★

72. search_path 在函数体内的可见性?

search_path 在函数体内的可见性是什么?函数内未限定对象如何解析?

  • 函数体内解析的 search_path 来源
  • 调用者路径 vs 定义者路径
  • 函数级 SET search_path

机制:函数体内"未限定对象名"(表、函数、类型)的解析——使用"函数被调用时当前会话的 search_path"?正确语义:PL/pgSQL 与 SQL 函数体内语句的解析发生在"函数执行时",使用"函数定义时的环境"还是"调用时的环境":PostgreSQL 中函数体内的解析使用"函数创建/定义时的 search_path 快照"?——实际规则:函数体内未限定引用按"执行时的 search_path"解析,而"执行时"的 search_path 是"调用者会话的 search_path(除非函数声明了 SET search_path)"——因此攻击者可用自己的 search_path 操纵函数体内解析(这是 search_path 攻击能放大到 SECURITY DEFINER 的原因)。更精确:SQL 函数(LANGUAGE SQL)在解析时会把"函数内语句的未限定名"解析为"创建时 search_path 下的对象 OID"(SQL 函数体是纯 SQL,创建时被解析并存储引用 OID?SQL 函数在首次执行/创建时绑定对象(catalog 引用),与调用者 search_path 无关(对象在创建时解析绑定);PL/pgSQL 函数不同——plpgsql 函数体内语句"每次执行时解析"(动态解析),使用"当前会话的 search_path"(调用者的),除非函数内 SET search_path 固定。因此:PL/pgSQL 函数受调用者 search_path 影响(易被攻击)、LANGUAGE SQL 函数绑定创建时对象(较安全但结构变更需重建?SQL 函数内联时重新解析)。实践结论(官方安全建议):SECURITY DEFINER 函数必须固定 search_path(CREATE FUNCTION ... SET search_path = pg_catalog, pg_temp, app)——函数体内解析不依赖调用者;普通函数建议同样固定(防御深度);函数内可用 SET LOCAL search_path 或 ALTER FUNCTION ... SET。可见性细节:其一,函数级 SET 子句(CREATE FUNCTION f() ... SET search_path = ...)在函数执行期间生效(进入函数时设置、退出恢复)——覆盖调用者路径;其二,pg_temp 建议显式加入(若函数需要临时表)或保持系统隐式;其三,函数调用其他函数时(嵌套),被调函数自己的 SET 覆盖;其四,视图同理——视图的查询在"创建时解析并存储 OID"(视图不受调用者 search_path 影响(存储的是已解析引用)),与 SQL 函数类似;其五,动态 SQL(EXECUTE '...')内未限定名按"执行时 search_path"解析(完全运行时,最需固定))。

答题先讲核心机制(PL/pgSQL 函数体内语句按执行时的会话 search_path 动态解析,SQL 函数/视图创建时绑定对象 OID),再讲安全影响(调用者可操纵 plpgsql 函数解析、SECURITY DEFINER 提权放大)与固定方式(函数级 SET search_path),最后列细节(嵌套调用、动态 SQL 完全运行时、视图已绑定)与官方最佳实践。

-- 推荐:函数内固定解析路径
CREATE FUNCTION safe_proc() RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, app AS $$
BEGIN
  PERFORM * FROM users;   -- 固定解析到 app.users(不随调用者变化)
END $$;
-- 视图:创建时绑定对象(不受调用者路径影响)
CREATE VIEW v_users AS SELECT * FROM app.users;
#
★★

73. 公共 schema(public)的安全最佳实践?

公共 schema(public)的安全最佳实践是什么?如何加固 public schema?

  • public 的默认权限风险
  • 加固措施清单
  • 15 的默认变化

风险背景:public schema 默认存在且是 search_path 落点;PostgreSQL 15 之前"PUBLIC 角色对 public schema 拥有 CREATE 权限"——任何能连接数据库的用户都能在 public 建对象(表/函数/视图),攻击者可创建"同名对象"劫持未限定引用(search_path 攻击),也能污染对象命名空间;15 起默认"仅数据库 owner 可在 public 建对象"(行为变更),但存量库与旧版本仍需手动加固。最佳实践清单:其一,回收 CREATE 权限——REVOKE CREATE ON SCHEMA public FROM PUBLIC;(15 前必须;15 后默认已回收,可复查);保留 SELECT 等读权限(public 中系统默认对象(无)与业务早期对象);其二,业务对象移出 public——应用表/函数建在业务 schema(CREATE SCHEMA app AUTHORIZATION app_owner),public 只留必要对象或清空;其三,收紧 search_path——SET search_path TO app(去掉 public),ALTER ROLE 设置默认,函数内固定;其四,权限最小化——public 的 USAGE 权限按需授予(GRANT USAGE ON SCHEMA public TO 需要的角色;默认 PUBLIC 有 USAGE(可访问 public 中对象)——若 public 无敏感对象可保留);其五,对象级防护——public 中已有对象检查授权(\dp 查看)、敏感表移到业务 schema;其六,审计——定期检查 pg_namespace 权限(nspacl)、public 中的对象清单(SELECT * FROM pg_class c JOIN pg_namespace n ... WHERE nspname='public');其七,新库模板——template1 预置"已加固"(回收权限+建业务 schema),所有新建库继承;其八,迁移工具注意——Flyway/Liquibase 默认建表在 public(search_path 默认),脚本显式 schema 或设置路径。结论:核心原则——"public 不承载业务对象、不授 CREATE、应用走专属 schema";15 前的库升级/加固时执行 REVOKE 并全量审计。

答题先讲风险(15 前 PUBLIC 可 CREATE 导致 search_path 攻击与命名空间污染、15 默认收紧),再给加固清单(REVOKE CREATE、业务 schema、search_path 收紧、权限最小化、审计、template1 模板),最后总结核心原则。

-- 加固
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
CREATE SCHEMA app AUTHORIZATION app_owner;
SET search_path TO app;
ALTER ROLE app_user SET search_path TO app;
-- 审计 public 对象
SELECT c.relname, c.relkind FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind IN ('r','v','f');
#
★★

74. 如何为函数固定 search_path?SET search_path FROM CURRENT?

如何为函数固定 search_path?SET search_path FROM CURRENT 的语义是什么?

  • 函数级 SET search_path
  • FROM CURRENT 语法
  • 固定路径的时机与作用

固定方式:其一,CREATE FUNCTION ... SET search_path = pg_catalog, app;——函数定义时声明"函数执行期间的 search_path"(进入函数生效、退出恢复),函数内未限定名按此解析(不随调用者变化)——SECURITY DEFINER 函数的官方安全要求;可用 ALTER FUNCTION f() SET search_path = ... 事后添加;其二,SET search_path FROM CURRENT——在"创建函数时"把"当前会话的 search_path"快照写入函数定义(CREATE FUNCTION f() ... SET search_path FROM CURRENT;):避免手工写死路径字符串、保证与创建环境一致(如统一在"已设置好路径的迁移会话"中创建函数);语义:FROM CURRENT 仅在建函数时取当前值(不是"每次执行时取当前"——函数定义固定了快照值),与显式写 SET search_path = 'app, public' 等价(一个是快照一个是字面量)。注意:SET 子句也可用于视图/物化视图(CREATE VIEW ... SET search_path)?PG 支持视图的 SET(13+?视图可带 SET(PG 14+ 支持 CREATE VIEW ... SET));存储过程同样支持。

作用与细节:其一,防止 search_path 攻击(函数内解析固定,调用者无法通过改自己路径操纵函数内对象引用);其二,函数内显式包含 pg_catalog(系统对象优先解析,避免业务同名对象覆盖系统函数/类型);其三,嵌套函数——被调函数若有自己的 SET 则覆盖(各函数独立);其四,动态 SQL——函数内 EXECUTE 语句的解析同样受函数级 SET 影响(固定路径也保护动态 SQL 的未限定引用);其五,移除——ALTER FUNCTION f() RESET search_path(恢复默认行为);查看——\df+ 显示 SET 子句、pg_proc.proconfig。实践:新建 SECURITY DEFINER/涉及敏感表函数统一加 SET search_path(含 pg_catalog 与业务 schema);迁移脚本(Flyway)在"配置好路径的会话"中用 FROM CURRENT 创建,减少手写不一致。

答题先给两种固定方式(显式 SET search_path 字面量与 ALTER FUNCTION 追加),再重点讲 SET search_path FROM CURRENT 的语义(建函数时快照当前会话路径、等价显式字面量),然后讲作用(防攻击、含 pg_catalog、动态 SQL 受保护)与细节(嵌套覆盖、RESET、proconfig 查看),最后给实践建议。

-- 显式固定
CREATE FUNCTION f() RETURNS int
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, app AS $$ ... $$;
-- 快照当前会话路径
SET search_path TO app, public;
CREATE FUNCTION f2() RETURNS int
LANGUAGE sql SET search_path FROM CURRENT AS $$ SELECT 1 $$;
-- 事后修改/清除
ALTER FUNCTION f() SET search_path = pg_catalog, app;
ALTER FUNCTION f() RESET search_path;
#
★★

75. DROP TRIGGER 的语法与权限要求?

DROP TRIGGER 的语法与权限要求是什么?有哪些注意事项?

  • DROP TRIGGER 语法(PG/MySQL)
  • 权限要求
  • 级联与 IF EXISTS

语法:PostgreSQL——DROP TRIGGER [IF EXISTS] 触发器名 ON 表名 [CASCADE|RESTRICT];(必须指定触发表);MySQL——DROP TRIGGER [IF EXISTS] [库名.]触发器名;(MySQL 触发器名在 schema 内唯一、无需指定表(8.0 也可 DROP TRIGGER 表名.触发器名);Oracle——DROP TRIGGER 名;SQL Server——DROP TRIGGER 名(或 ON 对象,2016+ 支持 IF EXISTS)。权限要求:PostgreSQL——需"表的 owner"(或超级用户)——PG 中 DROP TRIGGER 的权限是"触发表的属主"(不是触发器创建者);MySQL——需要 TRIGGER 权限(对表所在库);Oracle——需触发器属主或 DROP ANY TRIGGER;SQL Server——需 ALTER 权限。注意事项:其一,IF EXISTS——幂等(不存在时 NOTICE,脚本友好);其二,CASCADE——PG 中依赖触发器的对象(如触发器函数?函数与触发器是分离对象:DROP TRIGGER 不影响函数(函数可复用);CASCADE 用于"依赖触发器的事件触发器/其他"?实际触发器被事件触发器(ddl_command_end 审计)依赖时 RESTRICT 拒绝;一般无需 CASCADE);其三,触发函数保留——删除触发器不删除其触发器函数(函数独立,可复用或单独 DROP FUNCTION);其四,与约束触发器——CONSTRAINT TRIGGER 同样 DROP TRIGGER 删除;其五,事务性——PG 可回滚、MySQL 隐式提交;其六,影响评估——删除触发器前确认业务依赖(审计/联动逻辑消失),查看 \d 表 或 information_schema.triggers 确认当前触发器清单;其七,重建——删除后需重新 CREATE TRIGGER(无 CREATE OR REPLACE TRIGGER(PG 中 CREATE OR REPLACE TRIGGER 不存在?PG 有 CREATE OR REPLACE TRIGGER(13+ 支持 OR REPLACE?PG 无 OR REPLACE TRIGGER——需 DROP+CREATE;PG 13 无;实际 PG 不支持 OR REPLACE TRIGGER)——用 DROP+CREATE 或先检查存在(IF EXISTS 在 DROP 侧);MySQL 8.0 支持 CREATE OR REPLACE TRIGGER(8.0.29?MySQL 8.0 的 CREATE TRIGGER 不支持 OR REPLACE(Oracle 支持));其八,级联对象(依赖触发器的复制过滤配置等)在移除前确认)))。

答题先给三库语法(PG 需 ON 表、MySQL 库级名、Oracle/SQL Server),再讲权限(PG 表 owner、MySQL TRIGGER 权限),最后列注意事项(IF EXISTS、函数保留、无 OR REPLACE、事务性、影响评估)。

-- PostgreSQL
DROP TRIGGER IF EXISTS trg_norm ON users;
-- MySQL
DROP TRIGGER IF EXISTS app.trg_norm;
-- 查看现有触发器
SELECT tgname FROM pg_trigger WHERE tgrelid = 'users'::regclass AND NOT tgisinternal;
SHOW TRIGGERS FROM app LIKE 'users';
#
★★

76. 事件调度(Event Scheduler)能否替代业务侧的 Cron 作业?

数据库事件调度(Event Scheduler)能否替代业务侧的 Cron 作业?各自的适用场景是什么?

  • 库内调度的能力边界
  • Cron/任务平台的对比
  • 选型建议

能替代"一部分":库内事件调度(MySQL EVENT、pg_cron、SQL Server Agent Job)适合"数据库内维护类任务"——清理过期数据、刷新物化视图、归档、统计汇总、定期校验,优点:与库同进程/同机房、无需额外部署、语法直接执行 SQL/存储过程、事务上下文完整(可在事务中执行)、权限沿用数据库账号;替代 Cron 的典型场景:DELETE 过期日志(EVENT EVERY 1 DAY)、REFRESH 物化视图(pg_cron)、重建索引/ANALYZE(Agent Job)。不能替代/不合适的场景:其一,跨系统任务——调用外部 API、写消息队列、与其他服务交互(库内调度执行 HTTP/外部命令受限(MySQL EVENT 不能直接调外部命令(无 sys_exec;PG 的 pg_cron 可执行 shell?pg_cron 只执行 SQL;外部命令需 plpython 或扩展,安全风险大)));其二,复杂编排——多步骤、依赖、失败重试、条件分支、分布式 DAG(业务工作流),应使用任务平台(Airflow、XXL-JOB、Temporal 等);其三,高可用与可靠性——库内调度随实例存活(实例故障期间任务不执行、无跨实例容灾(MySQL 事件不复制、pg_cron 单节点));Cron/平台可多机部署、补偿机制、错过补跑;其四,可观测性——任务平台有日志、告警、重试、权限审计,库内调度需要自建(执行记录表);其五,资源隔离——重任务跑在业务库实例内会竞争数据库资源(IO/CPU/连接),独立调度更安全;其六,环境差异——开发/生产脚本一致性、配置管理(Cron 用 IaC 管理,EVENT 在库里难以版本化)。

选型建议:纯数据库内、低频、轻量的维护任务——用库内调度(EVENT/pg_cron/Agent Job)足够(减少组件);需要外部交互、复杂编排、跨环境可靠执行——用任务平台(Cron/工作流引擎),数据库只提供被调用的 SQL/过程;混合——平台调度调用存储过程/脚本。工程注意:库内调度任务必须有"执行日志+失败告警"(自建表记录、监控事件执行状态)、幂等设计(任务可重跑)、时区与夏令时确认、避免在高峰期运行重任务。

答题先讲库内调度的适用面(库内维护任务:清理/刷新/归档,优点同库执行),再列不能替代的六类场景(跨系统、复杂编排、高可用、可观测、资源隔离、版本化),最后给选型结论(纯库内用 EVENT/pg_cron、复杂用任务平台)与工程注意(日志告警、幂等、时区)。

#
★★

77. 序列的回滚(setval、ALTER SEQUENCE RESTART)在数据修复时的使用风险?

序列的回滚(setval、ALTER SEQUENCE RESTART)在数据修复时的使用风险是什么?

  • setval/RESTART 的语义
  • 回拨的冲突与单调性破坏
  • 安全修复流程

场景:数据修复(误删数据回滚、手动插入、迁移对齐)时常需要"把序列调整到合适位置"——setval(seq, n)(下一次 nextval 返回 n+1)与 ALTER SEQUENCE RESTART WITH n(下次返回 n)用于"前移/后移"序列。风险:其一,回拨冲突(重复键)——把序列拨回"小于现有最大 id"时,后续 nextval 可能生成"已存在的 id"→ 主键冲突(唯一约束报错)甚至覆盖语义(若无唯一约束则数据错乱);尤其并发场景——回拨时其他会话可能已持有更大的值(回拨后与"已分配未使用"值重叠);其二,破坏单调性——序列"只增"的语义被破坏后,后续生成的 id 与既有数据混序(外部排序、日志顺序、增量同步按 id 的假设失效);其三,并发竞态——setval 全局立即生效,回拨期间并发的 nextval 拿到"重复候选值"(回拨+并发插入=重复键事故);其四,回拨丢失窗口——回拨后"已分配但未使用"的值被重新生成(该值可能已在其他会话使用(事务未提交)——冲突)。安全修复流程:其一,先查清当前最大 id 与序列状态(SELECT MAX(id)、SELECT last_value FROM seq):对齐目标 = max(id)(或 max+1);其二,只"前移"(把序列设置到 ≥ 当前最大值)——setval(seq, MAX(id))(is_called=true 下一次返回 max+1)是安全操作;"回拨"(< max(id))仅在"确认无任何已存在/将存在的 id 冲突"时进行(通常绝不做);其三,修复在低峰/停写窗口执行(避免并发 nextval);其四,修复后验证(插入测试行确认 id 不重复);其五,批量修复场景(删除了大量行想把序列拨回)——务必评估"已分配未提交"与"外部依赖 id 顺序"(日志/缓存/消息队列中可能有旧 id);其六,记录变更(setval 操作审计);其七,多实例/复制——逻辑复制拓扑中各库序列独立(需在订阅端同步 setval);MySQL 中 ALTER TABLE t AUTO_INCREMENT = n 同样风险(且 8.0 中 AUTO_INCREMENT < max(id) 会被忽略或调整?MySQL 8.0 会忽略小于当前值的设置并告警)。结论:序列修复"只前移、不回拨";必须回拨时在停写窗口 + 确认无冲突 + 验证。

答题先定义 setval/RESTART 语义与数据修复场景(迁移对齐=前移安全),再列四类风险(回拨重复键、破坏单调性、并发竞态、已分配值复用),最后给安全流程(查 max、只前移、低峰停写、验证、审计、复制注意)。

-- 安全:前移到当前最大 id
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));
-- 危险:回拨(可能生成重复 id)
SELECT setval('users_id_seq', 100);   -- 若已有 id=150,下一次 101 可用但…已存在 150 前的值若被删过则无冲突,否则冲突
-- RESTART 等价
ALTER SEQUENCE users_id_seq RESTART WITH 101;
#
★★

78. 触发器与约束检查的执行顺序(BEFORE 触发器 vs CHECK 约束、DEFERRABLE 约束的推迟时机)

触发器与约束检查的执行顺序是什么?BEFORE 触发器与 CHECK 约束谁先执行?DEFERRABLE 约束的推迟时机如何?

  • 执行链:BEFORE → 约束 → 写入 → AFTER
  • DEFERRABLE 的推迟点
  • 不同库的顺序差异

标准执行链(行级):BEFORE 行触发器(可改 NEW)→ 行写入(期间做约束检查)→ AFTER 行触发器 → 语句级 AFTER 触发器。PostgreSQL 与 MySQL 的差异在约束检查时机:MySQL 中 NOT NULL、CHECK(8.0.16+)、唯一、外键等约束都在行写入时立即检查(BEFORE 之后、AFTER 之前),违反即报错;PostgreSQL 中 NOT NULL、CHECK、唯一/主键在行写入过程中检查(因此 BEFORE 对 NEW 的修改"先于约束"、约束看到修改后的值——BEFORE 可以把不合法的值改合法、也可以通过改坏触发约束报错;BEFORE 无法"绕过"约束,改坏的值照样被拒绝,但返回 NULL 可阻止该行、从而跳过该行的约束检查),非延迟外键在语句结束时统一检查——行级 AFTER 触发器同样在语句结束时触发(先于语句级 AFTER 触发器),因此 AFTER 中看到的行未必已通过外键检查(语句随后可能因外键违反而失败)。结论:"BEFORE 先于约束"两库都成立;"约束先于 AFTER"在 MySQL 与 PG 的 NOT NULL/CHECK/唯一上成立,PG 的外键检查在语句结束阶段。

DEFERRABLE 约束的推迟:可延迟约束(DEFERRABLE INITIALLY DEFERRED 或 SET CONSTRAINTS ALL DEFERRED)把检查"推迟到事务提交时"(而不是语句结束)——此时"BEFORE/AFTER 触发器都已执行、行已写入",提交前统一检查;影响顺序:延迟约束的检查点从"语句结束"移到"事务提交",因此"触发器执行完(含 AFTER)后约束才检查"——AFTER 触发器中访问的数据可能"暂时违反约束"(延迟窗口内);非延迟约束中 NOT NULL/CHECK/唯一/主键在行写入时即已检查(AFTER 触发器执行时已通过),外键在语句结束时统一检查(与行级 AFTER 触发器同处语句结束阶段)。注意:PG 中延迟约束与触发器顺序——AFTER 触发器先于延迟约束检查(提交时)执行。触发器内的 DML 触发嵌套触发;约束触发器(CONSTRAINT TRIGGER)本身是"延迟检查的触发器"。

答题先给标准执行链(BEFORE → 行写入(含行级约束检查)→ AFTER → 语句级 AFTER)与"BEFORE 先于 CHECK、可改 NEW、约束看到修改后值"的语义,再讲 MySQL 的立即检查 vs PG 的"行级约束写入时检查、外键语句结束检查"差异,最后讲 DEFERRABLE 的推迟时机(提交时检查、晚于 AFTER 触发器、延迟窗口内可暂时违反)。

-- BEFORE 触发器改 NEW 后 CHECK 检查修改后的值
CREATE FUNCTION fix() RETURNS trigger AS $$
BEGIN NEW.qty := GREATEST(NEW.qty, 0); RETURN NEW; END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg BEFORE INSERT ON t FOR EACH ROW EXECUTE FUNCTION fix();
-- 延迟约束:提交时检查(AFTER 触发器先执行)
ALTER TABLE t ADD CONSTRAINT fk_x FOREIGN KEY (x) REFERENCES p(id)
  DEFERRABLE INITIALLY DEFERRED;
#
★★

79. PostgreSQL 的 B-tree/GiST/GIN/BRIN 索引类型分别适合什么查询(精确/范围/全文/空间/时序)

PostgreSQL 的 B-tree、GiST、GIN、BRIN 索引类型分别适合什么查询?精确、范围、全文、空间、时序场景如何选型?

  • 四种索引类型的原理
  • 各自适合的查询形态
  • 选型矩阵

四种索引类型与适用查询:B-tree——默认类型(有序树):适合等值(=)与范围(<、>、BETWEEN)、排序(ORDER BY)、前缀匹配(LIKE 'abc%')、唯一约束、外键——通用 OLTP 主力;不支持"元素包含"(数组/JSON 的 @>)与模糊中缀(%x%);GiST(广义搜索树)——通用树(平衡、支持自定义距离/包含语义):适合空间数据(PostGIS 的 geometry 的 ST_Within/ST_DWithin 距离查询)、范围类型(tsrange 的 && 重叠)、相似/近邻(ORDER BY 距离 LIMIT k 的 KNN 搜索)、ltree 路径、模糊(pg_trgm 的 GiST 变体)——"多维/空间/邻近"查询;GIN(倒排)——元素级索引:适合数组(@>、&&)、JSONB(包含/存在)、全文检索(tsvector @@ tsquery)、hstore、pg_trgm 的 %(相似度)——"元素/词项匹配";BRIN(块范围摘要)——min/max 摘要:适合"物理有序大表 + 范围查询"(时间序日志、自增流水:按时间窗扫描)——极小的元数据代价换大范围过滤。

选型矩阵(按查询形态):精确等值/范围/排序/唯一——B-tree(唯一选择,GIN/GiST 无排序与唯一);元素包含/存在(数组/JSONB)——GIN;全文搜索——GIN(tsvector)或 GiST(pg_trgm);空间/地理——GiST(PostGIS);区间重叠/排除约束——GiST(范围类型的 EXCLUDE);模糊(LIKE %x%、相似度)——pg_trgm(GIN 或 GiST);时序大表范围扫描——BRIN(+ B-tree 辅助点查);多类型组合——同一列可建多种索引(B-tree + GIN 共存)。实现差异:GiST 无唯一/排序(用 btree_gist 扩展可部分支持排序)、GIN 无排序(btree_gin 可支持 =);写入维护成本:B-tree 每行维护、GiST 中、GIN 有 pending list(延迟合并)、BRIN 最小。工程建议:按"查询形态 + 数据特征"选择,默认 B-tree;元素/全文/空间/时序分别用 GIN/GiST/BRIN;用 EXPLAIN 验证索引使用;复合场景多索引共存。

答题先逐个讲四种索引的原理与适用查询(B-tree 等值范围排序、GiST 空间范围重叠近邻、GIN 元素包含全文、BRIN 有序大表范围),再给按查询形态的选型矩阵(含 pg_trgm 模糊、排除约束、组合共存),最后给工程建议(默认 B-tree、EXPLAIN 验证)。

CREATE INDEX idx_btree ON t (id);                    -- 默认 B-tree
CREATE INDEX idx_gin ON t USING GIN (tags);          -- 数组元素
CREATE INDEX idx_gin_tsv ON docs USING GIN (tsv);    -- 全文
CREATE INDEX idx_gist ON geo USING GIST (geom);      -- 空间
CREATE INDEX idx_brin ON logs USING BRIN (ts);       -- 时序大表
CREATE INDEX idx_trgm ON t USING GIN (name gin_trgm_ops);  -- 模糊
#

80. ALTER SEQUENCE 的常用子句有哪些?

PostgreSQL 中 ALTER SEQUENCE 的常用子句有哪些?各自的作用是什么?

  • ALTER SEQUENCE 的参数子句
  • 修改增量/起点/缓存
  • RESTART 与 OWNED BY

常用子句(ALTER SEQUENCE 名 子句;,可组合多个):INCREMENT BY n——修改步长(正数递增、负数递减);MINVALUE/MAXVALUE——修改取值范围;START WITH n——修改"下次 nextval 的起点"(实际生效依赖 RESTART?START WITH 只影响"RESTART 后的起点"——直接 ALTER SEQUENCE ... START WITH 不改变当前值,需配合 RESTART);RESTART [WITH n]——把序列"重置为 START 值或指定值"(下次 nextval 从该值开始(RESTART WITH n 等价 setval(seq, n, false)——下次返回 n));CACHE n——修改内存预分配数量;CYCLE/NO CYCLE——允许/禁止到达 MAXVALUE 后循环;OWNED BY 表.列——绑定序列到列(随表/列删除而删除);RENAME TO——改名;SET SCHEMA——移动 schema。注意:RESTART 与 START WITH 的区别(RESTART 立即重置当前值;START WITH 只改"将来 RESTART 的默认起点");INCREMENT 修改后按新步长继续(不回退);CACHE 修改对已预分配的内存值影响(新段生效);OWNED BY 绑定后 DROP 表自动删序列(序列成为表附属);查看当前设置:\d seq 或 SELECT * FROM 序列名(last_value、start_value、increment 等)。使用场景:修复序列(RESTART 对齐)、调整步长(多实例错开(与 MySQL 的 auto_increment_increment 类似:PG 多写场景用 INCREMENT 2/offset 1 与 2/2 错开?PG 序列无 offset 概念(用 INCREMENT 与 START 组合实现错开))、性能调整(CACHE)。注意:ALTER SEQUENCE 立即全局生效(并发会话的后续 nextval 使用新参数);权限——需序列 owner 或 USAGE/UPDATE 权限(ALTER 需要 owner);事务性(PG 中可回滚)。MySQL 等价:ALTER TABLE t AUTO_INCREMENT = n(重置自增起点,非独立序列))。

答题先列常用子句及作用(INCREMENT/MINMAX/START/RESTART/CACHE/CYCLE/OWNED BY/RENAME/SET SCHEMA),重点辨析 RESTART(立即重置)与 START WITH(改默认起点)的区别与"修改步长做多实例错开"的应用,最后给查看方式(\d/序列表)与权限/事务性细节。

ALTER SEQUENCE users_id_seq INCREMENT BY 5 RESTART WITH 100;
ALTER SEQUENCE users_id_seq CACHE 50;
ALTER SEQUENCE users_id_seq OWNED BY users.id;   -- 绑定列
ALTER SEQUENCE users_id_seq RENAME TO user_seq;
-- 查看
SELECT last_value, increment_by, cache_size FROM users_id_seq;
#

81. CREATE SEQUENCE seq START 1 INCREMENT 1 的完整语法含义?

CREATE SEQUENCE seq START 1 INCREMENT 1 的完整语法含义是什么?各参数如何理解?

  • CREATE SEQUENCE 的参数
  • START/INCREMENT/MINMAX/CACHE 语义
  • 序列的默认行为

语法:CREATE SEQUENCE seq START 1 INCREMENT 1 [MINVALUE 1] [MAXVALUE 上限] [CACHE 1] [NO CYCLE] [OWNED BY NONE];——创建名为 seq 的计数器:START 1——序列起点(首次 nextval 返回 1);INCREMENT 1——每次 nextval 递增 1(正数递增、负数递减);MINVALUE/MAXVALUE——取值范围(默认 1 与类型上限(bigint 9223372036854775807));CACHE——内存预分配个数(默认 1:每次持久化);CYCLE/NO CYCLE——到达最大值后是否循环(默认 NO CYCLE,到达上限报错);OWNED BY——绑定表列(默认 NONE 独立序列)。完整语义:nextval() 首次返回 START(1),之后每次返回"上一次值 + INCREMENT"(1、2、3…);值不随事务回滚;CACHE > 1 时内存预分配一段(崩溃丢段);序列是独立 schema 对象(可被多表 DEFAULT nextval 引用、可授权)。常用变体:SERIAL 简写(自动创建序列);CREATE SEQUENCE ... INCREMENT BY 2 START 1(奇数序列)或 START 2(偶数序列)——多实例错开(两个实例分别奇数/偶数序列不冲突);CYCLE 用于"循环编号"(周序号、短流水);MAXVALUE 限制(业务封顶)。注意:START 与 RESTART 的关系(START 是初始定义、RESTART 是运行期重置);INCREMENT 负数(递减序列:START 100 INCREMENT -1);MINVALUE 与 INCREMENT 方向匹配(递增序列 MINVALUE 是下限、递减序列 MINVALUE 是"到达即结束"的下界?递减序列到达 MINVALUE 后(NO CYCLE)报错);序列默认类型 bigint(最大值大);查看:\ds、SELECT * FROM seq。

答题先给完整语法与逐参数解释(START/INCREMENT/MINMAX/CACHE/CYCLE/OWNED BY 及默认值),再讲执行语义(首次返回 START、每步 +INCREMENT、不回滚),最后给常用变体(SERIAL、步长错开、CYCLE 循环、递减序列)与注意(START vs RESTART、递增递减与 MIN/MAX 方向)。

CREATE SEQUENCE seq START 1 INCREMENT 1;
SELECT nextval('seq'), nextval('seq'), nextval('seq');  -- 1, 2, 3
-- 奇数/偶数错开(多实例)
CREATE SEQUENCE odd_seq START 1 INCREMENT 2;
CREATE SEQUENCE even_seq START 2 INCREMENT 2;
-- 循环序列(到 9999 后回到 1)
CREATE SEQUENCE short_seq START 1 INCREMENT 1 MAXVALUE 9999 CYCLE;
#

82. 如何查询当前 schema 的所有序列?

如何查询当前 schema 的所有序列?各查询方式的差异是什么?

  • psql 的 \ds
  • information_schema.sequences
  • pg_class 的 relkind 'S'

查询方式:其一,psql——\ds(当前 search_path 的序列)、\ds .(全部 schema);其二,information_schema.sequences——SELECT sequence_schema, sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public';(标准视图:名称、schema、数据类型、start/increment/max/min/cycle/cache 等参数);其三,pg_catalog——SELECT n.nspname AS schema, c.relname AS name FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'S' AND n.nspname NOT IN ('pg_catalog','information_schema');(relkind 'S' 是序列;信息全(可 JOIN pg_sequences 拿 last_value、pg_sequence 拿参数));其四,pg_sequences 视图(PG 10+)——SELECT * FROM pg_sequences WHERE schemaname='public'(last_value、start_value、increment_by、min_value、max_value、cache_size、cycle,还区分"是否为数据类型的标识序列(data_type)")。差异:\ds 交互最方便;information_schema.sequences 可移植(跨库脚本)但不含 last_value 且性能一般;pg_class.relkind='S' 权威底层(可 JOIN 大小 pg_relation_size、owner pg_get_userbyid、依赖 pg_depend(OWNED BY 绑定));pg_sequences 最实用(参数+last_value 一屏)。补充:查看"与表列绑定的序列"(OWNED BY):SELECT ... FROM pg_depend JOIN pg_class;检查序列大小(pg_relation_size);序列权限(pg_sequences 无权限列,用 \dp 或 information_schema.sequence_privileges)。使用场景:审计(哪些序列存在)、修复(定位序列名做 setval)、迁移(导出序列清单重建(pg_dump 会处理))、清理(孤儿序列(无 OWNED BY 且无引用)识别)。注意:"当前 schema"——\ds 默认 search_path 范围、SQL 查询显式过滤 schema(避免系统序列(pg_catalog 中无业务序列,但 information_schema 的查询会包含系统序列?过滤 pg_catalog 即可))。

答题给四种方式(\ds、information_schema.sequences、pg_class relkind 'S'、pg_sequences)与各自差异(交互/可移植/底层权威/参数+last_value),再讲补充(OWNED BY 依赖、大小、权限)与应用场景(审计、修复、清理孤儿序列)。

\ds
SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public';
SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'S' AND n.nspname = 'public';
SELECT * FROM pg_sequences WHERE schemaname = 'public';
-- 孤儿序列(无绑定列)
SELECT c.relname FROM pg_class c WHERE c.relkind='S'
  AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = c.oid AND d.deptype='a');
#

83. 序列的 cache 参数对性能的影响如何?

序列的 cache 参数对性能的影响如何?如何设置与权衡?

  • CACHE 的机制(内存预分配)
  • 性能收益与空洞/重启风险
  • 设置建议

机制:CREATE SEQUENCE ... CACHE n:序列在内存中预分配 n 个值(首次访问时从磁盘持久化"段边界"并缓存 n 个值,内存内分配无需每次写盘/锁竞争),用尽后再申请下一段(更新持久值)。性能影响:CACHE 1(默认)——每次 nextval 都更新持久状态(WAL 写入、页更新),高并发 nextval 有锁/日志开销(PG 中 CACHE 1 时每次仍要写 WAL?PG 序列递增写 WAL:CACHE 1 每次 nextval 写 WAL(性能开销大);CACHE n 只在段边界写 WAL(n 次 nextval 只写一次)——写入放大显著降低);并发吞吐:大 CACHE 下内存原子递增(无锁)、并发能力高(测试中 CACHE 100+ 的 nextval 吞吐比 CACHE 1 高一个数量级);代价:其一,空洞——崩溃/重启丢失"未使用的预分配值"(从段边界继续,空洞 = 未用段);其二,跨节点共享——多实例共享同一序列(PG 无内建多主)不适用(各实例独立 CACHE 段会冲突?序列是单实例对象,多实例需外部方案);其三,取值"跳变"——外部观察到的值不再严格连续(每次段申请跳 n);其四,setval/重置后段内已分配值作废。权衡:并发写入吞吐敏感 + 可容忍空洞 → CACHE 大值(100-1000);必须最小空洞/慢速场景 → CACHE 1(或小值);中间(常见 OLTP 主键)→ CACHE 10-100(PG 默认 SERIAL 的序列 CACHE 1;生产高并发常调大)。注意:CACHE 值过大(如百万)导致"段边界间距大"(崩溃丢百万值、跳变明显);Oracle 的 CACHE 默认 20、MySQL 无 CACHE(AUTO_INCREMENT 由自增锁机制控制,8.0 交错模式性能好);修改用 ALTER SEQUENCE ... CACHE n(新段生效);监控——高并发 nextval 场景用 CACHE 提升吞吐是低成本优化。

答题先讲机制(内存预分配段、段边界写 WAL),再量化性能影响(CACHE 1 每次写 WAL vs CACHE n 段边界写、无锁内存递增、吞吐数量级差异),然后列代价(空洞、跳变、崩溃丢段),最后给设置建议(并发敏感调大、严格连续用小值)与各库对比。

CREATE SEQUENCE orders_seq CACHE 100;
ALTER SEQUENCE orders_seq CACHE 500;
-- 查看
SELECT cache_size FROM pg_sequences WHERE sequencename = 'orders_seq';
#

84. 生成列的两种类型(VIRTUAL vs STORED)各自的存储成本?

生成列的两种类型(VIRTUAL vs STORED)各自的存储成本是什么?如何选择?

  • VIRTUAL 的存储与计算
  • STORED 的存储与计算
  • 选择依据

生成列两种模式(MySQL 支持两者、PG 仅 STORED、SQL Server PERSISTED 对应 STORED):VIRTUAL(虚拟生成列)——不占用表存储:值在"查询时计算"(每次读取行时对表达式求值);写入时不物化(不影响表行大小);可建二级索引(MySQL 8.0 起 VIRTUAL 列可索引——索引会物化索引键,但表行不存);不可作分区键(MySQL 8.0 中 VIRTUAL 不能分区)、部分约束场景受限。STORED(存储生成列)——占用表存储:写入(INSERT/UPDATE)时计算并物化到行内(行大小增加、更新变慢、表文件变大),读取零计算(直接取值);可索引、可分区键、可外键引用(MySQL 中 STORED 支持更全);PG 只有 STORED(PG 12+)。存储成本对比:VIRTUAL 行内 0 字节(计算代价在查询时、CPU 换空间);STORED 行内 = 表达式结果大小(空间换查询 CPU,且写入路径多一次计算与 IO(行变宽可能触发行迁移/页分裂))。性能影响:VIRTUAL——查询每行计算(表达式昂贵时查询慢、可被索引缓解(索引预计算键值))、写入快(无物化);STORED——写入慢(每行计算+存储)、查询快(列值直接读、可用覆盖/过滤/排序、可建索引高效)。选择依据:其一,表达式计算成本——昂贵表达式(JSON 提取、字符串处理)且读多写少 → STORED(避免每查询重复算);廉价表达式(简单算术)→ VIRTUAL(省空间);其二,写入频率——高频写入表用 VIRTUAL(写入路径轻);低频写高频读 → STORED;其三,功能需求——需要分区键/外键引用/约束参与(MySQL)→ STORED;仅查询便捷/索引需求 → VIRTUAL(+ 索引);其四,空间敏感——宽表/大表用 VIRTUAL(不膨胀行);其五,PG 无选择(STORED 唯一);迁移注意:MySQL VIRTUAL 迁 PG 需改 STORED(或表达式索引替代——PG 中"VIRTUAL 语义"用表达式索引近似(查询时计算+索引键物化))。工程建议:默认按"读写比与表达式成本"决策:简单表达式+写多 → VIRTUAL(MySQL);复杂表达式+读多 → STORED;PG 统一 STORED,需"免存储"时用表达式索引。

答题先定义两种模式的存储行为(VIRTUAL 行内不存查询时算、STORED 写入时物化行内存储),再对比写入/查询性能与功能差异(索引、分区键、外键),然后给选择依据(表达式成本、读写频率、功能需求、空间敏感)与 PG 仅有 STORED 的迁移注意,最后给工程建议。

-- MySQL:VIRTUAL(不占行存储,可索引)
ALTER TABLE t ADD price_x_qty DECIMAL(12,2)
  GENERATED ALWAYS AS (price * qty) VIRTUAL;
-- STORED(写入物化,占存储,可分区键)
ALTER TABLE t ADD total DECIMAL(12,2)
  GENERATED ALWAYS AS (price * qty) STORED;
-- PostgreSQL:仅 STORED
ALTER TABLE t ADD total DECIMAL(12,2)
  GENERATED ALWAYS AS (price * qty) STORED;
#

85. MONEY 类型的局限(精度、地区格式化)?

PostgreSQL 的 MONEY 类型有哪些局限(精度、地区格式化)?何时不应使用它?

  • MONEY 的存储与精度
  • 地区格式化问题
  • 与 NUMERIC 的对比与选型

MONEY 类型(PostgreSQL 内建):存储 8 字节(定点整数表示"分/最小货币单位"),输出按"当前 locale 的货币格式"($1,234.56、¥1,234 等,受 lc_monetary 影响);局限:其一,精度——固定小数位(2 位)且"按最小货币单位整数存储":只能表达两位小数(金额、分),需要 4 位小数(利率、汇率、精细金额)或不同货币单位(日元 0 位小数)时不适用;运算可能因整数运算产生截断(除法的余数);其二,地区格式化——输入/输出依赖 lc_monetary:同一值在不同 locale 显示不同($1,234.56 vs 1.234,56 €),应用层解析输出文本容易踩坑(字符串解析、排序、导出 CSV 按 locale 变);跨库迁移(MONEY → NUMERIC)输出形态不稳定;其三,函数与运算限制——运算时货币与 numeric 混合需转换、无丰富的货币函数(舍入方向、货币换算)需应用层;其四,存储与精度上限——8 字节范围 ±922 万亿(约),大金额(万亿级)溢出;其五,索引/比较按"分"整数——无浮点误差(比 float 好)但精度固定。对比:NUMERIC/DECIMAL——任意精度与标度(NUMERIC(18,4))、无 locale 依赖(数值语义稳定)、运算灵活、跨库一致,是金额场景的标准选择;MONEY 的定位是"快速、紧凑的货币显示类型",其显示格式化与固定精度在现代全球化业务(多货币、多 locale)中是缺陷。选型结论:金融/对账/精确金额一律 NUMERIC;MONEY 仅用于"单 locale 内部展示、无需精细精度"的轻量场景(极少);SQL Server 的 MONEY 类型同理(8 字节、4 位小数?SQL Server money 是 8 字节 4 位小数(19,4),也有类似精度与运算问题——业界同样建议 decimal)。注意:Oracle/MySQL 无 MONEY 类型(用 NUMBER/DECIMAL)。

答题先讲 MONEY 的存储(8 字节按货币单位整数)与核心局限(固定精度、lc_monetary 地区格式化导致输入输出不稳定、运算限制、范围上限),再对比 NUMERIC(任意精度、无 locale、跨库一致)并给"金额用 NUMERIC"的结论,最后提 SQL Server money 同理。

#

86. 枚举类型能否新增值?ALTER TYPE ADD VALUE 的语法?

枚举类型能否新增值?ALTER TYPE ADD VALUE 的语法与限制是什么?

  • ADD VALUE 的语法与时机
  • 只能追加的限制
  • 删除/改名的替代

可以新增:PostgreSQL——ALTER TYPE mood ADD VALUE 'ecstatic';(PG 9.1+ 支持、12+ 可事务内);MySQL——枚举是列级列表(ALTER TABLE ... MODIFY col ENUM(...))而非独立类型;Oracle 无原生枚举。ALTER TYPE ADD VALUE 语义与限制:其一,只能"追加到末尾"——新值加在枚举定义的最后(新值的排序在最后):不能"插入到中间"或"指定位置"(枚举的排序=定义顺序,位置固定;需要中间插入只能重建类型:新建枚举类型→改列类型→删旧类型);其二,旧事务可见性(PG 12 前)——PG 12 之前 ADD VALUE 不能在事务块内使用("ALTER TYPE ... ADD cannot run inside a transaction block"),且新值"在提交事务完成前不可用"(PG 12+ 放宽:可事务内使用、新值对"已开始的事务"不可见直到提交——使用新值需在 ADD VALUE 事务提交后);其三,已使用中的枚举值不能删除/重命名——DROP VALUE/ALTER VALUE 不存在(PG 枚举值不可删除(10 之前的限制?PG 不支持删除枚举值),要移除需重建类型并迁移数据;改名同样需重建(或用"新增值+迁移数据+重建");其四,并发限制——ADD VALUE 与并发使用该类型的会话交互有锁(catalog 锁),高并发 DML 下建议低峰执行;其五,索引影响——新值追加不影响既有 B 树索引(排序不变,新值排最后);其六,新值立即可用于新写入(提交后)。MySQL 的等价:ALTER TABLE t MODIFY m ENUM('a','b','c') 重建表(8.0 为 COPY 算法、锁表,大表成本高;删除值会改变存量行的序号映射(旧序号指向新值——数据错位风险),与 PG 的"只追加"哲学不同。工程建议:枚举值"只增不改"(追加新值安全、中间插入/删除需重建);需要灵活值列表用字典表(外键);生产环境枚举变更走迁移脚本(Flyway)+ 低峰;ALTER TYPE ... ADD VALUE 后新值对长事务可见性注意))。

答题先给 ADD VALUE 语法与"可追加"的结论,再列限制(只能末尾追加、PG 12 前事务限制与新值可见性、不能删除/改名需重建、并发锁、索引影响),最后对比 MySQL 的 MODIFY 重建表与字典表替代建议。

CREATE TYPE mood AS ENUM ('sad','ok','happy');
ALTER TYPE mood ADD VALUE 'ecstatic';      -- 追加到末尾(排最后)
-- 不能:ALTER TYPE mood ADD VALUE 'angry' BEFORE 'happy';  -- 无 BEFORE/AFTER 语法
-- 需中间插入:重建类型
CREATE TYPE mood_new AS ENUM ('sad','angry','ok','happy','ecstatic');
ALTER TABLE t ALTER COLUMN m TYPE mood_new USING m::text::mood_new;
DROP TYPE mood;
-- MySQL:列级枚举重建表
ALTER TABLE t MODIFY m ENUM('sad','ok','happy','ecstatic');
#

87. DROP INDEX 与 ALTER TABLE DROP INDEX 的差异?

DROP INDEX 与 ALTER TABLE DROP INDEX 的差异是什么?各数据库的语法如何?

  • 两语法在各库的支持
  • 删除索引的权限与影响
  • 约束伴随索引的处理

语法差异(按库):PostgreSQL——只有 DROP INDEX [IF EXISTS] 名 [CASCADE|RESTRICT];(无 ALTER TABLE DROP INDEX 语法;主键/唯一约束的伴随索引不能用 DROP INDEX 直接删(报错 "cannot drop index ... because constraint requires it"),需 ALTER TABLE DROP CONSTRAINT);MySQL——两种都支持:DROP INDEX idx ON t; 与 ALTER TABLE t DROP INDEX idx;(等价,且约束的索引也通过 DROP INDEX 删除(MySQL 中唯一约束/主键即索引:DROP INDEX 删除唯一约束的索引即删除约束);主键用 ALTER TABLE t DROP PRIMARY KEY);SQL Server——DROP INDEX idx ON t;(需指定表名;2016+ 支持 IF EXISTS;约束伴随索引用 DROP INDEX 有限制(需先删约束或指定选项));Oracle——DROP INDEX idx;(无表名)。差异核心:PG 中"索引与约束对象分离"(DROP INDEX 删索引、约束伴随索引受保护需 DROP CONSTRAINT),MySQL 中"约束即索引"(DROP INDEX 即删约束)、SQL Server 需表名、Oracle 无表名。

权限与影响:DROP INDEX 需要索引 owner/表 owner(或相应权限);删除索引立即生效(查询不再使用,锁:PG 的 DROP INDEX 需锁(ACCESS EXCLUSIVE on index——不阻塞表读写?DROP INDEX 获取索引上的锁(排他 on index),短时;大索引删除是 IO 操作(删除文件));IF EXISTS 幂等;CASCADE(PG)删除依赖对象(如依赖该索引的约束?索引被约束引用时 RESTRICT 报错)。注意:其一,删除索引前评估——查询计划不再使用(EXPLAIN)、外键依赖(外键索引被删后父表删除变全扫)、覆盖索引删除影响;其二,约束伴随索引——PG 中删除唯一约束需 DROP CONSTRAINT(连带删索引)、MySQL 中 DROP INDEX 直接删约束(若该约束被外键引用会报错)、SQL Server 删除约束自动删伴随索引(或 DROP INDEX 需先处理约束);其三,主键索引——PG 删主键用 ALTER TABLE DROP CONSTRAINT pk、MySQL 用 DROP PRIMARY KEY、SQL Server 同约束;其四,生产删除用 IF EXISTS + 事务(PG 可回滚)并在低峰执行)。

答题先按库列语法(PG 仅 DROP INDEX、MySQL 双语法、SQL Server 需表名、Oracle 无表名),再重点讲"约束与索引分离"的差异(PG 约束伴随索引受保护 vs MySQL 约束即索引),最后讲权限、影响评估(EXPLAIN、外键、覆盖)与生产注意。

-- PostgreSQL
DROP INDEX IF EXISTS idx_orders_uid;
ALTER TABLE orders DROP CONSTRAINT uk_orders;   -- 删约束连带删伴随索引
-- MySQL(两种等价)
DROP INDEX idx_orders_uid ON orders;
ALTER TABLE orders DROP INDEX idx_orders_uid;
ALTER TABLE orders DROP PRIMARY KEY;
-- SQL Server
DROP INDEX idx_orders_uid ON orders;
#

88. 临时 schema(pg_temp_*)在 search_path 中的优先级,同名临时对象如何覆盖永久对象?

临时 schema(pg_temp_*)在 search_path 中的优先级是什么?同名临时对象如何覆盖永久对象?

  • pg_temp schema 的隐式优先级
  • 同名对象解析顺序
  • 显式 pg_temp 的使用

机制:PostgreSQL 为每个会话维护"临时 schema"(pg_temp_N,N 为会话序号):会话创建的临时表/临时视图存于此;解析"未限定对象名"时,pg_temp 具有"隐式最高优先级"——即使 search_path 中未显式写出 pg_temp,优化器也把 pg_temp 排在 search_path 之前(会话存在临时对象时),因此"同名临时表覆盖同名永久表":若用户建了临时表 t,后续 SELECT * FROM t 解析到临时表(永久表 t 被遮蔽,需要全限定 schema.t 才能访问);临时 schema 只在会话内可见(其他会话看不到本会话的临时表,也解析不到)。细节:其一,隐式优先的适用对象——表/视图/序列等关系对象;函数/类型也受 pg_temp 优先影响(同名函数解析优先临时函数?pg_temp 对函数解析同样优先(PG 文档:临时 schema 在函数名解析中也优先));其二,显式引用——pg_temp.t 或 schema.t 可绕过优先级(全限定永久表);其三,search_path 中可显式写 pg_temp(SET search_path TO pg_temp, public)——与隐式优先级等价(显式写时可控制"放在哪"?pg_temp 无论如何都优先(即使写在列表末尾),显式写主要是"文档化";实际上把 pg_temp 写在中间不会改变其隐式优先);其四,清空/销毁——会话结束时临时 schema 自动删除(DROP 会话临时对象)、事务内 ON COMMIT 控制临时表生命周期;pg_temp 在 pg_namespace 可见(nspname 以 pg_temp_ 开头);其五,安全影响——临时表遮蔽永久表可被利用(攻击者无法建他人会话的临时表(临时 schema 私有),但应用代码中"同名临时表意外遮蔽"是常见 bug 来源:存储过程/批处理创建临时表后忘删,后续查询解析到残留临时表(数据不一致));其六,与 pg_catalog 的优先级关系——pg_temp 优先于 pg_catalog(PG 文档:pg_temp 排在 pg_catalog 之前?实际解析顺序:pg_temp(若存在)→ pg_catalog → search_path 列表?官方:pg_catalog 总是先于 search_path 中列出的 schema,而临时 schema 又先于 pg_catalog?PostgreSQL 文档:临时表 schema 在 pg_catalog 之前被搜索——是的:pg_temp 优先、其次 pg_catalog、最后 search_path)。工程实践:批处理用临时表后显式 DROP(或 ON COMMIT DROP);查询区分"临时表与永久表同名"的场景用全限定名;审计 pg_temp 数量(残留会话)。

答题先讲临时 schema(pg_temp_N、会话私有)与解析优先级(pg_temp 隐式最高:同名临时对象覆盖永久对象,pg_temp > pg_catalog > search_path),再讲绕过方法(全限定名)与显式写 pg_temp 的意义,最后列安全/工程注意(残留遮蔽、函数解析同样优先、会话结束清理)。

CREATE TEMP TABLE users (id INT);            -- 遮蔽永久表 users
SELECT * FROM users;                          -- 解析到临时表
SELECT * FROM public.users;                   -- 全限定访问永久表
-- 显式写 pg_temp(文档化,不影响隐式优先)
SET search_path TO pg_temp, public;
SELECT nspname FROM pg_namespace WHERE nspname LIKE 'pg_temp%';
#

89. current_schema()、current_schemas(boolean) 的语义差异?

current_schema() 与 current_schemas(boolean) 的语义差异是什么?各自的使用场景是什么?

  • 两个函数的返回内容
  • 参数(include_implicit)的含义
  • 与 search_path 的关系

current_schema()——返回"当前 schema"(search_path 中"第一个存在的 schema"的名字,未找到时返回 NULL):即未限定对象将落定的 schema;current_schemas(boolean)——返回"当前搜索路径中所有 schema 的数组":参数 include_implicit(true 时包含隐式 schema:pg_catalog 与 pg_temp(临时 schema)),false 只返回"显式列出的 schema"(search_path 中的 $user 解析、public 等,不含隐式)。差异核心:current_schema 是"第一个 schema 的名字"(标量、落点)、current_schemas 是"完整路径列表"(数组、含隐式选项);current_schema 等价于 current_schemas(false)[1](第一个显式存在的 schema)。示例:search_path = app, public 且 app 存在:current_schema()='app';current_schemas(false)={app,public};current_schemas(true)={pg_catalog,pg_temp,app,public}(含隐式(pg_catalog 总在、pg_temp 会话存在时);注意数组顺序:pg_catalog/pg_temp 排最前)。使用场景:current_schema()——"对象应建在哪"(未限定建表落点)、函数内判断当前 schema、权限/诊断输出;current_schemas(true)——安全审计(查看实际解析路径(含隐式)、调试"对象解析到哪个 schema"、实现"解析顺序感知"的逻辑(如工具遍历所有可能 schema);current_schemas(false)——业务级路径展示(不含系统 schema)。注意:current_schema() 返回 NULL 的情况(search_path 中所有 schema 都不存在且无 $user);current_schemas 返回的数组中"不存在的 schema 被跳过"($user 不存在则不含);函数受 search_path 变更影响(会话内 SET 后返回值变化);跨库对应:MySQL 的 DATABASE()(当前默认库,类似 current_schema 的单值))。

答题先分别定义两个函数(单值当前 schema vs 数组完整路径)与参数语义(include_implicit 控制是否含 pg_catalog/pg_temp),再给示例对比(含隐式与不含的顺序),最后列使用场景(建表落点、审计调试、路径感知)与注意(NULL 情况、不存在跳过)。

SET search_path TO app, public;
SELECT current_schema();              -- 'app'
SELECT current_schemas(false);        -- {app, public}
SELECT current_schemas(true);         -- {pg_catalog, pg_temp, app, public}(会话有临时表时)
-- 等价
SELECT current_schemas(false)[1] = current_schema();
#

90. SET search_path TO public, app 的语义?

SET search_path TO public, app 的语义是什么?对象解析顺序如何变化?

  • 列表顺序与解析顺序
  • public 在前的含义
  • 与默认路径的差异

语义:SET search_path TO public, app——把会话的 schema 搜索路径设置为 [public, app](按此顺序解析未限定对象名):查询表 t 时先查 public.t(存在即用),没有再查 app.t;未限定 CREATE TABLE 建在第一个 schema(public)——注意"列表顺序即优先级":public 在前 = public 优先(与默认 "$user", public 相比:把 $user 换成了显式 public,且 app 作为第二个候选)。与默认值 "$user", public 的差异:其一,$user 被移除——若当前用户有同名 schema(user_schema),默认会优先解析到它,设置后不再查(或由 public 顶替第一个位置);其二,public 升为第一——未限定建表落点从"$user 同名 schema(若存在)"变为 public;其三,app 加入路径——原本不解析 app 的对象(默认不含 app),设置后未限定名可命中 app 的对象(免写 app. 前缀)。适用场景:其一,"公共表在 public、业务表在 app"的应用——public 优先级高保证公共对象(如配置表)优先、app 兜底业务对象;其二,需要"把 app 加入解析但保持 public 优先"的迁移场景(新 schema 渐进接管);其三,多 schema 分层(共享层 public + 应用层 app)。注意点:其一,顺序敏感——写反(app, public)则 app 优先(同名对象解析到 app)——"TO public, app"与"TO app, public"语义不同(第一个是落点与最高优先);其二,pg_catalog/pg_temp 隐式优先不受影响(无论顺序,系统目录与临时对象仍然先于 public/app);其三,安全——把 public 放第一位而 public 可写(15 前)增加 search_path 攻击面;其四,作用域——SET 会话级(后续语句生效,当前已解析的不变)、可 SET LOCAL 事务级、可 ALTER ROLE 持久化;其五,验证——SHOW search_path / current_schemas(false)。工程建议:新应用优先"业务 schema 在前"(SET search_path TO app, public 或仅 app);public 在前仅用于"共享层优先"的特定分层;任何顺序都要配合 public 权限收紧。

答题先解释列表语义(顺序即解析优先级、第一个是建表落点),再对比默认 "$user", public 的三点差异(移除 $user、public 升一、app 加入),然后列适用场景(分层、渐进迁移)与注意(顺序敏感、隐式优先不受影响、安全)、最后给工程建议。

SET search_path TO public, app;
SHOW search_path;                      -- 'public, app'
SELECT * FROM t;                       -- 先 public.t,没有再 app.t
CREATE TABLE t (...);                  -- 建在 public
-- 顺序相反:app 优先
SET search_path TO app, public;
#

91. CREATE TABLE 未限定模式名时默认创建在哪个 schema?

CREATE TABLE 未限定模式名时默认创建在哪个 schema?规则与影响是什么?

  • 建表落点规则(search_path 第一个 schema)
  • $user 与 public 的行为
  • 显式限定与安全

规则:CREATE TABLE t(未限定 schema)时,表创建在"search_path 中第一个存在的 schema"——默认 search_path 为 "$user", public:若存在与当前用户同名的 schema($user 命中),建在那里;否则建在 public(大多数情况,因为用户同名 schema 很少预建)。因此默认行为 = 建在 public(除非专门建了用户同名 schema 或修改过 search_path)。影响与细节:其一,落点随 search_path 变化——SET search_path TO app 后未限定建表落在 app(这是多 schema 应用的标准做法);其二,落点决定"后续未限定引用"——建在 public 的表在默认路径下可被未限定引用(public 在路径中),建在 app 的表若路径不含 app 则必须 app.t 引用;其三,权限——建表需要目标 schema 的 CREATE 权限(public 默认所有用户可建(15 前)→ 建表可能落到"他人可读的 public",敏感表注意授权;业务 schema 需显式 GRANT CREATE);其四,风险——未限定建表"落点不确定"(依赖路径配置)在多环境(开发/生产 search_path 配置不同)下可能建错 schema(同名表在不同环境落点不同)——迁移脚本/ORM 建表建议显式 schema(CREATE TABLE app.t)或固定 search_path;其五,与临时表区分——CREATE TEMP TABLE 总是建在 pg_temp(不受此规则影响);其六,查看落点——建表后查 current_schema()/pg_namespace,或用 CREATE TABLE ... INHERITS 等;其七,工具行为——ORM(ddl-auto)按连接配置的 search_path 建表(Hibernate 默认 public),Flyway 迁移脚本中的未限定建表同样落 public(除非脚本显式 schema 或设置默认路径)。工程规范:生产环境固定 search_path(ALTER ROLE/连接参数)并显式 schema 前缀(或统一"业务 schema 在前");新库用 template1 预置业务 schema;审计定期检查"表是否都建在预期 schema"(information_schema.tables 分组统计)。

答题先给规则(search_path 第一个存在的 schema,默认 $user 优先、通常落 public),再讲四类影响(落点随路径变化、权限、跨环境不一致风险、临时表例外),最后给工程规范(显式 schema/固定路径/审计)。

SHOW search_path;                 -- '"$user", public'
CREATE TABLE t (id INT);          -- 通常建在 public
-- 验证落点
SELECT table_schema FROM information_schema.tables WHERE table_name = 't';
-- 固定到业务 schema
SET search_path TO app;
CREATE TABLE t (id INT);          -- 落在 app
CREATE TABLE app.t (id INT);      -- 显式限定(推荐)
#

92. 为什么生产环境不推荐使用 public schema?

为什么生产环境不推荐使用 public schema?有哪些替代实践?

  • public 的风险(权限、污染、攻击)
  • 对象组织与运维问题
  • 业务 schema 的替代实践

不推荐使用 public 的原因:其一,权限风险——PostgreSQL 15 前 public 默认对 PUBLIC 角色开放 CREATE:任何连接用户都能在 public 建对象(表/函数/视图)——命名空间污染与 search_path 攻击温床;15 后虽收紧(仅 owner 可建),存量/宽松配置仍存在;其二,对象混乱——所有应用的表/函数都堆在 public:多应用共享实例时对象名冲突(同名表互相覆盖(CREATE 冲突)、同名函数解析歧义)、无法按应用分组管理权限(public 中对象权限要逐个授)、迁移/备份按 schema 粒度受限(pg_dump 按 schema 导出不便);其三,审计与治理——public 没有"应用边界":审计(谁的表)、容量(哪个应用占空间)、清理(废弃对象归属)都难定位;其四,默认路径的"隐性依赖"——对象引用依赖默认 search_path(public 在路径中),重构(把表迁出 public)要全量改引用;其五,安全性纵深——即使权限收紧,public 中对象仍可能被"宽权限"遗留(历史授权未清理)。替代实践:其一,每应用专属 schema——CREATE SCHEMA app AUTHORIZATION app_owner(对象分组、权限边界(GRANT USAGE/CREATE 给应用角色)、命名空间隔离(同名表不冲突));其二,search_path 固定——应用连接 SET search_path TO app(或 ALTER ROLE 默认),未限定引用落到业务 schema;其三,模板库——template1 预置 schema 与权限(新建库自动带);其四,迁移工具——Flyway/Liquibase 脚本显式 schema(或配置默认 schema),避免落 public;其五,多租户/多应用——每租户/每应用 schema 隔离(权限+对象+审计粒度清晰);其六,清理存量——把 public 中对象迁移到业务 schema(CREATE TABLE ... AS/ALTER 或迁移工具),public 只保留"共享只读对象"或清空并回收权限。结论:生产环境"public 不用、不给写、不承载业务";业务对象按应用/租户进专属 schema。

答题先列不推荐的四类原因(权限与攻击、对象混乱与名冲突、审计治理困难、隐性依赖与遗留授权),再给替代实践(业务 schema+owner、固定 search_path、模板库、迁移工具显式 schema、多租户隔离、存量清理),最后总结核心原则。

-- 业务 schema 实践
CREATE SCHEMA app AUTHORIZATION app_owner;
GRANT USAGE ON SCHEMA app TO app_user;
GRANT CREATE ON SCHEMA app TO app_owner;
ALTER ROLE app_user SET search_path TO app;
-- 存量迁移:public → app
ALTER TABLE public.orders SET SCHEMA app;
#

93. 什么是谓词触发器(Predicate Trigger)?

什么是谓词触发器(Predicate Trigger)?在哪些数据库中存在?与普通触发器的区别是什么?

  • 谓词触发器的概念与来源
  • SQL Server 的实现(INSTEAD OF?)
  • 与其他触发器机制的区别

谓词触发器(Predicate Trigger)概念:指"由数据满足/不满足某谓词条件而触发"的触发器——即"条件触发"的触发器(WHEN 条件触发:只有谓词为真时才执行触发器动作)。在主流数据库中的对应:其一,PostgreSQL——普通触发器上的 WHEN 子句(CREATE TRIGGER ... WHEN (谓词))即谓词触发器语义(条件满足才调用触发函数,如 WHEN (NEW.status = 'PAID'));其二,Oracle——触发器上可有 WHEN 条件(BEFORE/AFTER 行触发器的 WHEN 谓词,引用 :NEW/:OLD);其三,SQL Server——"INSTEAD OF 触发器"有时被混称,但 SQL Server 的谓词触发器(Predicate Trigger)特指"CREATE TRIGGER ... WITH ... ?"——实际上 SQL Server 没有独立的"谓词触发器"类型(其 AFTER/INSTEAD OF 都是事件触发;早期文档中的"谓词"概念与 INSTEAD OF 的判定相关);"谓词触发器"术语主要出现在学术/标准讨论(如 SQL 标准草案中的"约束谓词触发")与个别实现中。更常见的区分:谓词触发器 vs 事件触发器——事件触发器(PG 的 Event Trigger)响应 DDL 事件(无条件、事件驱动);谓词触发器响应"数据满足条件"(条件驱动)。与普通触发器区别:普通触发器"每次事件都执行"(可内部再判断),谓词触发器"条件不满足完全不执行"(声明式条件、省函数调用、语义在定义层可见)——本质上 PG 的 WHEN 子句实现了谓词触发器;触发器内部 IF 判断与 WHEN 的差别(WHEN 少一次函数调用)。结论:问"谓词触发器"时,回答"按谓词条件触发的触发器 = 带 WHEN 条件的触发器(PG/Oracle 支持),与事件触发器(DDL 事件)和普通无条件触发器相对;SQL Server 无独立谓词触发器类型(用触发器内条件或 INSTEAD OF 判定)"。

答题先定义谓词触发器(满足谓词才触发的条件触发器),再给各库对应(PG/Oracle 的 WHEN 子句、SQL Server 无独立类型),然后区分三个概念(谓词/事件/普通触发器)并说明 WHEN 与内部 IF 的差异,最后给工程语义(声明式条件过滤)。

#

94. MySQL 中如何为列设置自增?

MySQL 中如何为列设置自增?语法与注意事项是什么?

  • AUTO_INCREMENT 语法
  • 引擎与键的要求
  • 自增相关操作(重置、步长)

设置语法:建表时——id INT AUTO_INCREMENT PRIMARY KEY(AUTO_INCREMENT 列属性,必须配合键:该列必须是"索引的一部分"(主键/唯一/普通索引均可,InnoDB 要求其为索引;最规范是主键));id BIGINT AUTO_INCREMENT UNIQUE 也可;ALTER 添加——ALTER TABLE t MODIFY id INT AUTO_INCREMENT, ADD PRIMARY KEY (id);(已存在表加自增需先有键或同时加)。插入行为:省略该列(INSERT INTO t (name) VALUES ('a'))自动分配(从 1 或当前值+1);显式插入 id 后计数器自动跳到"max+1"(插入 100 后下一条为 101);LAST_INSERT_ID() 取会话最后分配值(配合 AUTO_INCREMENT=0 可强制分配)。注意事项:其一,类型——整数类型(TINYINT/SMALLINT/INT/BIGINT),建议 BIGINT(防溢出,INT 21 亿上限);其二,引擎——InnoDB(支持事务与持久化;MyISAM 自增行为不同(表锁、删最大行复用));其三,键要求——AUTO_INCREMENT 列必须是"索引列"(主键/唯一/前缀索引除外?必须为"索引中第一列"?MySQL 要求 AUTO_INCREMENT 列必须是索引(且通常建议为主键);复合索引时须为第一列);其四,空洞——回滚/删除产生空洞、多主复制用 auto_increment_increment/offset 错开;其五,重置——ALTER TABLE t AUTO_INCREMENT = 100;(8.0 中若指定值 ≤ max(id)+1 会被调整为 max+1(不能回拨));TRUNCATE 重置(8.0 中 TRUNCATE 后自增从 1 开始);其六,8.0 持久化——自增计数器随 redo 持久化(重启不回退,5.7 及以前重启后按 max(id)+1 计算可能回退);其七,AUTO_INCREMENT 与 GENERATED 列——生成列不能用 AUTO_INCREMENT;其八,会话分配——LAST_INSERT_ID() 是"会话级"(多连接互不影响);批量插入(INSERT ... SELECT/多行)LAST_INSERT_ID() 返回"第一个分配值"(8.0 中多行插入返回第一行的值?MySQL 中多行 INSERT 的 LAST_INSERT_ID() 返回第一条的自增值(5.7 行为),可用 LAST_INSERT_ID() 与行数推算;实际上 MySQL 文档:多行插入 LAST_INSERT_ID() 返回第一个自动生成的值)。工程建议:BIGINT + 主键 + InnoDB;需要"全局唯一"(分库分表)不用自增。

答题先给建表与 ALTER 语法(AUTO_INCREMENT 列属性+索引要求),再讲插入行为(省略列自动分配、显式插入跳变、LAST_INSERT_ID 会话级)与八项注意(类型、引擎、键、空洞、重置、8.0 持久化、生成列、批量插入返回值),最后给工程建议。

CREATE TABLE users (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50)
);
INSERT INTO users (name) VALUES ('a');
SELECT LAST_INSERT_ID();
ALTER TABLE users AUTO_INCREMENT = 1000;   -- 重置(不能低于 max+1)
-- 多实例错开
SET GLOBAL auto_increment_increment = 2, auto_increment_offset = 1;
#

95. 雪花 ID 的 64 位结构如何划分?

雪花 ID(Snowflake ID)的 64 位结构如何划分?各部分的作用与限制是什么?

  • 位结构(符号/时间戳/机器/序列)
  • 各字段的作用与容量
  • 时钟回拨与变体

经典雪花 ID(Twitter Snowflake)64 位划分:第 1 位——符号位(恒为 0,保证 ID 为正数);接下来 41 位——毫秒时间戳(相对"自定义纪元"(epoch,如 2010-11-04)的偏移:2^41 ms ≈ 69 年可用(从纪元起),容量满足长期使用;这 41 位保证"趋势递增"(时间序);接下来 10 位——机器/工作节点 ID(5 位数据中心 + 5 位机器(Twitter 原版)或 10 位 worker:最多 1024 个节点);最后 12 位——序列号(同一毫秒内的递增计数:每毫秒每节点可生成 4096 个 ID,超出则等待下一毫秒)。总容量:每毫秒每节点 4096 个(约 409.6 万/秒/集群(1024 节点 × 4096/ms))。各部分作用:时间戳——趋势递增(按时间排序、B 树插入友好、可解析生成时间(调试/审计));机器 ID——去中心化(各节点独立分配不冲突,需静态配置或自动注册(ZooKeeper/DB 分配));序列号——同毫秒并发区分(解决"同节点同毫秒多请求")。限制与变体:其一,时钟回拨——节点时钟回拨(NTP 校正)时"时间戳变小":若不处理会生成"重复 ID"(时间戳+序列回退);处理方案:等待时钟追上(阻塞)、用备用位/记忆上次时间戳(回拨时用序列号空间或"拒绝生成直到追上"(百度 UidGenerator 用"时间戳缓存"、美团 Leaf 用"时钟回拨检查"));其二,机器 ID 管理——最多 1024 节点、需分配机制(静态配置/注册中心)、节点扩容受限;其三,变体——41 位时间戳可缩短(如 39 位时间戳 + 更多机器/序列:Instagram 用 41 位时间戳+13 位分片+10 位序列;或增加 epoch 长度)、序列位可调;其四,非严格有序——跨节点 ID 不完全单调(节点 A 晚生成的 ID 可能小于节点 B 早生成的(不同时间戳)——排序需按"时间戳+节点"理解;同节点内严格递增);其五,可解析性——ID 可反解出时间(业务上可作"近似创建时间",但不可依赖(时钟回拨/序列));其六,与 UUID/号段对比:64 位紧凑、趋势有序、无中心依赖(需节点 ID),是分布式主键主流。工程实践:实现时处理时钟回拨(记录 lastTimestamp,回拨时等待或抛错)、节点 ID 唯一性保证(注册/配置)、监控生成速率与时钟偏移)。

答题先给 64 位结构(1 符号 + 41 时间戳(69 年)+ 10 节点(1024)+ 12 序列(4096/ms))与各部分作用(趋势递增、去中心、同毫秒区分),再列限制(时钟回拨重复、节点 ID 分配、非全局严格有序)与变体(Instagram/Leaf 调整位分配),最后给实现要点(回拨处理、节点注册)。

# 雪花 ID 生成核心(伪代码)
def next_id():
    ts = current_ms() - EPOCH
    if ts < last_ts:  # 时钟回拨
        raise ClockBackwardError
    if ts == last_ts:
        seq = (seq + 1) & 4095
        if seq == 0:  # 本毫秒用完,等待下一毫秒
            ts = wait_next_ms(last_ts)
    else:
        seq = 0
    last_ts = ts
    return (ts << 22) | (node_id << 12) | seq   # 41+10+12
#

96. CREATE INDEX idx_name ON t(col) 的基本语法?

CREATE INDEX idx_name ON t(col) 的基本语法是什么?参数与选项如何扩展?

  • 基本语法结构与默认行为
  • 常用扩展选项
  • 索引命名规范

基本语法:CREATE [UNIQUE] INDEX 索引名 ON 表名 (列名);——在 t 表的 col 列上创建 B 树索引(默认方法、非唯一):CREATE INDEX idx_users_email ON users (email);。要素:索引名(schema 内唯一(PG 中索引与表同名空间?索引名在 schema 内唯一、可与表不同名;MySQL 中索引名在表内唯一);命名规范 idx_表_列(可读、运维识别))、表名(可用 schema 限定:app.users)、列列表(单列/复合(多列按顺序,最左前缀));默认行为——USING btree(PG 默认 B 树)、非唯一、允许 NULL(多 NULL 合法)、随表 DROP 自动删除(PG 中索引随表删除;MySQL 同理)。常用扩展:UNIQUE(唯一索引:CREATE UNIQUE INDEX ...)、USING 方法(USING GIN/GiST/BRIN/hash:CREATE INDEX ... USING GIN (tags))、复合(CREATE INDEX idx ON t (a, b))、表达式(CREATE INDEX idx ON t (LOWER(email)))、部分(CREATE INDEX idx ON t (col) WHERE 条件)、INCLUDE(CREATE INDEX idx ON t (a) INCLUDE (b))、表空间(TABLESPACE ts)、存储参数(WITH (fillfactor=80))、并发(CONCURRENTLY,PG)、排序方向(MySQL 8.0 支持 (col DESC)、PG 支持 DESC/NULLS FIRST 排序)、前缀长度(MySQL:col(20))。删除与查看:DROP INDEX idx(PG)/DROP INDEX idx ON t(MySQL);\d t、pg_indexes、SHOW INDEX FROM t(MySQL)。注意:其一,索引名与约束伴随索引——主键/唯一约束自动建索引(名为约束名或自动名),显式 CREATE INDEX 是独立索引;其二,创建即生效——后续查询优化器自动使用(无需代码改动);其三,维护成本——每次写入维护索引(写放大:插入/更新/删除都更新索引);其四,权限——需表 owner(PG)/INDEX 权限(MySQL)。工程规范:命名 idx_表_列[后缀](唯一索引 ux、主键 pk_);建索引前 EXPLAIN 验证需求;控制索引数量(写入性能与存储);大表用 CONCURRENTLY/INPLACE。

答题先给基本语法与默认行为(B 树、非唯一、命名与删除),再列常用扩展(UNIQUE/USING/复合/表达式/部分/INCLUDE/TABLESPACE/CONCURRENTLY/前缀/排序方向)与删除查看,最后给命名规范、维护成本与权限注意。

CREATE INDEX idx_users_email ON users (email);
CREATE UNIQUE INDEX ux_users_email ON users (email);
CREATE INDEX idx_orders ON orders (user_id, created_at DESC);
CREATE INDEX idx_gin ON t USING GIN (tags);
CREATE INDEX idx_part ON t (status) WHERE status = 'PENDING';
CREATE INDEX CONCURRENTLY idx_big ON big_table (col);
DROP INDEX idx_users_email;
#

97. 什么是倒排索引(Inverted Index)?

什么是倒排索引(Inverted Index)?原理与典型应用是什么?

  • 倒排索引的定义与结构
  • 与 B 树的对比
  • 典型应用(全文、GIN)

倒排索引(Inverted Index):以"值/词项 → 包含它的文档/行列表"为方向的索引("倒排"指把"文档→词项"的正向关系反转为"词项→文档列表"(posting list)):数据结构——词典(词项表)+ 每个词项对应的"倒排列表"(docID 列表,可含位置/权重信息);查询"包含词项 w 的文档"直接查词典定位 w 的倒排列表(O(词项查找)),无需扫描全部文档。与 B 树对比:B 树按"键值"组织(适合等值/范围/排序:键是完整值);倒排索引按"元素/词项"组织(适合"复合值中的元素匹配":数组元素、JSON 键值、文本词项),B 树无法高效回答"哪个文档包含某词/哪个数组包含某元素"(只能扫描)。典型应用:其一,全文检索——搜索引擎核心:文本分词(tokenize)→ 词项倒排(docID+位置+词频),查询 AND/OR/短语在倒排列表上做集合运算(PostgreSQL 的 GIN 索引实现 tsvector 倒排、Elasticsearch 的 Lucene 倒排);其二,数据库的 GIN 索引——PostgreSQL GIN 是"通用倒排索引":数组(@> 包含)、JSONB(键值包含/存在)、hstore、pg_trgm(trigram 倒排做模糊);其三,列存/搜索引擎——Elasticsearch/Solr 的字段索引;其四,数据库的位图索引(Oracle Bitmap:对低基数列建"值→行位图",也是"值→行"的倒排思想)。实现细节:倒排列表压缩(差值编码、变长编码——海量 docID 存储优化)、合并算法(多词 AND 取交集、OR 并集、NOT 差集)、位置信息(短语查询需存词位置)、更新(增量合并(PG 的 pending list)、删除标记(逻辑删除+合并清理))。局限:不适合范围/排序查询(倒排无值域顺序)、更新成本(词项列表维护)、存储(位置/权重信息膨胀)。结论:倒排索引解决"按内容元素找行/文档"的问题(全文、数组、JSON 元素),数据库层由 GIN 与搜索引擎(Lucene)实现。

答题先定义倒排索引(词项/值→行列表的"值到行"反向映射:词典+倒排列表),再对比 B 树(键组织 vs 元素组织)说明适用边界,然后列典型应用(全文检索、GIN 的数组/JSONB/trigram、位图索引思想)与实现细节(压缩、合并、位置、增量更新),最后讲局限(无范围排序、更新成本)。

#

98. 部分索引(Partial Index)与表达式索引的适用场景与维护成本

部分索引(Partial Index)与表达式索引的适用场景与维护成本是什么?

  • 两种索引的适用场景
  • 维护成本的差异
  • 组合使用

部分索引(WHERE 条件限定索引行集):适用场景——"高频谓词 + 小基数子集"(软删除(WHERE deleted_at IS NULL)、状态子集(WHERE status='PENDING')、热点数据、时间窗);价值——体积小(只含子集行)、查询命中率高、部分唯一(子集内唯一);维护成本——插入/更新时"判断行是否满足索引条件"(条件表达式每行求值)、满足才写索引(不满足的行零索引维护)、更新使行"进出子集"时增删索引条目——总维护量正比于"子集大小"而非全表(子集小则维护成本远低于全量索引);统计——ANALYZE 按子集(小集合统计准确)。表达式索引(表达式作索引键):适用场景——"函数包裹列的高频查询"(LOWER(email) 不敏感匹配、date_trunc('day', ts) 报表、JSON 字段提取、规范化值);价值——让"函数条件"走索引(否则全扫)、表达式唯一(不敏感唯一);维护成本——每次写入"对表达式求值并写入索引键"(表达式计算成本 × 行数,与全量索引相同量级——表达式昂贵时写入变慢);统计——基于表达式值。两者组合:部分表达式索引(CREATE INDEX ON t (LOWER(email)) WHERE active)——"子集内的表达式索引"(体积与维护都最小化:只对活跃行维护 LOWER 键):高频场景的"组合优化";部分唯一 + 表达式唯一叠加(活跃用户邮箱不敏感唯一)。维护成本对比总结:全量普通索引——每行维护(键=列值);表达式索引——每行"计算+维护"(贵在计算);部分索引——"判断+子集维护"(贵在判断、总量小);部分+表达式——"子集内计算+维护"(最优组合)。工程建议:先识别"高频查询形态"(过滤谓词 + 函数条件),再组合建"部分表达式索引";监控写入性能(索引多则写慢)与索引膨胀(pgstattuple);删除不再使用的索引(pg_stat_user_indexes 的 idx_scan 低值清理)。注意:部分索引条件与查询匹配(蕴含)要求、表达式 IMMUTABLE 要求。

答题先分别讲部分索引(子集场景:软删除/状态,维护=判断+子集量)与表达式索引(函数查询场景,维护=计算+全量)的适用与维护成本,再重点讲组合(部分+表达式:子集内计算维护最优),最后给工程建议(按高频查询形态组合建索引、监控清理)。

-- 部分索引:子集维护
CREATE INDEX idx_active ON users (email) WHERE deleted_at IS NULL;
-- 表达式索引:全量计算维护
CREATE INDEX idx_lower ON users (LOWER(email));
-- 组合:子集内表达式
CREATE INDEX idx_active_lower ON users (LOWER(email)) WHERE deleted_at IS NULL;
-- 监控使用
SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE relname='users';