Tech-Paul

work hard, play hard

索引的核心作用是通过特定的数据结构快速定位数据,从而减少查询时需要扫描的数据量。

创建索引能提升查询速度?

避免全表扫描(Full Table Scan)​:

如果没有索引,执行 SELECT * FROM table WHERE column = value 时,数据库需要逐行扫描整个表(全表扫描),时间复杂度为 ​**O(N)​(N 为数据行数)。
如果存在索引,数据库可以直接定位到符合条件的数据行,时间复杂度降低到 ​
O(log N)**​(如 B-Tree 索引)。

减少磁盘 I/O:

索引通常存储在独立的磁盘结构中,且体积远小于表数据。
通过索引快速定位到目标数据页,减少需要读取的磁盘块数量。

​ 优化排序和连接操作:

索引本身是有序的(如 B-Tree 索引),可以避免 ORDER BY 或 GROUP BY 时的显式排序。
在表连接(JOIN)时,索引能加速关联条件的匹配。

索引的原理:以 B-Tree 为例

PostgreSQL 默认使用 B-Tree(Balanced Tree) 索引,它是一种高效的平衡树结构,适合范围查询和等值查询。

1. B-Tree 的结构

节点分层
根节点(Root):树的顶层入口。
内部节点(Internal):存储键值和指向子节点的指针。
叶子节点(Leaf):存储键值和指向实际数据行(Heap)的指针(TID)。

平衡性
• 所有叶子节点到根节点的路径长度相同,保证查询效率稳定。

2. 查找过程

假设执行 SELECT * FROM users WHERE id = 42

  1. 从根节点开始,比较键值,找到下一层子节点。
  2. 递归向下,直到找到叶子节点。
  3. 从叶子节点获取数据行的物理位置(TID),直接读取目标数据页。

3. 范围查询

对于 WHERE id BETWEEN 10 AND 20
• B-Tree 可以快速定位到键值 10 的位置,然后顺序遍历叶子节点直到键值 20。

4. 维护索引

插入/删除数据:调整树的结构以保持平衡。
更新数据:如果索引列被修改,需要更新索引中的键值。


三、其他索引类型及其原理

PostgreSQL 支持多种索引类型,适用于不同场景:

1. 哈希索引(Hash Index)

原理:对索引列计算哈希值,直接映射到存储位置。
适用场景:等值查询(=),但不支持范围查询。
示例

1
CREATE INDEX idx_name ON table USING HASH (column);

2. GiST(Generalized Search Tree)

原理:支持自定义数据类型的索引(如地理坐标、全文检索)。
适用场景:复杂查询(如 && 判断几何图形相交)。
示例

1
CREATE INDEX idx_gist ON table USING GIST (geo_column);

3. GIN(Generalized Inverted Index)

原理:存储键值到行的映射,适合多值类型(如数组、JSONB)。
适用场景:包含操作(@><@)或全文检索。
示例

1
CREATE INDEX idx_gin ON table USING GIN (jsonb_column);

4. BRIN(Block Range Index)

原理:按数据块(Block)范围存储统计信息(如最小值、最大值)。
适用场景:有序大数据表的范围查询。
示例

1
CREATE INDEX idx_brin ON table USING BRIN (timestamp_column);

四、索引的代价

虽然索引能加速查询,但需要付出额外成本:

  1. 存储空间:索引需要占用磁盘空间。
  2. 写操作性能
    • 插入、更新、删除数据时,需要同步更新索引。
    • 对写频繁的表,索引过多可能导致性能下降。
  3. 维护成本:索引需要定期 VACUUMREINDEX 以清理无效数据。

