目录
递归计算树
DROP TABLE IF EXISTS "model_structure";
CREATE TABLE model_structure(
id VARCHAR(50),
model_id VARCHAR(50),
node_id VARCHAR(50),
node_name VARCHAR(50),
weight DECIMAL(22,2),
node_level TINYINT,
node_is_leaf TINYINT,
parent_node_id VARCHAR(50),
PRIMARY KEY(id)
);
DROP TABLE IF EXISTS "node_data";
CREATE TABLE node_data(
id VARCHAR(50),
node_id VARCHAR(50),
node_value DECIMAL(22,2),
period VARCHAR(50),
PRIMARY KEY(id)
);
DROP TABLE IF EXISTS "node_normalization";
CREATE TABLE node_normalization(
id VARCHAR(50),
node_id VARCHAR(50),
max DECIMAL(22,2),
min DECIMAL(22,2),
PRIMARY KEY(id)
);
DROP TABLE IF EXISTS "node_score";
CREATE TABLE node_score(
id VARCHAR(50),
node_id VARCHAR(50),
node_score DECIMAL(22,2),
period VARCHAR(50),
PRIMARY KEY(id)
);
INSERT INTO model_structure (id, model_id, node_id, node_name, weight, node_level, node_is_leaf, parent_node_id) VALUES
('MS0', 'TEST_M1', 'N0', 'ROOT', 0, 0, 0, NULL),
('MS1', 'TEST_M1', 'N1', 'A', 3, 1, 0, 'N0'),
('MS2', 'TEST_M1', 'N2', 'B', 2, 1, 0, 'N0'),
('MS11', 'TEST_M1', 'N11', 'A1', 1, 2, 1, 'N1'),
('MS12', 'TEST_M1', 'N12', 'A2', 4, 2, 1, 'N1'),
('MS21', 'TEST_M1', 'N21', 'B1', 2, 2, 1, 'N2'),
('MS22', 'TEST_M1', 'N22', 'B2', 3, 2, 1, 'N2');
INSERT INTO node_normalization (id, node_id, max, min) VALUES
('NN11', 'N11', 100, 0),
('NN12', 'N12', 50, 10),
('NN21', 'N21', 20, 0),
('NN22', 'N22', 80, 20);
INSERT INTO node_data (id, node_id, node_value, period) VALUES
('ND11_Q1', 'N11', 80, '2025Q1'),
('ND12_Q1', 'N12', 30, '2025Q1'),
('ND21_Q1', 'N21', 15, '2025Q1'),
('ND22_Q1', 'N22', 50, '2025Q1');
INSERT INTO node_data (id, node_id, node_value, period) VALUES
('ND11_Q2', 'N11', 60, '2025Q2'),
('ND12_Q2', 'N12', 40, '2025Q2'),
('ND21_Q2', 'N21', 10, '2025Q2'),
('ND22_Q2', 'N22', 70, '2025Q2');
COMMIT;
CREATE OR REPLACE PROCEDURE CALCULATE_MODEL_SCORE(
p_model_id IN VARCHAR(50)
)
AS
TYPE t_period_list IS TABLE OF VARCHAR(50);
TYPE t_node_record IS RECORD (
v_node_id VARCHAR(50),
v_weight DECIMAL,
v_is_leaf TINYINT,
v_level TINYINT
);
TYPE t_node_table IS TABLE OF t_node_record;
v_periods t_period_list;
v_nodes t_node_table;
v_child_weights DECIMAL;
v_total_weight DECIMAL;
v_normalized_value DECIMAL;
v_child_score DECIMAL;
v_final_score DECIMAL;
v_min_val DECIMAL;
v_max_val DECIMAL;
v_node_value DECIMAL;
BEGIN
SELECT DISTINCT period BULK COLLECT INTO v_periods
FROM node_data nd
JOIN model_structure ms ON nd.node_id = ms.node_id
WHERE ms.model_id = p_model_id;
DELETE FROM node_score ns
WHERE EXISTS (
SELECT 1 FROM model_structure ms
WHERE ms.node_id = ns.node_id AND ms.model_id = p_model_id
);
SELECT
node_id, weight, node_is_leaf, node_level
BULK COLLECT INTO v_nodes
FROM model_structure
WHERE model_id = p_model_id
ORDER BY node_level DESC;
FOR i IN 1 .. v_periods.COUNT LOOP
DECLARE
v_period VARCHAR(50) := v_periods(i);
BEGIN
FOR j IN 1 .. v_nodes.COUNT LOOP
DECLARE
v_node_id VARCHAR(50) := v_nodes(j).v_node_id;
v_weight DECIMAL := v_nodes(j).v_weight;
v_is_leaf TINYINT := v_nodes(j).v_is_leaf;
v_score DECIMAL := NULL;
BEGIN
IF v_is_leaf = 1 THEN
BEGIN
SELECT nd.node_value, nn.min, nn.max
INTO v_node_value, v_min_val, v_max_val
FROM node_data nd
JOIN node_normalization nn ON nd.node_id = nn.node_id
WHERE nd.node_id = v_node_id AND nd.period = v_period;
IF v_max_val > v_min_val THEN
v_score := (v_node_value - v_min_val) / (v_max_val - v_min_val);
ELSE
v_score := 0;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_score := 0;
END;
ELSE
v_total_weight := 0;
v_final_score := 0;
FOR child_rec IN (
SELECT
ms_child.weight AS child_weight,
ns_child.node_score AS child_score
FROM model_structure ms_child
JOIN node_score ns_child ON ms_child.node_id = ns_child.node_id
WHERE ms_child.parent_node_id = v_node_id
AND ms_child.model_id = p_model_id
AND ns_child.period = v_period
) LOOP
v_total_weight := v_total_weight + child_rec.child_weight;
v_final_score := v_final_score + child_rec.child_weight * child_rec.child_score;
END LOOP;
IF v_total_weight > 0 THEN
v_score := v_final_score / v_total_weight;
ELSE
v_score := 0;
END IF;
END IF;
INSERT INTO node_score (id, node_id, node_score, period)
VALUES (SYS_GUID(), v_node_id, v_score, v_period);
END;
END LOOP;
END;
END LOOP;
COMMIT;
END;
CREATE OR REPLACE PROCEDURE CALCULATE_MODEL_SCORE(
p_model_id IN VARCHAR(50)
)
AS
v_sql VARCHAR(4000);
BEGIN
DELETE FROM node_score ns
WHERE EXISTS (
SELECT 1 FROM model_structure ms
WHERE ms.node_id = ns.node_id AND ms.model_id = p_model_id
);
INSERT INTO node_score (id, node_id, node_score, period)
SELECT
SYS_GUID(),
nd.node_id,
CASE
WHEN nn.max > nn.min THEN (nd.node_value - nn.min) / (nn.max - nn.min)
ELSE 0
END AS node_score,
nd.period
FROM node_data nd
JOIN model_structure ms ON nd.node_id = ms.node_id
JOIN node_normalization nn ON nd.node_id = nn.node_id
WHERE ms.model_id = p_model_id
AND ms.node_is_leaf = 1;
DECLARE
v_max_level TINYINT;
v_current_level TINYINT;
v_rows_affected INT;
BEGIN
SELECT MAX(node_level) INTO v_max_level
FROM model_structure
WHERE model_id = p_model_id;
v_current_level := v_max_level - 1;
WHILE v_current_level >= 0 LOOP
v_sql := '
INSERT INTO node_score (id, node_id, node_score, period)
SELECT
SYS_GUID(),
parent.node_id,
COALESCE(
SUM(child_w.weight * ns.node_score) / NULLIF(SUM(child_w.weight), 0),
0
) AS node_score,
ns.period
FROM model_structure parent
JOIN model_structure child_w ON child_w.parent_node_id = parent.node_id
JOIN node_score ns ON child_w.node_id = ns.node_id
WHERE parent.model_id = ''' || p_model_id || '''
AND parent.node_level = ' || v_current_level || '
AND parent.node_is_leaf = 0
GROUP BY parent.node_id, ns.period
';
EXECUTE IMMEDIATE v_sql;
v_current_level := v_current_level - 1;
END LOOP;
END;
COMMIT;
END;
<mapper namespace="com.yourpackage.mapper.ModelScoreMapper">
<update id="calculateModelScore" statementType="CALLABLE">
{ CALL CALCULATE_MODEL_SCORE(#{modelId, jdbcType=VARCHAR}) }
</update>
</mapper>
在应用层实现
<select id="selectModelStructure" resultType="ModelStructure">
SELECT * FROM model_structure WHERE model_id = #{modelId} ORDER BY node_level DESC
</select>
<select id="selectNormalizationMap" resultType="map">
SELECT node_id, max, min FROM node_normalization
</select>
<select id="selectNodeDataByPeriod" resultType="NodeData">
SELECT node_id, node_value, period FROM node_data
WHERE node_id IN
<foreach collection="nodeIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<insert id="batchInsertNodeScores">
INSERT INTO node_score (id, node_id, node_score, period) VALUES
<foreach collection="scores" item="s" separator=",">
(SYS_GUID(), #{s.nodeId}, #{s.score}, #{s.period})
</foreach>
</insert>
@Transactional
public void calculateModelScore(String modelId) {
nodeScoreMapper.deleteByModelId(modelId);
List<ModelStructure> nodes = modelMapper.selectModelStructure(modelId);
Map<String, ModelStructure> nodeMap = nodes.stream()
.collect(Collectors.toMap(n -> n.getNodeId(), n -> n));
Map<String, Normalization> normMap = normalizationMapper.selectAll()
.stream().collect(Collectors.toMap(n -> n.getNodeId(), n -> n));
Set<String> leafIds = nodes.stream()
.filter(n -> n.getNodeIsLeaf() == 1)
.map(ModelStructure::getNodeId)
.collect(Collectors.toSet());
List<NodeData> allData = nodeDataMapper.selectByNodeIds(leafIds);
Map<String, List<NodeData>> dataByPeriod = allData.stream()
.collect(Collectors.groupingBy(NodeData::getPeriod));
for (String period : dataByPeriod.keySet()) {
Map<String, BigDecimal> scoreMap = new HashMap<>();
for (NodeData data : dataByPeriod.get(period)) {
Normalization norm = normMap.get(data.getNodeId());
BigDecimal score = normalize(data.getValue(), norm.getMin(), norm.getMax());
scoreMap.put(data.getNodeId(), score);
}
for (ModelStructure node : nodes) {
if (node.getNodeIsLeaf() == 1) continue;
List<ModelStructure> children = nodes.stream()
.filter(n -> node.getNodeId().equals(n.getParentNodeId()))
.collect(Collectors.toList());
BigDecimal totalWeight = BigDecimal.ZERO;
BigDecimal weightedSum = BigDecimal.ZERO;
for (ModelStructure child : children) {
BigDecimal childScore = scoreMap.get(child.getNodeId());
if (childScore == null) childScore = BigDecimal.ZERO;
BigDecimal w = BigDecimal.valueOf(child.getWeight());
totalWeight = totalWeight.add(w);
weightedSum = weightedSum.add(w.multiply(childScore));
}
BigDecimal finalScore = totalWeight.compareTo(BigDecimal.ZERO) > 0
? weightedSum.divide(totalWeight, 6, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
scoreMap.put(node.getNodeId(), finalScore);
}
List<NodeScore> scores = scoreMap.entrySet().stream()
.map(e -> new NodeScore(e.getKey(), e.getValue(), period))
.collect(Collectors.toList());
nodeScoreMapper.batchInsertNodeScores(scores);
}
}