Joe


年少不知愁滋味,老来方知行路难

进入博客 >

Joe

Joe

年少不知愁滋味,老来方知行路难
  • 文章 105篇
  • 评论 1条
  • 分类 5个
  • 标签 15个
2020-03-01

SQL高级查询优化技巧

SQL高级查询优化技巧:从执行计划到索引设计

0x00 前言:数据库性能优化的核心战场

在现代应用架构中,数据库往往是性能瓶颈的主战场。一个未优化的SQL查询可能将响应时间从毫秒级拖慢到秒级,甚至引发雪崩效应导致整个系统崩溃。作为开发者,我们必须像调试复杂算法一样,以系统化的思维审视每一条SQL语句。

数据库查询优化不是玄学,而是基于:

  • 存储引擎的物理实现:B+树、LSM树的数据结构特性
  • 查询优化器的成本模型:基于统计信息的执行计划选择
  • 操作系统的I/O机制:顺序读写与随机读写的巨大差异

本文将从程序员的视角,深入剖析SQL优化的底层原理与实战技巧。我们不仅要知道"怎么做",更要理解"为什么"。

0x01 执行计划:数据库的汇编代码

1.1 EXPLAIN:查询优化的第一步

就像分析程序性能要看CPU火焰图一样,优化SQL必须从执行计划入手。执行计划(Execution Plan)是数据库查询优化器生成的"汇编代码",它揭示了数据库如何执行你的查询。

-- MySQL中查看执行计划
EXPLAIN SELECT u.name, o.amount 
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
  AND o.created_at > '2026-01-01';

典型的执行计划输出(MySQL):

+----+-------------+-------+------+---------------+---------+---------+----------+------+-------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref      | rows | Extra       |
+----+-------------+-------+------+---------------+---------+---------+----------+------+-------------+
|  1 | SIMPLE      | u     | ref  | PRIMARY,idx_s | idx_s   | 50      | const    | 1000 | Using where |
|  1 | SIMPLE      | o     | ref  | idx_user      | idx_user| 8       | u.id     | 5    | Using where |
+----+-------------+-------+------+---------------+---------+---------+----------+------+-------------+

1.2 关键字段解读

type字段:访问类型的性能阶梯

从最优到最差排列:

  • system/const:常量查询,O(1)时间复杂度

    SELECT * FROM users WHERE id = 123;  -- const
  • eq_ref:唯一索引查找,JOIN时的最优情况

    SELECT * FROM orders o
    JOIN users u ON o.user_id = u.id;  -- u表是eq_ref
  • ref:非唯一索引查找,返回匹配某个值的所有行

    SELECT * FROM users WHERE status = 'active';  -- ref
  • range:索引范围扫描,使用<、>、BETWEEN等

    SELECT * FROM orders WHERE created_at > '2026-01-01';  -- range
  • index:全索引扫描,比全表扫描快但仍需遍历整个索引

    SELECT id FROM users;  -- 如果id有索引,使用index
  • ALL:全表扫描,O(n)时间复杂度,性能最差

    SELECT * FROM users WHERE YEAR(created_at) = 2026;  -- ALL

Extra字段:隐藏的性能杀手

  • Using index:覆盖索引,无需回表查询(最优)
  • Using where:在存储引擎层过滤数据
  • Using temporary:使用临时表,性能杀手
  • Using filesort:需要额外排序操作,内存或磁盘排序

1.3 PostgreSQL的执行计划

PostgreSQL提供更详细的成本分析:

EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) 
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

输出示例:

HashAggregate  (cost=1234.56..1250.78 rows=1000 width=64) (actual time=15.234..15.678 rows=1000 loops=1)
  Group Key: u.id, u.name
  ->  Hash Left Join  (cost=100.00..1200.00 rows=5000 width=72) (actual time=2.345..12.456 rows=5000 loops=1)
        Hash Cond: (o.user_id = u.id)
        ->  Seq Scan on orders o  (cost=0.00..1000.00 rows=5000 width=8)
        ->  Hash  (cost=50.00..50.00 rows=1000 width=64)
              ->  Seq Scan on users u  (cost=0.00..50.00 rows=1000 width=64)