五、索引的最佳实践

  1. 选择性高的列
    • 如果某列的唯一值比例高(如用户 ID),索引效果更显著。
    • 计算选择性:

    1
    2
    SELECT COUNT(DISTINCT column) / COUNT(*) FROM table;
    -- 值越接近 1,选择性越高。
  2. 复合索引(多列索引)
    • 对多列查询(如 WHERE a=1 AND b=2)有效。
    • 注意列的顺序,优先将高选择性列放在前面。
    • 示例:

    1
    CREATE INDEX idx_ab ON table (a, b);
  3. 避免冗余索引
    • 如果已有索引 (a, b),单独的 (a) 索引是冗余的。

  4. 监控索引使用情况

    1
    2
    -- 查看索引使用频率
    SELECT indexrelname, idx_scan FROM pg_stat_user_indexes;
  5. 使用覆盖索引(Index-Only Scan)
    • 如果查询的列全部在索引中,可以直接从索引返回数据,无需访问表。
    • 示例:

    1
    2
    CREATE INDEX idx_covering ON table (a) INCLUDE (b, c);
    -- 查询 SELECT b, c FROM table WHERE a = 1 可以直接使用索引。

六、示例:索引如何加速查询

场景

orders 有 1000 万行数据,执行以下查询:

1
SELECT * FROM orders WHERE user_id = 42 AND status = 'shipped';

无索引

• 需要全表扫描,检查每一行的 user_idstatus

有复合索引

1
CREATE INDEX idx_orders_user_status ON orders (user_id, status);

• 数据库直接通过索引定位到 user_id=42status='shipped' 的数据行,无需扫描全表。


七、总结

索引的本质:通过高效的数据结构(如 B-Tree、哈希)快速定位数据。
核心优势:减少磁盘 I/O 和 CPU 计算量,避免全表扫描。
适用场景:高频查询的列、高选择性列、排序和连接操作。
注意事项:权衡查询加速与写操作成本,合理设计索引。

Node.js 是单线程的,默认情况下无法充分利用多核 CPU 的性能。为了解决这个问题,Node.js 提供了 Cluster 模块,它可以帮助我们创建多个子进程(Worker),每个子进程运行在独立的 CPU 核心上,从而提升应用程序的性能和吞吐量。

以下是使用 Cluster 模块提升多核 CPU 利用率的详细说明:


一、Cluster 模块的工作原理

  1. 主进程(Master)
    • 负责创建和管理子进程。
    • 监听端口,并将请求分发给子进程。
    • 可以监控子进程的状态(如崩溃后重启)。

  2. 子进程(Worker)
    • 每个子进程都是一个独立的 Node.js 实例。
    • 处理主进程分发的请求。
    • 共享同一个端口。

  3. 通信机制
    • 主进程和子进程通过 IPC(Inter-Process Communication)进行通信。


二、使用 Cluster 模块的步骤

1. 引入 Cluster 模块

1
2
3
const cluster = require('cluster');
const http = require('http');
const os = require('os');

2. 判断当前进程是主进程还是子进程

1
2
3
4
5
if (cluster.isMaster) {
// 主进程逻辑
} else {
// 子进程逻辑
}

3. 主进程创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (cluster.isMaster) {
const numCPUs = os.cpus().length; // 获取 CPU 核心数
console.log(`Master process is running. Forking for ${numCPUs} CPUs...`);

// 根据 CPU 核心数创建子进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

// 监听子进程退出事件
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Forking a new one...`);
cluster.fork(); // 重启子进程
});
}

4. 子进程处理请求

1
2
3
4
5
6
7
8
9
else {
// 创建一个 HTTP 服务器
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, World!\n');
}).listen(3000);

console.log(`Worker ${process.pid} started`);
}

三、完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`Master process is running. Forking for ${numCPUs} CPUs...`);

for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Forking a new one...`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, World!\n');
}).listen(3000);

console.log(`Worker ${process.pid} started`);
}

四、Cluster 模块的优势

  1. 提升性能
    • 充分利用多核 CPU,提高并发处理能力。
  2. 高可用性
    • 子进程崩溃后,主进程可以自动重启新的子进程。
  3. 负载均衡
    • 主进程将请求均匀分发给子进程,避免单个进程过载。

五、Cluster 模块的注意事项

  1. 共享状态
    • 子进程之间不共享内存,需要通过 Redis 或数据库共享状态。
  2. 端口占用
    • 所有子进程共享同一个端口,由操作系统负责负载均衡。
  3. 资源消耗
    • 创建过多子进程可能导致内存和 CPU 资源耗尽。
  4. 不适合所有场景
    • 如果应用是 CPU 密集型任务,Cluster 模块效果显著;如果是 I/O 密集型任务,效果可能有限。

