数据库优化技术 DatabaseOptimizationTechniques

数据库优化技术是一系列用于提升数据库查询性能、降低资源消耗、确保数据高效访问的方法和策略。关键词包括查询优化、索引策略、连接池、缓存策略、数据库维护等。

数据工程 0 次安装 0 次浏览 更新于 3/5/2026

数据库优化技术

概览

数据库优化涉及提高查询性能、减少资源消耗和确保高效的数据访问模式。这项技能包括查询分析、索引策略、缓存和维护实践。

前提条件

  • 理解SQL和数据库操作
  • 了解数据库模式设计原则
  • 熟悉数据库管理工具
  • 基本了解性能监控

关键概念

性能优化领域

  1. 查询优化:编写高效的SQL查询
  2. 索引策略:创建和维护索引
  3. 连接管理:高效的连接池
  4. 缓存:减少数据库负载
  5. 模式设计:适当的规范化和反规范化
  6. 维护:定期的数据库维护任务

性能指标

  • 查询响应时间:执行查询的时间
  • 吞吐量:每秒查询数
  • 资源使用:CPU、内存、I/O
  • 锁争用:等待锁的时间
  • 缓存命中率:从缓存中服务的查询百分比

实施指南

查询优化

EXPLAIN分析

-- 基本EXPLAIN
EXPLAIN
SELECT * FROM users WHERE email = 'user@example.com';

-- EXPLAIN带实际执行(PostgreSQL)
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';

-- EXPLAIN带缓冲区(PostgreSQL)
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM users WHERE email = 'user@example.com';

-- EXPLAIN在MySQL中
EXPLAIN
SELECT * FROM users WHERE email = 'user@example.com';

-- EXPLAIN在MongoDB中
db.users.find({ email: 'user@example.com' }).explain('executionStats');

查询规划

-- 理解查询计划
-- 分析的关键指标:
-- - 顺序扫描与索引扫描
-- - 过滤条件
-- - 连接类型(嵌套循环、哈希连接、合并连接)
-- - 排序操作
-- - 聚合操作

-- 示例:分析复杂查询
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 10;

-- 查找:
-- 1. 大表上的顺序扫描(应使用索引)
-- 2. 高成本估计
-- 3. 被物化的大型结果集
-- 4. 低效的连接策略

连接优化

-- 好:使用索引列进行连接
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- 好:在索引列上进行连接
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';

-- 坏:在非索引列上进行连接
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.email = o.user_email
WHERE o.status = 'completed';

-- 好:使用适当的连接类型
-- INNER JOIN:仅匹配行
-- LEFT JOIN:左侧全部,右侧匹配
-- RIGHT JOIN:右侧全部,左侧匹配
-- FULL JOIN:两个表的全部

-- 好:连接前先过滤
SELECT u.name, o.total
FROM users u
INNER JOIN (
    SELECT user_id, total
    FROM orders
    WHERE status = 'completed'
) o ON u.id = o.user_id;

索引策略

B-Tree索引

-- 单列索引
CREATE INDEX idx_users_email ON users(email);

-- 多列(复合)索引
CREATE INDEX idx_users_name_email ON users(name, email);

-- 带顺序的索引
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);

-- 唯一索引
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- 部分索引(仅索引子集的行)
CREATE INDEX idx_active_users ON users(email) WHERE active = true;

-- 表达式索引(索引计算值)
CREATE INDEX idx_users_lower_email ON users(LOWER(email));

-- 覆盖索引(包括非索引列)
-- PostgreSQL
CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);

复合索引

-- 好:按选择性排序列
-- 最具选择性的列放在最前面
CREATE INDEX idx_users_status_created ON users(status, created_at);

-- 好:将查询顺序与索引顺序匹配
-- 此查询使用索引:
SELECT * FROM users WHERE status = 'active' AND created_at > '2024-01-01';