Planning Time: 0.234 ms
Execution Time: 15.890 ms

关键指标:

  • cost:查询优化器估算的成本(启动成本..总成本)
  • rows:预估返回行数
  • actual time:实际执行时间
  • loops:该操作执行的次数

0x02 索引设计:数据结构的艺术

2.1 B+树索引:关系型数据库的基石

B+树是关系型数据库索引的默认实现,理解其结构是优化查询的基础。

B+树的核心特性

  1. 非叶子节点只存储键值:用于导航
  2. 叶子节点存储完整数据:形成有序链表
  3. 高度低:3-4层可存储百万级数据
  4. 范围查询高效:叶子节点链表结构
        [30, 60]              -- 根节点(非叶子)
       /    |    \
  [10,20] [40,50] [70,80]     -- 非叶子节点
   /  |  \
[1-9][11-19][21-29]...        -- 叶子节点(包含数据或指针)
  ↓      ↓      ↓
[data] [data] [data]          -- 实际数据行

索引查找的时间复杂度

  • 精确查找:O(log n),树高度决定
  • 范围查询:O(log n + k),k为结果集大小
  • 全表扫描:O(n)

2.2 索引设计原则

原则1:选择性高的列优先

选择性(Selectivity)= 不同值数量 / 总行数

-- 计算列的选择性
SELECT 
  COUNT(DISTINCT email) / COUNT(*) AS email_selectivity,
  COUNT(DISTINCT status) / COUNT(*) AS status_selectivity
FROM users;

-- 结果示例:
-- email_selectivity: 0.99 (高选择性,适合索引)
-- status_selectivity: 0.01 (低选择性,不适合单独索引)

经验法则:选择性 > 0.1 才考虑建立索引。

原则2:复合索引的列顺序

遵循最左前缀匹配原则:

-- 创建复合索引
CREATE INDEX idx_user_status_created ON users(status, created_at, age);

-- 能使用索引的查询
SELECT * FROM users WHERE status = 'active';  -- ✓ 使用索引第一列
SELECT * FROM users WHERE status = 'active' AND created_at > '2026-01-01';  -- ✓ 使用前两列
SELECT * FROM users WHERE status = 'active' AND created_at > '2026-01-01' AND age > 18;  -- ✓ 使用全部列

-- 不能使用索引的查询
SELECT * FROM users WHERE created_at > '2026-01-01';  -- ✗ 跳过第一列
SELECT * FROM users WHERE age > 18;  -- ✗ 跳过前两列

列顺序优化策略

  1. 等值条件优先=IN 的列放在前面
  2. 范围条件靠后><BETWEEN 的列放在后面
  3. 选择性高的列优先:区分度大的列放在前面

原则3:覆盖索引最小化回表

覆盖索引(Covering Index)包含查询所需的所有列,无需回表查询:

-- 创建覆盖索引
CREATE INDEX idx_cover ON orders(user_id, status, amount, created_at);

-- 覆盖索引查询(EXPLAIN显示"Using index")
SELECT user_id, status, amount 
FROM orders 
WHERE user_id = 123 AND status = 'completed';

-- 需要回表的查询
SELECT user_id, status, amount, shipping_address  -- shipping_address不在索引中
FROM orders 
WHERE user_id = 123;

2.3 特殊索引类型

哈希索引:等值查询的极致优化

-- PostgreSQL中创建哈希索引
CREATE INDEX idx_hash_email ON users USING HASH (email);

-- 适用场景
SELECT * FROM users WHERE email = 'user@example.com';  -- O(1)

-- 不适用场景
SELECT * FROM users WHERE email LIKE 'user%';  -- 无法使用哈希索引

特点

  • 等值查询O(1)
  • 不支持范围查询
  • 不支持排序
  • 哈希冲突时性能下降

全文索引:文本搜索的利器

-- MySQL全文索引
CREATE FULLTEXT INDEX idx_fulltext ON articles(title, content);

-- 使用全文搜索
SELECT * FROM articles 
WHERE MATCH(title, content) AGAINST('database optimization' IN NATURAL LANGUAGE MODE);

