perf(feed): replace correlated subqueries with LEFT JOIN aggregations
Feed queries ran 5 correlated subqueries per row (3 COUNT + 2 EXISTS for engagement counts and viewer context). Replaced with LEFT JOIN aggregations computed once per query. Adds migration 016 with indexes on likes(thought_id), boosts(thought_id), thoughts(in_reply_to_id), and compound viewer-context indexes — expected to drop ~3s queries to <100ms on typical page sizes. Also removes WebFinger from the footer (requires query params, zero standalone value as a link).
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
-- Indexes for feed engagement counts and sorting.
|
||||||
|
-- likes and boosts are joined/counted per thought on every feed query.
|
||||||
|
-- thoughts(in_reply_to_id) is scanned for reply_count.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_likes_thought_id ON likes(thought_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_boosts_thought_id ON boosts(thought_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_thoughts_in_reply_to_id ON thoughts(in_reply_to_id) WHERE in_reply_to_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Viewer-context lookups: "did I like/boost this?"
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_likes_user_thought ON likes(user_id, thought_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_boosts_user_thought ON boosts(user_id, thought_id);
|
||||||
@@ -114,12 +114,19 @@ impl<'a> FeedSqlBuilder<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select(&self) -> String {
|
fn select(&self) -> String {
|
||||||
let viewer_checks = match self.viewer {
|
let (viewer_cols, viewer_joins) = match self.viewer {
|
||||||
Some(uid) => format!(
|
Some(uid) => (
|
||||||
"EXISTS(SELECT 1 FROM likes WHERE user_id='{uid}' AND thought_id=t.id) AS liked_by_viewer,
|
"(lv.thought_id IS NOT NULL) AS liked_by_viewer,
|
||||||
EXISTS(SELECT 1 FROM boosts WHERE user_id='{uid}' AND thought_id=t.id) AS boosted_by_viewer"
|
(bv.thought_id IS NOT NULL) AS boosted_by_viewer".to_string(),
|
||||||
|
format!(
|
||||||
|
"LEFT JOIN (SELECT thought_id FROM likes WHERE user_id='{uid}') lv ON lv.thought_id = t.id
|
||||||
|
LEFT JOIN (SELECT thought_id FROM boosts WHERE user_id='{uid}') bv ON bv.thought_id = t.id"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
"false AS liked_by_viewer, false AS boosted_by_viewer".to_string(),
|
||||||
|
String::new(),
|
||||||
),
|
),
|
||||||
None => "false AS liked_by_viewer, false AS boosted_by_viewer".to_string(),
|
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"
|
"
|
||||||
@@ -143,13 +150,17 @@ impl<'a> FeedSqlBuilder<'a> {
|
|||||||
u.header_url, u.custom_css,
|
u.header_url, u.custom_css,
|
||||||
u.local AS author_local,
|
u.local AS author_local,
|
||||||
u.created_at AS author_created_at, u.updated_at AS author_updated_at,
|
u.created_at AS author_created_at, u.updated_at AS author_updated_at,
|
||||||
(SELECT COUNT(*) FROM likes l WHERE l.thought_id=t.id) AS like_count,
|
COALESCE(l_agg.cnt, 0) AS like_count,
|
||||||
(SELECT COUNT(*) FROM boosts b WHERE b.thought_id=t.id) AS boost_count,
|
COALESCE(b_agg.cnt, 0) AS boost_count,
|
||||||
(SELECT COUNT(*) FROM thoughts r WHERE r.in_reply_to_id=t.id) AS reply_count,
|
COALESCE(r_agg.cnt, 0) AS reply_count,
|
||||||
{viewer_checks}
|
{viewer_cols}
|
||||||
FROM thoughts t
|
FROM thoughts t
|
||||||
JOIN users u ON u.id=t.user_id
|
JOIN users u ON u.id=t.user_id
|
||||||
LEFT JOIN remote_actors ra ON u.ap_id = ra.url"
|
LEFT JOIN remote_actors ra ON u.ap_id = ra.url
|
||||||
|
LEFT JOIN (SELECT thought_id, COUNT(*) AS cnt FROM likes GROUP BY thought_id) l_agg ON l_agg.thought_id = t.id
|
||||||
|
LEFT JOIN (SELECT thought_id, COUNT(*) AS cnt FROM boosts GROUP BY thought_id) b_agg ON b_agg.thought_id = t.id
|
||||||
|
LEFT JOIN (SELECT in_reply_to_id, COUNT(*) AS cnt FROM thoughts WHERE in_reply_to_id IS NOT NULL GROUP BY in_reply_to_id) r_agg ON r_agg.in_reply_to_id = t.id
|
||||||
|
{viewer_joins}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { BookOpen, Code2, Globe, Info } from "lucide-react";
|
||||||
BookOpen,
|
|
||||||
Code2,
|
|
||||||
Fingerprint,
|
|
||||||
Globe,
|
|
||||||
Info,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "";
|
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||||
|
|
||||||
@@ -15,30 +9,28 @@ const LINKS = [
|
|||||||
label: "API Reference",
|
label: "API Reference",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
external: true,
|
external: true,
|
||||||
|
title: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: `${API_URL}/.well-known/nodeinfo`,
|
href: `${API_URL}/.well-known/nodeinfo`,
|
||||||
label: "NodeInfo",
|
label: "NodeInfo",
|
||||||
icon: Info,
|
icon: Info,
|
||||||
external: true,
|
external: true,
|
||||||
},
|
title: undefined,
|
||||||
{
|
|
||||||
href: `${API_URL}/.well-known/webfinger`,
|
|
||||||
label: "WebFinger",
|
|
||||||
icon: Fingerprint,
|
|
||||||
external: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/about/fediverse",
|
href: "/about/fediverse",
|
||||||
label: "About the Fediverse",
|
label: "About the Fediverse",
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
external: false,
|
external: false,
|
||||||
|
title: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
href: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts",
|
||||||
label: "Source Code",
|
label: "Source Code",
|
||||||
icon: Code2,
|
icon: Code2,
|
||||||
external: true,
|
external: true,
|
||||||
|
title: undefined,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -52,7 +44,7 @@ export function Footer() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container flex flex-wrap items-center justify-center gap-x-1 gap-y-2 px-4 py-3">
|
<div className="container flex flex-wrap items-center justify-center gap-x-1 gap-y-2 px-4 py-3">
|
||||||
{LINKS.map(({ href, label, icon: Icon }, i) => (
|
{LINKS.map(({ href, label, icon: Icon, title }, i) => (
|
||||||
<span key={href} className="flex items-center gap-1">
|
<span key={href} className="flex items-center gap-1">
|
||||||
{i > 0 && (
|
{i > 0 && (
|
||||||
<span className="text-muted-foreground/40 select-none text-xs mx-1">
|
<span className="text-muted-foreground/40 select-none text-xs mx-1">
|
||||||
@@ -64,6 +56,7 @@ export function Footer() {
|
|||||||
{...(href.startsWith("http") || href.startsWith(API_URL)
|
{...(href.startsWith("http") || href.startsWith(API_URL)
|
||||||
? { target: "_blank", rel: "noopener noreferrer" }
|
? { target: "_blank", rel: "noopener noreferrer" }
|
||||||
: {})}
|
: {})}
|
||||||
|
title={title}
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary transition-colors duration-150 group"
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary transition-colors duration-150 group"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
Reference in New Issue
Block a user