-- 此查询不能有效地使用索引:
SELECT * FROM users WHERE created_at > '2024-01-01' AND status = 'active';

-- 好:为不同的查询模式创建多个索引
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created ON users(created_at);
CREATE INDEX idx_users_status_created ON users(status, created_at);

-- 坏:复合索引中列太多
CREATE INDEX idx_users_all ON users(name, email, status, created_at, updated_at);
-- 只有前几列被有效使用

部分索引

-- 仅索引活跃用户
CREATE INDEX idx_active_users_email ON users(email) WHERE active = true;

-- 仅索引近期订单
CREATE INDEX idx_recent_orders ON orders(user_id) WHERE created_at > NOW() - INTERVAL '30 days';

-- 仅索引高价值订单
CREATE INDEX idx_high_value_orders ON orders(user_id) WHERE total > 1000;

-- 好处:
-- - 更小的索引大小
-- - 更快的索引扫描
-- - 维护开销更小
-- - 更适合过滤查询

索引维护

-- 重建索引(PostgreSQL)
REINDEX INDEX idx_users_email;

-- 重建表上的所有索引(PostgreSQL)
REINDEX TABLE users;

-- 分析表统计信息(PostgreSQL)
ANALYZE users;

-- 分析表(MySQL)
ANALYZE TABLE users;

-- 优化表(MySQL)
OPTIMIZE TABLE users;

-- 检查索引使用情况(PostgreSQL)
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;

-- 查找未使用的索引(PostgreSQL)
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexname NOT LIKE '%_pkey';

连接池

连接池配置

// Prisma连接池
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // 连接池设置
  connection_limit = 10  // 最大连接数
  pool_timeout = 20      // 等待连接的秒数
}