-- 布尔模式
SELECT * FROM articles 
WHERE MATCH(title, content) AGAINST('+mysql -oracle' IN BOOLEAN MODE);

部分索引(Partial Index)

PostgreSQL支持,MySQL 8.0+支持函数索引:

-- PostgreSQL部分索引:只索引活跃用户
CREATE INDEX idx_active_users ON users(created_at) 
WHERE status = 'active';

-- 查询时必须包含过滤条件才能使用索引
SELECT * FROM users 
WHERE status = 'active' AND created_at > '2026-01-01';

0x03 JOIN优化:多表关联的性能陷阱

3.1 JOIN算法解析

数据库引擎有三种主要的JOIN算法:

Nested Loop Join(嵌套循环)

# 伪代码
def nested_loop_join(table_a, table_b, join_condition):
    result = []
    for row_a in table_a:  # 外层循环
        for row_b in table_b:  # 内层循环
            if join_condition(row_a, row_b):
                result.append(merge(row_a, row_b))
    return result

时间复杂度:O(n * m)

适用场景:小表驱动大表,且内表有索引

-- 100行的小表驱动10000行的大表,大表有索引
SELECT * FROM small_table s
JOIN large_table l ON s.id = l.small_id;
-- 实际复杂度:O(100 * log(10000)) ≈ 100 * 13 = 1300次查找

Hash Join(哈希连接)

# 伪代码
def hash_join(table_a, table_b, join_key):
    # 构建阶段:小表构建哈希表
    hash_map = {}
    for row in table_a:  # O(n)
        key = row[join_key]
        hash_map[key] = hash_map.get(key, []) + [row]
    
    # 探测阶段:大表探测哈希表
    result = []
    for row in table_b:  # O(m)
        key = row[join_key]
        if key in hash_map:  # O(1)
            for matched_row in hash_map[key]:
                result.append(merge(matched_row, row))
    return result

时间复杂度:O(n + m)

适用场景:两表都较大,且无合适索引

Merge Join(归并连接)

# 伪代码(假设两表已排序)
def merge_join(table_a, table_b, join_key):
    result = []
    i, j = 0, 0
    while i < len(table_a) and j < len(table_b):
        if table_a[i][join_key] == table_b[j][join_key]:
            result.append(merge(table_a[i], table_b[j]))
            j += 1
        elif table_a[i][join_key] < table_b[j][join_key]:
            i += 1
        else:
            j += 1
    return result

时间复杂度:O(n log n + m log m + n + m)

适用场景:两表都已按JOIN键排序

3.2 JOIN优化策略

策略1:小表驱动大表

-- 错误示例:大表驱动小表
SELECT * FROM large_orders o  -- 1000万行
JOIN small_users u ON o.user_id = u.id  -- 1万行
WHERE u.status = 'active';

-- 优化:改为子查询或强制JOIN顺序
SELECT * FROM small_users u
JOIN large_orders o ON u.id = o.user_id
WHERE u.status = 'active';

-- MySQL强制驱动表顺序
SELECT STRAIGHT_JOIN * FROM small_users u
JOIN large_orders o ON u.id = o.user_id;

策略2:避免JOIN中的函数运算

-- 错误:JOIN条件中使用函数
SELECT * FROM orders o
JOIN users u ON DATE(o.created_at) = u.register_date;
-- 问题:o.created_at上的索引失效

-- 优化:将函数移到WHERE子句或预先计算
SELECT * FROM orders o
JOIN users u ON o.created_at >= u.register_date 
            AND o.created_at < DATE_ADD(u.register_date, INTERVAL 1 DAY);

策略3:分批JOIN大数据集

-- 原查询:一次性JOIN百万级数据
SELECT * FROM huge_table_a a
JOIN huge_table_b b ON a.id = b.a_id;

-- 优化:按分区或范围分批处理
SELECT * FROM huge_table_a a
JOIN huge_table_b b ON a.id = b.a_id
WHERE a.created_at >= '2026-01-01' 
  AND a.created_at < '2026-02-01';

