Files
nxxmdata/backend/analyze-foreign-keys.js
2025-08-25 15:00:46 +08:00

225 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 分析数据库中表之间的外键关系
* @file analyze-foreign-keys.js
* @description 识别所有外键约束和引用关系为ID重排做准备
*/
const { sequelize } = require('./config/database-simple');
const { QueryTypes } = require('sequelize');
async function analyzeForeignKeys() {
try {
console.log('=== 分析数据库外键关系 ===\n');
// 获取所有外键约束
const foreignKeys = await sequelize.query(`
SELECT
kcu.TABLE_NAME as table_name,
kcu.COLUMN_NAME as column_name,
kcu.REFERENCED_TABLE_NAME as referenced_table,
kcu.REFERENCED_COLUMN_NAME as referenced_column,
rc.CONSTRAINT_NAME as constraint_name,
rc.UPDATE_RULE as update_rule,
rc.DELETE_RULE as delete_rule
FROM information_schema.KEY_COLUMN_USAGE kcu
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
WHERE kcu.TABLE_SCHEMA = DATABASE()
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY kcu.TABLE_NAME, kcu.COLUMN_NAME
`, { type: QueryTypes.SELECT });
console.log(`发现 ${foreignKeys.length} 个外键关系:\n`);
const relationshipMap = new Map();
const tablesWithForeignKeys = new Set();
const referencedTables = new Set();
foreignKeys.forEach(fk => {
const key = `${fk.table_name}.${fk.column_name}`;
const reference = `${fk.referenced_table}.${fk.referenced_column}`;
relationshipMap.set(key, {
table: fk.table_name,
column: fk.column_name,
referencedTable: fk.referenced_table,
referencedColumn: fk.referenced_column,
constraintName: fk.constraint_name,
updateRule: fk.update_rule,
deleteRule: fk.delete_rule
});
tablesWithForeignKeys.add(fk.table_name);
referencedTables.add(fk.referenced_table);
console.log(`🔗 ${fk.table_name}.${fk.column_name} -> ${fk.referenced_table}.${fk.referenced_column}`);
console.log(` 约束名: ${fk.constraint_name}`);
console.log(` 更新规则: ${fk.update_rule}`);
console.log(` 删除规则: ${fk.delete_rule}`);
console.log('');
});
// 分析每个外键字段的数据分布
console.log('\n=== 外键字段数据分布 ===\n');
const foreignKeyStats = [];
for (const [key, relationship] of relationshipMap) {
const { table, column, referencedTable, referencedColumn } = relationship;
try {
// 获取外键字段的统计信息
const stats = await sequelize.query(`
SELECT
COUNT(*) as total_count,
COUNT(DISTINCT ${column}) as unique_count,
MIN(${column}) as min_value,
MAX(${column}) as max_value,
COUNT(CASE WHEN ${column} IS NULL THEN 1 END) as null_count
FROM ${table}
`, { type: QueryTypes.SELECT });
const stat = stats[0];
// 检查引用完整性
const integrityCheck = await sequelize.query(`
SELECT COUNT(*) as invalid_references
FROM ${table} t
WHERE t.${column} IS NOT NULL
AND t.${column} NOT IN (
SELECT ${referencedColumn}
FROM ${referencedTable}
WHERE ${referencedColumn} IS NOT NULL
)
`, { type: QueryTypes.SELECT });
const invalidRefs = parseInt(integrityCheck[0].invalid_references);
const fkStat = {
table,
column,
referencedTable,
referencedColumn,
totalCount: parseInt(stat.total_count),
uniqueCount: parseInt(stat.unique_count),
minValue: stat.min_value,
maxValue: stat.max_value,
nullCount: parseInt(stat.null_count),
invalidReferences: invalidRefs,
hasIntegrityIssues: invalidRefs > 0
};
foreignKeyStats.push(fkStat);
console.log(`📊 ${table}.${column} -> ${referencedTable}.${referencedColumn}:`);
console.log(` - 总记录数: ${fkStat.totalCount}`);
console.log(` - 唯一值数: ${fkStat.uniqueCount}`);
console.log(` - 值范围: ${fkStat.minValue} - ${fkStat.maxValue}`);
console.log(` - NULL值数: ${fkStat.nullCount}`);
console.log(` - 无效引用: ${fkStat.invalidReferences}`);
console.log(` - 完整性问题: ${fkStat.hasIntegrityIssues ? '是' : '否'}`);
console.log('');
} catch (error) {
console.log(`${table}.${column}: 分析失败 - ${error.message}`);
}
}
// 生成依赖关系图
console.log('\n=== 表依赖关系 ===\n');
const dependencyGraph = new Map();
foreignKeys.forEach(fk => {
if (!dependencyGraph.has(fk.table_name)) {
dependencyGraph.set(fk.table_name, new Set());
}
dependencyGraph.get(fk.table_name).add(fk.referenced_table);
});
// 计算更新顺序(拓扑排序)
const updateOrder = [];
const visited = new Set();
const visiting = new Set();
function topologicalSort(table) {
if (visiting.has(table)) {
console.log(`⚠️ 检测到循环依赖: ${table}`);
return;
}
if (visited.has(table)) {
return;
}
visiting.add(table);
const dependencies = dependencyGraph.get(table) || new Set();
for (const dep of dependencies) {
topologicalSort(dep);
}
visiting.delete(table);
visited.add(table);
updateOrder.push(table);
}
// 对所有表进行拓扑排序
const allTables = new Set([...tablesWithForeignKeys, ...referencedTables]);
for (const table of allTables) {
topologicalSort(table);
}
console.log('建议的ID重排顺序被引用的表优先:');
updateOrder.reverse().forEach((table, index) => {
const deps = dependencyGraph.get(table);
const depList = deps ? Array.from(deps).join(', ') : '无';
console.log(`${index + 1}. ${table} (依赖: ${depList})`);
});
// 汇总报告
console.log('\n=== 汇总报告 ===');
console.log(`外键关系总数: ${foreignKeys.length}`);
console.log(`涉及外键的表: ${tablesWithForeignKeys.size}`);
console.log(`被引用的表: ${referencedTables.size}`);
const tablesWithIssues = foreignKeyStats.filter(stat => stat.hasIntegrityIssues);
if (tablesWithIssues.length > 0) {
console.log(`\n⚠️ 发现完整性问题的表:`);
tablesWithIssues.forEach(stat => {
console.log(`- ${stat.table}.${stat.column}: ${stat.invalidReferences} 个无效引用`);
});
} else {
console.log('\n✅ 所有外键关系完整性正常');
}
return {
foreignKeys,
foreignKeyStats,
updateOrder: updateOrder.reverse(),
relationshipMap,
tablesWithIssues
};
} catch (error) {
console.error('分析外键关系失败:', error);
throw error;
} finally {
await sequelize.close();
}
}
// 如果直接运行此脚本
if (require.main === module) {
analyzeForeignKeys()
.then(() => {
console.log('\n分析完成!');
process.exit(0);
})
.catch(error => {
console.error('分析失败:', error);
process.exit(1);
});
}
module.exports = { analyzeForeignKeys };