// PostgreSQL池配置
const pool = new Pool({
  host: 'localhost',
  port: 5432,
  database: 'mydb',
  user: 'postgres',
  password: 'password',
  max: 20,              // 最大池大小
  min: 2,               // 最小池大小
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

连接池最佳实践

// 好:使用连接池
const pool = new Pool({
  max: 20,
  min: 2,
  idleTimeoutMillis: 30000,
});

// 坏:为每个查询创建新连接
async function getUser(id: string) {
  const client = new Client({ /* config */ });
  await client.connect();
  const result = await client.query('SELECT * FROM users WHERE id = $1', [id]);
  await client.end();
  return result.rows[0];
}

// 好:使用池进行查询
async function getUser(id: string) {
  const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
  return result.rows[0];
}

N+1查询问题

识别N+1问题

// 坏:N+1查询问题
async function getUsersWithOrders() {
  const users = await db.user.findMany(); // 1个查询

  const usersWithOrders = [];
  for (const user of users) {
    const orders = await db.order.findMany({ // N个查询
      where: { userId: user.id }
    });
    usersWithOrders.push({ ...user, orders });
  }

  return usersWithOrders;
}

// 总查询数:1 + N(N是用户数)

使用连接解决N+1

// 好:使用JOIN
async function getUsersWithOrders() {
  const result = await db.$queryRaw`
    SELECT
      u.id, u.name, u.email,
      o.id as order_id, o.total, o.status
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
  `;

  // 转换结果为嵌套结构
  const usersMap = new Map();
  for (const row of result) {
    if (!usersMap.has(row.id)) {
      usersMap.set(row.id, {
        id: row.id,
        name: row.name,
        email: row.email,
        orders: []
      });
    }
    if (row.order_id) {
      usersMap.get(row.id).orders.push({
        id: row.order_id,
        total: row.total,
        status: row.status
      });
    }
  }

  return Array.from(usersMap.values());
}

// 总查询数:1

使用Include解决N+1

// 好:Prisma include
async function getUsersWithOrders() {
  return await db.user.findMany({
    include: {
      orders: true
    }
  });
}

// 好:Mongoose populate
async function getUsersWithOrders() {
  return await User.find().populate('orders');
}

使用批量加载解决N+1

// 好:批量加载
async function getUsersWithOrders() {
  const users = await db.user.findMany();
  const userIds = users.map(u => u.id);

  const orders = await db.order.findMany({
    where: { userId: { in: userIds } }
  });

  const ordersMap = new Map();
  orders.forEach(order => {
    if (!ordersMap.has(order.userId)) {
      ordersMap.set(order.userId, []);
    }
    ordersMap.get(order.userId).push(order);
  });

  return users.map(user => ({
    ...user,
    orders: ordersMap.get(user.id) || []
  }));
}

// 总查询数:2

缓存策略

查询结果缓存

// 使用Redis缓存查询结果
import { redisClient } from './redis';

async function getCachedUser(userId: string) {
  const cacheKey = `user:${userId}`;

  // 先尝试缓存
  const cached = await redisClient.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // 缓存未命中 - 查询数据库
  const user = await db.user.findUnique({ where: { id: userId } });

  // 缓存存储1小时TTL
  await redisClient.set(cacheKey, JSON.stringify(user), {
    EX: 3600
  });

  return user;
}

缓存失效

// 更新时使缓存失效
async function updateUser(userId: string, data: any) {
  const user = await db.user.update({
    where: { id: userId },
    data
  });

  // 使缓存失效
  await redisClient.del(`user:${userId}`);

  return user;
}

// 使多个缓存失效
async function invalidateUserCaches(userId: string) {
  await redisClient.del(`user:${userId}`);
  await redisClient.del(`user:${userId}:profile`);
  await redisClient.del(`user:${userId}:orders`);
}

旁路缓存模式

// 旁路缓存实现
class CacheAside {
  async get(key: string) {
    // 1. 尝试缓存
    const cached = await redisClient.get(key);
    if (cached) {
      return JSON.parse(cached);
    }

    // 2. 缓存未命中 - 从数据库加载
    const data = await this.loadFromDatabase(key);

    // 3. 填充缓存
    await redisClient.set(key, JSON.stringify(data), { EX: 3600 });

    return data;
  }

  async set(key: string, data: any) {
    // 更新数据库
    await this.saveToDatabase(key, data);

    // 更新缓存
    await redisClient.set(key, JSON.stringify(data), { EX: 3600 });
  }

  async delete(key: string) {
    // 从数据库删除
    await this.deleteFromDatabase(key);

    // 使缓存失效
    await redisClient.del(key);
  }
}

必要时去规范化

何时去规范化

-- 好:为读重负载去规范化
-- 添加冗余数据以避免连接

-- 规范化模式
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    email VARCHAR(255)
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(10, 2),
    status VARCHAR(50),
    created_at TIMESTAMP
);

-- 快速读取的去规范化模式
CREATE TABLE orders_denormalized (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    user_name VARCHAR(255),  -- 去规范化
    user_email VARCHAR(255),   -- 去规范化
    total DECIMAL(10, 2),
    status VARCHAR(50),
    created_at TIMESTAMP
);

-- 好:对于复杂聚合使用物化视图
CREATE MATERIALIZED VIEW user_order_summary AS
SELECT
    u.id,
    u.name,
    u.email,
    COUNT(o.id) as order_count,
    SUM(o.total) as total_spent,
    MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name, u.email;

-- 刷新物化视图
REFRESH MATERIALIZED VIEW user_order_summary;

权衡

-- 去规范化的好处:
-- - 更快的读取查询(无连接)
-- - 更简单的查询
-- - 更适合报告

-- 去规范化的缺点:
-- - 数据冗余
-- - 更新异常
-- - 存储增加
-- - 复杂的数据同步

分区

表分区(PostgreSQL)

-- 按日期范围分区
CREATE TABLE orders (
    id SERIAL,
    user_id INTEGER,
    total DECIMAL(10, 2),
    status VARCHAR(50),
    created_at TIMESTAMP
) PARTITION BY RANGE (created_at);

-- 创建分区
CREATE TABLE orders_2024_q1 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE orders_2024_q2 PARTITION OF orders
    FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

CREATE TABLE orders_2024_q3 PARTITION OF orders
    FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');

CREATE TABLE orders_2024_q4 PARTITION OF orders
    FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');

-- 列表分区
CREATE TABLE orders_by_status (
    id SERIAL,
    user_id INTEGER,
    total DECIMAL(10, 2),
    status VARCHAR(50),
    created_at TIMESTAMP
) PARTITION BY LIST (status);

CREATE TABLE orders_active PARTITION OF orders_by_status
    FOR VALUES IN ('pending', 'processing');

CREATE TABLE orders_completed PARTITION OF orders_by_status
    FOR VALUES IN ('completed');

CREATE TABLE orders_cancelled PARTITION OF orders_by_status
    FOR VALUES IN ('cancelled', 'refunded');

-- 哈希分区
CREATE TABLE orders_hash (
    id SERIAL,
    user_id INTEGER,
    total DECIMAL(10, 2),
    status VARCHAR(50),
    created_at TIMESTAMP
) PARTITION BY HASH (user_id);

CREATE TABLE orders_hash_0 PARTITION OF orders_hash
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE orders_hash_1 PARTITION OF orders_hash
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE orders_hash_2 PARTITION OF orders_hash
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE orders_hash_3 PARTITION OF orders_hash
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

分区修剪

-- 查询受益于分区修剪
-- 仅扫描相关分区

-- 好:带有分区键的查询
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-04-01';
-- 仅扫描orders_2024_q1分区

-- 坏:没有分区键的查询
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE total > 1000;
-- 扫描所有分区

数据库维护

真空和分析

-- 真空(PostgreSQL)
-- 回收存储空间并更新统计信息
VACUUM users;

-- 真空带分析
VACUUM ANALYZE users;

-- 真空所有表
VACUUM ANALYZE;

-- 真空特定表带完整选项
VACUUM FULL users;  -- 回收更多空间但锁定表

-- 自动真空配置(PostgreSQL)
ALTER SYSTEM SET autovacuum = on;
ALTER SYSTEM SET autovacuum_vacuum_threshold = 50;
ALTER SYSTEM SET autovacuum_analyze_threshold = 50;
SELECT pg_reload_conf();

重新索引

-- 重新索引单个索引(PostgreSQL)
REINDEX INDEX idx_users_email;

-- 重新索引表上的所有索引(PostgreSQL)
REINDEX TABLE users;

-- 同时重新索引(PostgreSQL)
-- 不锁定表但需要更长时间
REINDEX INDEX CONCURRENTLY idx_users_email;

表优化

-- 优化表(MySQL)
OPTIMIZE TABLE users;

-- 优化表(SQLite)
VACUUM;

-- 分析表(MySQL)
ANALYZE TABLE users;

-- 检查表完整性(MySQL)
CHECK TABLE users;

监控查询

慢查询日志

-- 启用慢查询日志(PostgreSQL)
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 记录> 1秒的查询
SELECT pg_reload_conf();

-- 查看慢查询
SELECT
    query,
    calls,
    total_time,
    mean_time,
    max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- 启用慢查询日志(MySQL)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 记录> 1秒的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';

查询性能监控

-- 查找最耗时的查询(PostgreSQL)
SELECT
    query,
    calls,
    total_exec_time,
    mean_exec_time,
    max_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- 查找最频繁的查询(PostgreSQL)
SELECT
    query,
    calls,
    total_exec_time
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 20;

-- 监控查询执行(MySQL)
SHOW PROFILE;
SHOW PROFILE FOR QUERY 1;

数据库指标

-- 连接使用情况(PostgreSQL)
SELECT
    state,
    COUNT(*) as count
FROM pg_stat_activity
GROUP BY state;

-- 表大小(PostgreSQL)
SELECT
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

-- 索引大小(PostgreSQL)
SELECT
    schemaname,
    tablename,
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;

常见反模式

SELECT *

-- 坏:SELECT *检索所有列
SELECT * FROM users WHERE id = 1;

-- 好:仅选择需要的列
SELECT id, name, email FROM users WHERE id = 1;

-- 好处:
-- - 传输的数据更少
-- - 使用的内存更少
-- - 可以使用覆盖索引

通配符前缀匹配

-- 坏:前缀通配符阻止索引使用
SELECT * FROM users WHERE name LIKE '%Smith%';

-- 好:使用全文搜索
SELECT * FROM users WHERE to_tsvector('english', name) @@ to_tsquery('english', 'Smith');

-- 好:使用trigram扩展(PostgreSQL)
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_users_name_trgm ON users USING gin (name gin_trgm_ops);
SELECT * FROM users WHERE name LIKE '%Smith%';

OR条件

-- 坏:OR条件可以阻止索引使用
SELECT * FROM users WHERE email = 'user@example.com' OR name = 'John';

-- 好:使用UNION
SELECT * FROM users WHERE email = 'user@example.com'
UNION
SELECT * FROM users WHERE name = 'John';

-- 好:使用IN子句
SELECT * FROM users WHERE email IN ('user@example.com', 'user2@example.com');

子查询

-- 坏:子查询可能效率低下
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);