0x04 子查询与临时表:内存与磁盘的博弈

4.1 子查询优化

标量子查询的性能陷阱

-- 反模式:在SELECT中使用相关子查询(N+1问题)
SELECT 
  u.name,
  (SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count
FROM users u;
-- 问题:每行users都执行一次子查询,O(n²)

-- 优化:改为JOIN
SELECT 
  u.name,
  COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- 复杂度:O(n + m)

IN子查询 vs EXISTS

-- IN子查询:先执行子查询,生成结果集
SELECT * FROM users 
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);

-- EXISTS:对外表每行执行子查询(但可以提前终止)
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000);

选择策略

  • 子查询结果集小:使用 IN
  • 外表结果集小:使用 EXISTS
  • 子查询有索引:优先 EXISTS

4.2 临时表的使用与优化

显式临时表

-- 复杂查询分解为多步
CREATE TEMPORARY TABLE temp_active_users AS
SELECT id, name, email 
FROM users 
WHERE status = 'active' AND created_at > '2025-01-01';

CREATE INDEX idx_temp_id ON temp_active_users(id);

SELECT t.name, COUNT(o.id) AS order_count
FROM temp_active_users t
JOIN orders o ON t.id = o.user_id
GROUP BY t.id, t.name;

优势

  • 减少重复计算
  • 可以为临时表建立索引
  • 分解复杂查询,提升可维护性

CTE(公用表表达式)

-- 递归查询:组织层级结构
WITH RECURSIVE org_hierarchy AS (
  -- 基础查询
  SELECT id, name, parent_id, 1 AS level
  FROM departments
  WHERE parent_id IS NULL
  
  UNION ALL
  
  -- 递归查询
  SELECT d.id, d.name, d.parent_id, h.level + 1
  FROM departments d
  JOIN org_hierarchy h ON d.parent_id = h.id
)
SELECT * FROM org_hierarchy;

注意事项

  • CTE结果不会自动索引
  • 递归CTE注意终止条件
  • MySQL 8.0+ 才支持CTE

0x05 分页查询:OFFSET的性能灾难

5.1 传统分页的问题

-- 深度分页的性能灾难
SELECT * FROM articles 
ORDER BY created_at DESC 
LIMIT 1000000, 20;
-- 问题:数据库需要扫描并跳过前100万行

性能测试(以100万行表为例):

LIMIT 0, 20:     0.01秒
LIMIT 10000, 20: 0.05秒
LIMIT 100000, 20: 0.5秒
LIMIT 1000000, 20: 5秒

5.2 游标分页(Cursor-based Pagination)

-- 第一页
SELECT * FROM articles 
WHERE id > 0
ORDER BY id ASC 
LIMIT 20;
-- 返回最后一条记录的id: 20

-- 第二页
SELECT * FROM articles 
WHERE id > 20  -- 使用上一页的最后一条id
ORDER BY id ASC 
LIMIT 20;

优势

  • 性能稳定,O(1)时间复杂度
  • 适合实时数据流
  • 避免跳页带来的数据遗漏

劣势

  • 无法跳转到指定页
  • 需要唯一且递增的游标字段

5.3 延迟关联优化

-- 优化:先通过索引获取id,再回表查询
SELECT a.* 
FROM articles a
JOIN (
  SELECT id FROM articles 
  ORDER BY created_at DESC 
  LIMIT 1000000, 20
) AS tmp ON a.id = tmp.id;

原理

  1. 子查询只扫描索引(covering index),速度快
  2. 外层查询只回表20次
  3. 性能提升:从5秒降低到0.5秒

0x06 GROUP BY与聚合优化

6.1 松散索引扫描(Loose Index Scan)

-- 创建复合索引
CREATE INDEX idx_status_date ON orders(status, created_at);

-- 利用索引优化GROUP BY
SELECT status, MIN(created_at) 
FROM orders 
GROUP BY status;
-- 如果status有索引,数据库可以直接从索引中读取分组结果

EXPLAIN输出

Extra: Using index for group-by (scanning)

6.2 避免隐式排序

