Files
nxxmdata/backend/analyze-foreign-keys.js

225 lines
7.4 KiB
JavaScript
Raw Normal View History

/**
* 分析数据库中表之间的外键关系
* @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 };