-- 好:使用JOIN
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 1000;

-- 好:使用EXISTS
SELECT u.*
FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.user_id = u.id AND o.total > 1000
);

索引列上的函数

-- 坏:索引列上的函数阻止索引使用
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';

-- 好:存储数据时使用规范化形式
SELECT * FROM users WHERE email = 'user@example.com';

-- 好:使用表达式索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';

大事务

// 坏:大事务持有锁太久
async function processLargeBatch() {
  const transaction = await db.$transaction([
    db.user.create({ data: user1 }),
    db.user.create({ data: user2 }),
    db.user.create({ data: user3 }),
    // ... 1000多个操作
  ]);
}

// 好:分批处理
async function processInBatches(users: any[]) {
  const batchSize = 100;
  for (let i = 0; i < users.length; i += batchSize) {
    const batch = users.slice(i, i + batchSize);
    await db.$transaction(
      batch.map(user => db.user.create({ data: user }))
    );
  }
}

缺少索引

-- 坏:频繁查询的列上没有索引
SELECT * FROM orders WHERE user_id = 123;

-- 好:创建索引
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- 坏:外键上没有索引
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),  -- 没有索引
    total DECIMAL(10, 2)
);

-- 好:始终索引外键
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(10, 2)
);
CREATE INDEX idx_orders_user_id ON orders(user_id);

过度索引

-- 坏:太多索引会减慢写入
CREATE INDEX idx_users_name ON users(name);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created ON users(created_at);
CREATE INDEX idx_users_updated ON users(updated_at);
CREATE INDEX idx_users_name_email ON users(name, email);
CREATE INDEX idx_users_email_name ON users(email, name);
-- ... 更多索引

-- 好:只创建必要的索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status_created ON users(status, created_at);

最佳实践

  1. 查询优化

    • 使用EXPLAIN分析查询计划
    • 只选择需要的列
    • 使用适当的连接类型
    • 避免在索引列上使用函数
  2. 索引策略

    • 在频繁查询的列上创建索引
    • 为多列查询使用复合索引
    • 考虑为过滤查询使用部分索引
    • 监控并移除未使用的索引
  3. 连接管理

    • 使用连接池
    • 配置适当的池大小
    • 设置连接超时
    • 监控连接使用情况
  4. 缓存

    • 缓存频繁访问的数据
    • 实施适当的缓存失效
    • 使用适当的TTL值
    • 监控缓存命中率
  5. 维护

    • 定期进行真空和分析操作
    • 监控慢查询
    • 定期重建索引
    • 保持统计信息更新

相关技能