-- GROUP BY默认会排序结果
SELECT category, COUNT(*) 
FROM products 
GROUP BY category;
-- MySQL会对category排序

-- 不需要排序时显式指定
SELECT category, COUNT(*) 
FROM products 
GROUP BY category 
ORDER BY NULL;
-- 节省排序开销

6.3 预聚合策略

-- 反模式:实时聚合大量历史数据
SELECT DATE(created_at), COUNT(*) 
FROM orders 
WHERE created_at >= '2020-01-01'
GROUP BY DATE(created_at);
-- 每次查询都扫描数百万行

-- 优化:创建汇总表
CREATE TABLE daily_order_stats (
  stat_date DATE PRIMARY KEY,
  order_count INT,
  total_amount DECIMAL(10,2),
  updated_at TIMESTAMP
);

-- 定时任务维护汇总表
INSERT INTO daily_order_stats (stat_date, order_count, total_amount)
SELECT DATE(created_at), COUNT(*), SUM(amount)
FROM orders
WHERE DATE(created_at) = CURDATE() - INTERVAL 1 DAY
ON DUPLICATE KEY UPDATE 
  order_count = VALUES(order_count),
  total_amount = VALUES(total_amount);

-- 查询直接读取汇总表
SELECT * FROM daily_order_stats 
WHERE stat_date >= '2020-01-01';

0x07 数据库配置与硬件优化

7.1 InnoDB关键参数

# MySQL配置文件 my.cnf

# 缓冲池大小(最重要的参数)
# 建议设置为物理内存的50-80%
innodb_buffer_pool_size = 8G

# 缓冲池实例数(多核CPU优化)
innodb_buffer_pool_instances = 8

# 日志文件大小(影响写入性能)
innodb_log_file_size = 512M

# 刷新策略(安全性与性能权衡)
# 0: 每秒刷新(性能最高,但可能丢失1秒数据)
# 1: 每次事务刷新(最安全,性能最低)
# 2: 每次事务写入OS缓存,每秒刷新(折衷)
innodb_flush_log_at_trx_commit = 2

# I/O线程数
innodb_read_io_threads = 8
innodb_write_io_threads = 8

# 并发线程数(CPU核心数的2倍)
innodb_thread_concurrency = 16

7.2 连接池配置

// HikariCP最佳实践
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("pass");

// 连接池大小计算公式
// connections = ((core_count * 2) + effective_spindle_count)
// 对于4核CPU + 1块SSD:(4 * 2) + 1 = 9
config.setMaximumPoolSize(10);

// 最小空闲连接
config.setMinimumIdle(5);

// 连接超时
config.setConnectionTimeout(30000);

// 空闲超时(10分钟)
config.setIdleTimeout(600000);

// 连接最大生命周期(30分钟)
config.setMaxLifetime(1800000);

// 连接测试查询
config.setConnectionTestQuery("SELECT 1");

7.3 SSD vs HDD的I/O特性

指标HDDSSD
随机读IOPS100-20010,000-100,000
顺序读吞吐100-200 MB/s500-3,500 MB/s
延迟10-20ms0.1-0.2ms

优化建议

  • SSD环境:随机读写性能强,可以更激进地使用索引
  • HDD环境:优先考虑顺序读写,减少随机I/O

0x08 监控与诊断工具

8.1 慢查询日志分析

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 1秒以上的查询记录
SET GLOBAL log_queries_not_using_indexes = 'ON';

-- 查看慢查询统计
SELECT * FROM mysql.slow_log 
ORDER BY query_time DESC 
LIMIT 10;

使用pt-query-digest分析:

pt-query-digest /var/log/mysql/slow.log > slow_report.txt

8.2 性能监控指标

-- 查看索引使用情况
SELECT 
  TABLE_SCHEMA,
  TABLE_NAME,
  INDEX_NAME,
  CARDINALITY,
  SEQ_IN_INDEX
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'mydb';

-- 查看未使用的索引
SELECT * FROM sys.schema_unused_indexes;

-- 查看重复索引
SELECT * FROM sys.schema_redundant_indexes;

