Skip to content

Commit 4657cc0

Browse files
committed
fix: restore per-branch ORDER BY for browse_dht_torrents performance
The previous migration used CASE-based ORDER BY which prevented index usage on 9.4M rows, causing 15s timeouts. Strategy: fast indexed path when no filters active, CASE ORDER BY only when filters reduce the result set.
1 parent 4e03cae commit 4657cc0

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
-- Fix browse_dht_torrents performance regression.
2+
-- The previous migration used CASE-based ORDER BY which prevents index usage
3+
-- on 9.4M rows. Restore per-branch approach with filter params added.
4+
--
5+
-- Strategy: When NO filters are active, use the fast per-branch index-scan path.
6+
-- When filters ARE active, use a filtered query with CASE ORDER BY (smaller result set).
7+
8+
CREATE OR REPLACE FUNCTION public.browse_dht_torrents(
9+
result_limit integer DEFAULT 50,
10+
result_offset integer DEFAULT 0,
11+
sort_by text DEFAULT 'seeders',
12+
sort_order text DEFAULT 'desc',
13+
filter_category text DEFAULT NULL,
14+
min_seeders integer DEFAULT NULL,
15+
max_seeders integer DEFAULT NULL,
16+
min_leechers integer DEFAULT NULL,
17+
max_leechers integer DEFAULT NULL,
18+
min_size bigint DEFAULT NULL,
19+
max_size bigint DEFAULT NULL,
20+
date_from timestamptz DEFAULT NULL,
21+
date_to timestamptz DEFAULT NULL
22+
)
23+
RETURNS TABLE(
24+
id text, infohash text, name text, magnet_uri text,
25+
size bigint, files_count integer, seeders integer, leechers integer,
26+
created_at timestamptz, content_type text, source text
27+
)
28+
LANGUAGE plpgsql STABLE
29+
SET statement_timeout = '15s'
30+
AS $$
31+
DECLARE
32+
has_filters boolean;
33+
BEGIN
34+
-- Check if any advanced filters are active
35+
has_filters := (min_seeders IS NOT NULL OR max_seeders IS NOT NULL
36+
OR min_leechers IS NOT NULL OR max_leechers IS NOT NULL
37+
OR min_size IS NOT NULL OR max_size IS NOT NULL
38+
OR date_from IS NOT NULL OR date_to IS NOT NULL
39+
OR filter_category IS NOT NULL);
40+
41+
-- =========================================================================
42+
-- FILTERED PATH: When filters are active, use a single query.
43+
-- Filters reduce the result set so CASE ORDER BY is acceptable.
44+
-- =========================================================================
45+
IF has_filters THEN
46+
RETURN QUERY
47+
SELECT
48+
encode(t.info_hash,'hex')::TEXT,
49+
encode(t.info_hash,'hex')::TEXT,
50+
t.name::TEXT,
51+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
52+
t.size,
53+
COALESCE(t.files_count,0),
54+
COALESCE(tts.seeders,0)::INTEGER,
55+
COALESCE(tts.leechers,0)::INTEGER,
56+
t.created_at::TIMESTAMPTZ,
57+
tc.content_type::TEXT,
58+
'dht'::TEXT
59+
FROM torrents t
60+
LEFT JOIN LATERAL (
61+
SELECT tts_inner.seeders, tts_inner.leechers
62+
FROM torrents_torrent_sources tts_inner
63+
WHERE tts_inner.info_hash = t.info_hash
64+
ORDER BY tts_inner.seeders DESC NULLS LAST
65+
LIMIT 1
66+
) tts ON true
67+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
68+
WHERE
69+
(filter_category IS NULL OR tc.content_type::TEXT = filter_category)
70+
AND (min_seeders IS NULL OR COALESCE(tts.seeders, 0) >= min_seeders)
71+
AND (max_seeders IS NULL OR COALESCE(tts.seeders, 0) <= max_seeders)
72+
AND (min_leechers IS NULL OR COALESCE(tts.leechers, 0) >= min_leechers)
73+
AND (max_leechers IS NULL OR COALESCE(tts.leechers, 0) <= max_leechers)
74+
AND (min_size IS NULL OR t.size >= min_size)
75+
AND (max_size IS NULL OR t.size <= max_size)
76+
AND (date_from IS NULL OR t.created_at >= date_from)
77+
AND (date_to IS NULL OR t.created_at <= date_to)
78+
ORDER BY
79+
CASE WHEN sort_by = 'seeders' AND sort_order = 'desc' THEN COALESCE(tts.seeders, 0) END DESC NULLS LAST,
80+
CASE WHEN sort_by = 'seeders' AND sort_order = 'asc' THEN COALESCE(tts.seeders, 0) END ASC NULLS LAST,
81+
CASE WHEN sort_by = 'leechers' AND sort_order = 'desc' THEN COALESCE(tts.leechers, 0) END DESC NULLS LAST,
82+
CASE WHEN sort_by = 'leechers' AND sort_order = 'asc' THEN COALESCE(tts.leechers, 0) END ASC NULLS LAST,
83+
CASE WHEN sort_by = 'size' AND sort_order = 'desc' THEN t.size END DESC NULLS LAST,
84+
CASE WHEN sort_by = 'size' AND sort_order = 'asc' THEN t.size END ASC NULLS LAST,
85+
CASE WHEN sort_by = 'date' AND sort_order = 'desc' THEN t.created_at END DESC NULLS LAST,
86+
CASE WHEN sort_by = 'date' AND sort_order = 'asc' THEN t.created_at END ASC NULLS LAST,
87+
CASE WHEN sort_by = 'name' AND sort_order = 'asc' THEN t.name END ASC NULLS LAST,
88+
CASE WHEN sort_by = 'name' AND sort_order = 'desc' THEN t.name END DESC NULLS LAST,
89+
COALESCE(tts.seeders, 0) DESC NULLS LAST
90+
LIMIT result_limit
91+
OFFSET result_offset;
92+
RETURN;
93+
END IF;
94+
95+
-- =========================================================================
96+
-- FAST PATH: No filters — use per-branch queries for index scans.
97+
-- Each branch has a plain ORDER BY that the planner can match to an index.
98+
-- =========================================================================
99+
100+
-- SEEDERS DESC (most common — default)
101+
IF sort_by = 'seeders' AND sort_order = 'desc' THEN
102+
RETURN QUERY
103+
WITH top AS (
104+
SELECT tts.info_hash, tts.seeders AS s, tts.leechers AS l
105+
FROM torrents_torrent_sources tts
106+
WHERE tts.seeders > 0
107+
ORDER BY tts.seeders DESC
108+
LIMIT result_limit + result_offset
109+
)
110+
SELECT encode(t.info_hash,'hex')::TEXT, encode(t.info_hash,'hex')::TEXT, t.name::TEXT,
111+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
112+
t.size, COALESCE(t.files_count,0), COALESCE(top.s,0)::INTEGER, COALESCE(top.l,0)::INTEGER,
113+
t.created_at::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
114+
FROM top
115+
JOIN torrents t ON t.info_hash = top.info_hash
116+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
117+
ORDER BY top.s DESC
118+
LIMIT result_limit OFFSET result_offset;
119+
120+
-- SEEDERS ASC
121+
ELSIF sort_by = 'seeders' AND sort_order = 'asc' THEN
122+
RETURN QUERY
123+
WITH top AS (
124+
SELECT tts.info_hash, tts.seeders AS s, tts.leechers AS l
125+
FROM torrents_torrent_sources tts
126+
ORDER BY tts.seeders ASC
127+
LIMIT result_limit + result_offset
128+
)
129+
SELECT encode(t.info_hash,'hex')::TEXT, encode(t.info_hash,'hex')::TEXT, t.name::TEXT,
130+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
131+
t.size, COALESCE(t.files_count,0), COALESCE(top.s,0)::INTEGER, COALESCE(top.l,0)::INTEGER,
132+
t.created_at::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
133+
FROM top
134+
JOIN torrents t ON t.info_hash = top.info_hash
135+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
136+
ORDER BY top.s ASC
137+
LIMIT result_limit OFFSET result_offset;
138+
139+
-- LEECHERS DESC
140+
ELSIF sort_by = 'leechers' AND sort_order = 'desc' THEN
141+
RETURN QUERY
142+
WITH top AS (
143+
SELECT tts.info_hash, tts.seeders AS s, tts.leechers AS l
144+
FROM torrents_torrent_sources tts
145+
WHERE tts.leechers > 0
146+
ORDER BY tts.leechers DESC
147+
LIMIT result_limit + result_offset
148+
)
149+
SELECT encode(t.info_hash,'hex')::TEXT, encode(t.info_hash,'hex')::TEXT, t.name::TEXT,
150+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
151+
t.size, COALESCE(t.files_count,0), COALESCE(top.s,0)::INTEGER, COALESCE(top.l,0)::INTEGER,
152+
t.created_at::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
153+
FROM top
154+
JOIN torrents t ON t.info_hash = top.info_hash
155+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
156+
ORDER BY top.l DESC
157+
LIMIT result_limit OFFSET result_offset;
158+
159+
-- LEECHERS ASC
160+
ELSIF sort_by = 'leechers' AND sort_order = 'asc' THEN
161+
RETURN QUERY
162+
WITH top AS (
163+
SELECT tts.info_hash, tts.seeders AS s, tts.leechers AS l
164+
FROM torrents_torrent_sources tts
165+
ORDER BY tts.leechers ASC
166+
LIMIT result_limit + result_offset
167+
)
168+
SELECT encode(t.info_hash,'hex')::TEXT, encode(t.info_hash,'hex')::TEXT, t.name::TEXT,
169+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
170+
t.size, COALESCE(t.files_count,0), COALESCE(top.s,0)::INTEGER, COALESCE(top.l,0)::INTEGER,
171+
t.created_at::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
172+
FROM top
173+
JOIN torrents t ON t.info_hash = top.info_hash
174+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
175+
ORDER BY top.l ASC
176+
LIMIT result_limit OFFSET result_offset;
177+
178+
-- DATE DESC
179+
ELSIF sort_by = 'date' AND sort_order = 'desc' THEN
180+
RETURN QUERY
181+
WITH sorted AS (
182+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
183+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
184+
FROM torrents t
185+
ORDER BY t.created_at DESC
186+
LIMIT result_limit + result_offset
187+
)
188+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
189+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
190+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
191+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
192+
FROM sorted s
193+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
194+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
195+
ORDER BY s.tcreated DESC
196+
LIMIT result_limit OFFSET result_offset;
197+
198+
-- DATE ASC
199+
ELSIF sort_by = 'date' AND sort_order = 'asc' THEN
200+
RETURN QUERY
201+
WITH sorted AS (
202+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
203+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
204+
FROM torrents t
205+
ORDER BY t.created_at ASC
206+
LIMIT result_limit + result_offset
207+
)
208+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
209+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
210+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
211+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
212+
FROM sorted s
213+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
214+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
215+
ORDER BY s.tcreated ASC
216+
LIMIT result_limit OFFSET result_offset;
217+
218+
-- SIZE DESC
219+
ELSIF sort_by = 'size' AND sort_order = 'desc' THEN
220+
RETURN QUERY
221+
WITH sorted AS (
222+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
223+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
224+
FROM torrents t
225+
ORDER BY t.size DESC
226+
LIMIT result_limit + result_offset
227+
)
228+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
229+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
230+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
231+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
232+
FROM sorted s
233+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
234+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
235+
ORDER BY s.tsize DESC
236+
LIMIT result_limit OFFSET result_offset;
237+
238+
-- SIZE ASC
239+
ELSIF sort_by = 'size' AND sort_order = 'asc' THEN
240+
RETURN QUERY
241+
WITH sorted AS (
242+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
243+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
244+
FROM torrents t
245+
ORDER BY t.size ASC
246+
LIMIT result_limit + result_offset
247+
)
248+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
249+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
250+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
251+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
252+
FROM sorted s
253+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
254+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
255+
ORDER BY s.tsize ASC
256+
LIMIT result_limit OFFSET result_offset;
257+
258+
-- NAME ASC
259+
ELSIF sort_by = 'name' AND sort_order = 'asc' THEN
260+
RETURN QUERY
261+
WITH sorted AS (
262+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
263+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
264+
FROM torrents t
265+
ORDER BY t.name ASC
266+
LIMIT result_limit + result_offset
267+
)
268+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
269+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
270+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
271+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
272+
FROM sorted s
273+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
274+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
275+
ORDER BY s.tname ASC
276+
LIMIT result_limit OFFSET result_offset;
277+
278+
-- NAME DESC
279+
ELSIF sort_by = 'name' AND sort_order = 'desc' THEN
280+
RETURN QUERY
281+
WITH sorted AS (
282+
SELECT t.info_hash, t.name AS tname, t.size AS tsize,
283+
COALESCE(t.files_count, 0) AS tfiles, t.created_at AS tcreated
284+
FROM torrents t
285+
ORDER BY t.name DESC
286+
LIMIT result_limit + result_offset
287+
)
288+
SELECT encode(s.info_hash,'hex')::TEXT, encode(s.info_hash,'hex')::TEXT, s.tname::TEXT,
289+
('magnet:?xt=urn:btih:'||encode(s.info_hash,'hex')||'&dn='||encode(s.tname::bytea,'escape'))::TEXT,
290+
s.tsize, s.tfiles, COALESCE(tts.seeders,0), COALESCE(tts.leechers,0),
291+
s.tcreated::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
292+
FROM sorted s
293+
LEFT JOIN torrents_torrent_sources tts ON tts.info_hash = s.info_hash
294+
LEFT JOIN torrent_contents tc ON tc.info_hash = s.info_hash
295+
ORDER BY s.tname DESC
296+
LIMIT result_limit OFFSET result_offset;
297+
298+
ELSE
299+
-- Default fallback: seeders desc
300+
RETURN QUERY
301+
WITH top AS (
302+
SELECT tts.info_hash, tts.seeders AS s, tts.leechers AS l
303+
FROM torrents_torrent_sources tts
304+
WHERE tts.seeders > 0
305+
ORDER BY tts.seeders DESC
306+
LIMIT result_limit + result_offset
307+
)
308+
SELECT encode(t.info_hash,'hex')::TEXT, encode(t.info_hash,'hex')::TEXT, t.name::TEXT,
309+
('magnet:?xt=urn:btih:'||encode(t.info_hash,'hex')||'&dn='||encode(t.name::bytea,'escape'))::TEXT,
310+
t.size, COALESCE(t.files_count,0), COALESCE(top.s,0)::INTEGER, COALESCE(top.l,0)::INTEGER,
311+
t.created_at::TIMESTAMPTZ, tc.content_type::TEXT, 'dht'::TEXT
312+
FROM top
313+
JOIN torrents t ON t.info_hash = top.info_hash
314+
LEFT JOIN torrent_contents tc ON tc.info_hash = t.info_hash
315+
ORDER BY top.s DESC
316+
LIMIT result_limit OFFSET result_offset;
317+
END IF;
318+
END;
319+
$$;

0 commit comments

Comments
 (0)