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 };
|