六、优化 Cluster 模块的使用

  1. 动态调整子进程数量
    • 根据系统负载动态创建或销毁子进程。
    • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    if (cluster.isMaster) {
    const numCPUs = os.cpus().length;
    for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
    }

    // 动态调整子进程数量
    setInterval(() => {
    const load = os.loadavg()[0]; // 获取系统负载
    if (load > numCPUs && Object.keys(cluster.workers).length < numCPUs * 2) {
    cluster.fork();
    } else if (load < numCPUs / 2 && Object.keys(cluster.workers).length > numCPUs) {
    const workerId = Object.keys(cluster.workers)[0];
    cluster.workers[workerId].kill();
    }
    }, 5000);
    }
  2. 使用 PM2 管理进程
    • PM2 是一个进程管理工具,可以自动使用 Cluster 模块,并提供监控、日志等功能。
    • 启动命令:

    1
    pm2 start app.js -i max
  3. 结合其他技术
    • 使用 Nginx 或 HAProxy 进行反向代理和负载均衡。
    • 结合 Redis 或消息队列(如 RabbitMQ)实现进程间通信。


七、适用场景

  1. Web 服务器
    • 处理大量并发 HTTP 请求。
  2. 实时应用
    • 如聊天服务器、实时数据推送。
  3. CPU 密集型任务
    • 如加密解密、图像处理。

通过 Cluster 模块,可以显著提升 Node.js 应用在多核 CPU 环境下的性能,但需要根据实际场景合理配置和优化。

getServerSideProps(SSR)

📌 服务器端渲染(Server-Side Rendering, SSR)

每次请求 都会 运行在服务器端
适用于 动态数据(如用户定制内容)

getStaticProps(SSG)

📌 静态生成(Static Site Generation, SSG)

生成 HTML 时 运行,仅在 构建时 执行
适用于 静态数据(如博客文章)

getInitialProps 是什么?

📌 getInitialProps 是 Next.js 早期的数据获取方法,可用于 页面级组件,但 官方不推荐在新项目中使用,建议改用 getServerSideProps 或 getStaticProps。

Next.js 面试题 3

问题:Next.js 如何处理 API 路由?API 路由与传统 Express/Koa 的区别是什么?


1️⃣ Next.js API 路由是什么?

📌 Next.js 提供内置的 API 路由,可以在 pages/api/ 目录下创建 API 处理请求,类似于 Express/Koa 后端。

示例pages/api/hello.js):

1
2
3
export default function handler(req, res) {
res.status(200).json({ message: "Hello, Next.js API!" });
}

📌 特点

  • 运行在服务器端(不会被前端打包)
  • 自动处理 JSON 解析
  • 支持中间件、CORS 等

2️⃣ API 路由 vs 传统 Express/Koa

