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