AdvancedTopics: partitioning.sql

File partitioning.sql, 9.7 KB (added by 231058, 5 days ago)
Line 
1
2-- ddl
3
4CREATE TABLE matches
5(
6 id BIGINT,
7 game_id BIGINT NOT NULL DEFAULT 0 REFERENCES games(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
8 game_mode_id BIGINT NOT NULL DEFAULT 0 REFERENCES game_modes(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
9 tournament_id BIGINT NOT NULL DEFAULT 0 REFERENCES tournaments(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
10 server_id BIGINT NOT NULL DEFAULT 0 REFERENCES servers(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
11 status_id BIGINT NOT NULL DEFAULT 1 REFERENCES match_statuses(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
12 started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Part of PK, must be NOT NULL
13 ended_at TIMESTAMP,
14 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 CHECK (ended_at IS NULL OR started_at IS NULL OR ended_at >= started_at),
16 PRIMARY KEY (id, started_at)
17) PARTITION BY RANGE (started_at);
18
19CREATE TABLE match_teams
20(
21 match_id BIGINT NOT NULL DEFAULT 0,
22 match_started_at TIMESTAMP NOT NULL, -- Added to satisfy FK constraint
23 team_id BIGINT NOT NULL DEFAULT 0 REFERENCES teams(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
24 side_label VARCHAR(20),
25 PRIMARY KEY (match_id, team_id),
26 FOREIGN KEY (match_id, match_started_at) REFERENCES matches(id, started_at) ON DELETE CASCADE ON UPDATE CASCADE
27);
28
29CREATE TABLE match_participants
30(
31 id BIGINT PRIMARY KEY,
32 match_id BIGINT NOT NULL DEFAULT 0,
33 match_started_at TIMESTAMP NOT NULL, -- Added to satisfy FK constraint
34 user_id BIGINT NOT NULL DEFAULT 0 REFERENCES users(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
35 team_id BIGINT NOT NULL DEFAULT 0 REFERENCES teams(id) ON DELETE SET DEFAULT ON UPDATE CASCADE,
36 result_id BIGINT DEFAULT NULL REFERENCES participant_results(id) ON DELETE SET NULL ON UPDATE CASCADE,
37 score INT CHECK (score >= 0),
38 elo_change INT NOT NULL DEFAULT 0,
39 joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
40 UNIQUE (match_id, user_id),
41 FOREIGN KEY (match_id, team_id) REFERENCES match_teams(match_id, team_id),
42 FOREIGN KEY (match_id, match_started_at) REFERENCES matches(id, started_at) ON DELETE CASCADE ON UPDATE CASCADE
43);
44
45
46-- views
47
48CREATE OR REPLACE VIEW public.leaderboard
49AS SELECT u.id AS user_id,
50 u.username,
51 p.rank_points,
52 l.country,
53 p.avatar_url,
54 count(DISTINCT mp.id) AS matches_played,
55 count(DISTINCT mp.id) FILTER (WHERE pr.name = 'win') AS wins,
56 CASE
57 WHEN count(DISTINCT mp.id) = 0 THEN 0::numeric
58 ELSE round(count(DISTINCT mp.id) FILTER (WHERE pr.name = 'win')::numeric * 100.0 / count(DISTINCT mp.id)::numeric, 2)
59 END AS win_rate
60 FROM users u
61 JOIN profiles p ON p.user_id = u.id
62 JOIN locations l ON l.id = u.location_id
63 LEFT JOIN match_participants mp ON mp.user_id = u.id
64 LEFT JOIN participant_results pr ON pr.id = mp.result_id
65 GROUP BY u.id, u.username, p.rank_points, l.country, p.avatar_url
66 ORDER BY p.rank_points DESC;
67
68CREATE OR REPLACE VIEW public.match_history
69AS SELECT m.id AS match_id,
70 g.title AS game,
71 gm.mode_name AS game_mode,
72 t.name AS tournament_name,
73 m.started_at,
74 m.ended_at,
75 EXTRACT(epoch FROM m.ended_at - m.started_at) / 60::numeric AS duration_minutes,
76 l.latitude AS server_lat,
77 l.longitude AS server_lon
78 FROM matches m
79 JOIN games g ON g.id = m.game_id
80 JOIN game_modes gm ON gm.id = m.game_mode_id
81 LEFT JOIN tournaments t ON t.id = m.tournament_id
82 JOIN servers s ON s.id = m.server_id
83 JOIN locations l ON l.id = s.location_id
84 WHERE m.ended_at IS NOT NULL;
85
86CREATE OR REPLACE VIEW public.player_match_stats
87AS SELECT mp.id AS user_match_id,
88 u.id AS user_id,
89 u.username,
90 m.id AS match_id,
91 g.title AS game,
92 pr.name AS result,
93 mp.score,
94 mp.elo_change,
95 mp.joined_at,
96 jsonb_object_agg(st.name, ps.value) FILTER (WHERE st.id IS NOT NULL) AS stats
97 FROM match_participants mp
98 JOIN users u ON u.id = mp.user_id
99 JOIN matches m ON m.id = mp.match_id AND m.started_at = mp.match_started_at
100 JOIN games g ON g.id = m.game_id
101 LEFT JOIN participant_results pr ON pr.id = mp.result_id
102 LEFT JOIN participant_stats ps ON ps.match_participant_id = mp.id
103 LEFT JOIN stat_types st ON st.id = ps.stat_type_id
104 GROUP BY mp.id, u.id, u.username, m.id, g.title, pr.name, mp.score, mp.elo_change, mp.joined_at;
105
106
107
108-- fn, proc, trigg
109
110CREATE OR REPLACE PROCEDURE public.create_match(
111 IN p_match_id bigint,
112 IN p_game_id bigint,
113 IN p_game_mode_id bigint,
114 IN p_tournament_id bigint,
115 IN p_server_id bigint,
116 IN p_participants jsonb,
117 OUT p_started_at timestamp -- Added OUT parameter to pass back the chosen partition time
118)
119 LANGUAGE plpgsql
120AS $procedure$
121DECLARE
122 v_status_id BIGINT;
123BEGIN
124 IF NOT EXISTS (SELECT 1 FROM servers WHERE id = p_server_id AND is_active = TRUE) THEN
125 RAISE EXCEPTION 'Server % is not active or does not exist.', p_server_id;
126 END IF;
127
128 IF NOT EXISTS (SELECT 1 FROM tournaments WHERE id = p_tournament_id AND game_id = p_game_id) THEN
129 RAISE EXCEPTION 'Tournament % is not connected to game %.', p_tournament_id, p_game_id;
130 END IF;
131
132 SELECT id INTO v_status_id FROM match_statuses WHERE name = 'in_progress';
133 p_started_at := CURRENT_TIMESTAMP;
134
135 INSERT INTO matches (id, game_id, game_mode_id, tournament_id, server_id, status_id, started_at)
136 VALUES (p_match_id, p_game_id, p_game_mode_id, p_tournament_id, p_server_id, v_status_id, p_started_at);
137
138 RAISE NOTICE 'The match % is created in partition window %.', p_match_id, p_started_at;
139END;
140$procedure$
141;
142
143CREATE OR REPLACE PROCEDURE public.add_match_participants(IN p_match_id bigint, IN p_user_ids bigint[], IN p_team_ids bigint[])
144 LANGUAGE plpgsql
145AS $procedure$
146DECLARE
147 v_ongoing_id BIGINT;
148 v_started_at TIMESTAMP;
149BEGIN
150 SELECT id INTO v_ongoing_id FROM match_statuses WHERE name = 'in_progress';
151
152 -- Look up the partition timestamp key along with status validation
153 SELECT started_at INTO v_started_at
154 FROM matches
155 WHERE id = p_match_id AND status_id = v_ongoing_id;
156
157 IF v_started_at IS NULL THEN
158 RAISE EXCEPTION 'Match % is not active, does not exist, or status mismatch.', p_match_id;
159 END IF;
160
161 IF cardinality(p_user_ids) != cardinality(p_team_ids) THEN
162 RAISE EXCEPTION 'The number of user IDs must match the number of team IDs.';
163 END IF;
164
165 -- Include composite foreign key tracking column: match_started_at
166 INSERT INTO match_teams (match_id, match_started_at, team_id, side_label)
167 SELECT DISTINCT p_match_id, v_started_at, t_id, 'Team_' || t_id
168 FROM unnest(p_team_ids) AS t_id
169 ON CONFLICT DO NOTHING;
170
171 -- Include composite foreign key tracking column: match_started_at
172 INSERT INTO match_participants (id, match_id, match_started_at, user_id, team_id)
173 SELECT
174 coalesce((select max(id) from match_participants), 0) + row_number() OVER (),
175 p_match_id,
176 v_started_at,
177 arr.u_id,
178 arr.t_id
179 FROM unnest(p_user_ids, p_team_ids) AS arr(u_id, t_id);
180
181 RAISE NOTICE 'Participants successfully added to match %.', p_match_id;
182END;
183$procedure$
184;
185
186CREATE OR REPLACE FUNCTION public.register_match_result(r_match_id bigint, r_participant_ids bigint[], r_results text[], r_scores integer[], r_elo_changes integer[])
187 RETURNS text
188 LANGUAGE plpgsql
189AS $function$
190DECLARE
191 r_status_name text;
192 v_started_at TIMESTAMP;
193 v_comp_id BIGINT;
194BEGIN
195 IF cardinality(r_participant_ids) != cardinality(r_results) OR
196 cardinality(r_participant_ids) != cardinality(r_scores) OR
197 cardinality(r_participant_ids) != cardinality(r_elo_changes) THEN
198 RETURN 'Error: Input arrays must have matching dimensions.';
199 END IF;
200
201 -- Gather started_at for highly precise partition isolation
202 SELECT ms.name, m.started_at INTO r_status_name, v_started_at
203 FROM matches m
204 JOIN match_statuses ms ON ms.id = m.status_id
205 WHERE m.id = r_match_id;
206
207 IF v_started_at IS NULL THEN
208 RETURN 'Error: Match does not exist.';
209 END IF;
210
211 IF r_status_name NOT IN ('in_progress', 'scheduled') THEN
212 RETURN 'Error: The match is over or canceled.';
213 END IF;
214
215 -- Primary updates utilize child match indices
216 UPDATE match_participants AS mp
217 SET result_id = (SELECT id FROM participant_results WHERE name = arr.res),
218 score = arr.scr,
219 elo_change = arr.elo
220 FROM unnest(r_participant_ids, r_results, r_scores, r_elo_changes) AS arr(part_id, res, scr, elo)
221 WHERE mp.id = arr.part_id
222 AND mp.match_id = r_match_id;
223
224 SELECT id INTO v_comp_id FROM match_statuses WHERE name = 'completed';
225
226 -- Included started_at = v_started_at in the WHERE clause for partition pruning
227 UPDATE matches
228 SET status_id = v_comp_id,
229 ended_at = CURRENT_TIMESTAMP
230 WHERE id = r_match_id
231 AND started_at = v_started_at;
232
233 RETURN 'OK: Match is over.';
234END;
235$function$
236;
237
238CREATE OR REPLACE FUNCTION public.update_rank()
239 RETURNS trigger
240 LANGUAGE plpgsql
241AS $function$
242BEGIN
243 IF OLD.result_id IS NULL AND NEW.result_id IS NOT NULL THEN
244 UPDATE profiles
245 SET rank_points = GREATEST(0, rank_points + NEW.elo_change)
246 WHERE user_id = NEW.user_id;
247 END IF;
248 RETURN NEW;
249END;
250$function$
251;
252
253CREATE OR REPLACE TRIGGER update_rank_after_match
254AFTER UPDATE OF result_id ON match_participants
255FOR EACH ROW
256EXECUTE FUNCTION public.update_rank();