-- 监控缓冲池命中率
SHOW STATUS LIKE 'Innodb_buffer_pool%';
-- 目标:命中率 > 99%
-- 命中率 = Innodb_buffer_pool_read_requests / 
--         (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_reads)

8.3 实时性能分析

-- MySQL 5.7+ Performance Schema
SELECT 
  DIGEST_TEXT,
  COUNT_STAR,
  AVG_TIMER_WAIT / 1000000000 AS avg_ms,
  SUM_TIMER_WAIT / 1000000000 AS total_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

0x09 实战案例:电商订单查询优化

9.1 问题场景

-- 原始查询:响应时间5秒
SELECT 
  o.id,
  o.order_no,
  u.name AS user_name,
  u.email,
  o.total_amount,
  o.status,
  COUNT(oi.id) AS item_count
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.created_at >= '2025-01-01'
  AND o.status IN ('pending', 'processing')
  AND u.level = 'VIP'
GROUP BY o.id, o.order_no, u.name, u.email, o.total_amount, o.status
ORDER BY o.created_at DESC
LIMIT 100;

执行计划分析

  • orders表全表扫描(type: ALL)
  • users表索引缺失(type: ALL)
  • Using temporary; Using filesort

9.2 优化步骤

第一步:创建索引

-- orders表复合索引
CREATE INDEX idx_orders_opt ON orders(status, created_at, user_id);

-- users表索引
CREATE INDEX idx_users_level ON users(level, id);

-- order_items表索引
CREATE INDEX idx_items_order ON order_items(order_id);

第二步:拆分聚合

-- 先查询订单(避免GROUP BY)
SELECT 
  o.id,
  o.order_no,
  u.name AS user_name,
  u.email,
  o.total_amount,
  o.status,
  o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at >= '2025-01-01'
  AND o.status IN ('pending', 'processing')
  AND u.level = 'VIP'
ORDER BY o.created_at DESC
LIMIT 100;

-- 再单独查询商品数量(批量IN查询)
SELECT order_id, COUNT(*) AS item_count
FROM order_items
WHERE order_id IN (?, ?, ?, ...)  -- 上一步查询的订单ID
GROUP BY order_id;

第三步:应用层合并结果

// Java代码示例
List<Order> orders = orderMapper.getOrders(params);
List<Long> orderIds = orders.stream()
    .map(Order::getId)
    .collect(Collectors.toList());

Map<Long, Integer> itemCounts = orderMapper.getItemCounts(orderIds);

orders.forEach(order -> 
    order.setItemCount(itemCounts.getOrDefault(order.getId(), 0))
);

9.3 优化结果

  • 响应时间:从5秒降低到50ms
  • 执行计划:全部使用索引查找(type: ref/range)
  • 数据库负载:CPU使用率从80%降低到10%

0x10 总结:优化的系统化方法论

SQL优化不是一次性的工作,而是持续迭代的过程。建立系统化的优化方法论:

10.1 性能优化四步法

  1. 测量:使用EXPLAIN和慢查询日志定位问题
  2. 分析:理解执行计划,找出瓶颈
  3. 优化:应用索引、重写查询、调整配置
  4. 验证:对比优化前后的性能指标

10.2 优化优先级

  1. 索引优化(投入产出比最高)
  2. 查询重写(逻辑优化)
  3. 表结构优化(反范式化、分区)
  4. 硬件升级(SSD、内存扩容)
  5. 架构演进(读写分离、分库分表)

10.3 避免过早优化

  • 不要为所有字段创建索引
  • 不要盲目反范式化
  • 不要在没有性能问题时优化

记住:过早优化是万恶之源,但性能监控永远不嫌早。


参考资料

  1. MySQL官方文档 - Optimization
  2. PostgreSQL Performance Tuning
  3. High Performance MySQL (4th Edition)
  4. Use The Index, Luke! - A Guide To Database Performance
  5. Schwartz, B., Zaitsev, P., & Tkachenko, V. (2012). "High Performance MySQL"

#标签: SQL

- THE END -

非特殊说明,本博所有文章均为博主原创。


暂无评论 >_<