特性 Next.js API 路由 Express/Koa
创建方式 pages/api/*.js 需要手动创建 server.js
服务器环境 自动管理(Vercel 无需额外配置) 需要 自己配置服务器
性能 适用于 小型 API 更适合 复杂 API
路由 基于文件(自动映射) 需要手动 app.get('/api')

📌 适用场景
✅ 适合 小型项目(无需单独后端)
前后端一体化(如 SSR、静态生成 + API)
不适合复杂 API(如 WebSocket、流式数据)


3️⃣ 处理 POST 请求

示例pages/api/user.js):

1
2
3
4
5
6
7
export default function handler(req, res) {
if (req.method === "POST") {
const { name } = req.body;
return res.status(201).json({ message: `User ${name} created` });
}
res.status(405).json({ message: "Method Not Allowed" });
}

📌 特点

  • 支持 req.body 解析(Next.js 自动解析 JSON)
  • 需要手动检查 req.method

4️⃣ 在前端调用 API

使用 fetch
1
2
3
4
5
async function fetchData() {
const res = await fetch("/api/hello");
const data = await res.json();
console.log(data); // { message: "Hello, Next.js API!" }
}

5️⃣ API 路由支持动态参数

动态 API 路由(pages/api/user/[id].js):

1
2
3
4
export default function handler(req, res) {
const { id } = req.query;
res.status(200).json({ message: `User ID: ${id}` });
}

📌 访问方式

1
GET /api/user/123

📌 返回

1
{ "message": "User ID: 123" }

6️⃣ 面试官可能的追问

  1. Next.js API 适合做什么?

    • ✅ 适合 小型 API(如身份验证、数据处理)
    • ❌ 不适合 长连接(如 WebSocket),可以使用 外部 APInext.config.js 配置代理
  2. 如何在 API 路由中连接数据库?

    • 可以使用 Prisma / Mongoose / Knex.js
      1
      2
      3
      4
      5
      6
      import { prisma } from "@/lib/prisma";

      export default async function handler(req, res) {
      const users = await prisma.user.findMany();
      res.status(200).json(users);
      }
  3. 如何处理 CORS 跨域?

    • 简单方式:在 res.setHeader 添加 CORS 头:
      1
      2
      res.setHeader("Access-Control-Allow-Origin", "*");
      res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    • 更好的方式:使用 next.config.js 配置 API 代理,避免 CORS 问题:
      1
      2
      3
      4
      5
      6
      7
      module.exports = {
      async rewrites() {
      return [
      { source: "/api/:path*", destination: "https://external-api.com/:path*" },
      ];
      },
      };

动态路由

📌 动态路由 允许根据 URL 中的动态部分生成不同的页面。例如,URL /posts/[id] 可以匹配 /posts/1、/posts/2 等不同的页面。

Next.js 支持 文件系统路由,通过在 pages 文件夹中创建带有 方括号 的文件来定义动态路由。

1
2
3
/pages
/posts
[id].js

// 在这个例子中,[id].js 会匹配 /posts/1、/posts/2 等路径。动态部分是 id,它会成为 query 参数传递给页面组件。

动态路由的实现方法

📌 在 pages/posts/[id].js 中访问动态路由参数:

1
2
3
4
5
6
7
8
9
import { useRouter } from 'next/router';

export default function Post() {
const router = useRouter();
const { id } = router.query;

return <div>Post ID: {id}</div>;
}

实现带多个动态参数的路由?

📌 多个动态参数:你可以在文件名中定义多个动态部分,Next.js 会根据路由中不同的部分匹配它们。

获取动态路由数据

在动态路由中,你可能需要 获取远程数据。可以结合 getServerSideProps 或 getStaticProps 使用,以在请求页面时获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// pages/posts/[id].js
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.example.com/posts/${id}`);
const data = await res.json();

return {
props: { post: data },
};
}

export default function Post({ post }) {
return <div>{post.title}</div>;
}
// context.params 包含路由参数(如 { id: '1' })。
// getServerSideProps 在每次请求时运行,适用于需要实时数据的动态页面。

Incremental Static Regeneration (ISR)?

📌 Incremental Static Regeneration (ISR) 是 Next.js 的一项强大功能,它允许你在构建完成之后,更新静态页面,而无需重新构建整个站点。这意味着即使在应用运行后,静态页面也可以根据需要被重新生成,并且可以 增量更新,确保静态内容保持最新。

工作原理:

首次构建时:Next.js 会生成静态页面并将其保存下来。
增量更新:当一个页面请求到达时,Next.js 会检查该页面是否需要更新。如果页面需要更新,它会重新生成页面并替换旧的页面,同时在后台处理。
ISR 的关键点是:通过增量的方式更新静态页面,而无需重新构建整个站点。

启用 ISR?

ISR 是通过在 getStaticProps 方法中配置 revalidate 参数来启用的。revalidate 指定了静态页面重新生成的间隔时间(以秒为单位)。如果在 revalidate 时间内有用户请求访问该页面,Next.js 将重新生成该页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// pages/index.js
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

return {
props: { posts },
revalidate: 60, // 每60秒重新生成一次页面
};
}

export default function HomePage({ posts }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}

0%