浅聊一下数据库的索引优化

news2025/5/15 14:58:41

背景

这里的索引说的是关系数据库(MSSQL)中的索引。

本篇不是纯技术性的内容,只是聊一次性能调优的经历,包含到一些粗浅的实现和验证手段,所以,大神忽略即可。

额…对了,笔者对数据库的优化手段了解有限,文中若有原则性或者概念性错误的地方,欢迎大家指正。

提起索引,相信有部分同学跟我的感觉一样,熟悉又陌生。熟悉是知道索引的概念,也知道它的利弊,但实际工作中并没有太多利用过索引。

我个人比较喜欢所谓的CodeFirst的编码方式,对关系数据库的操作,大部分都是通过orm或者一些自动化脚本来完成,直接去操作数据库的话,只有改造老项目,或者需要去库里查询一些数据的时候才会借助管理工具去直接操作数据库。所以,对索引的实际操作经历非常少。正巧,最近公司的一个常态化运行项目就遇到了检索的性能瓶颈,而现阶段又不能去修改业务代码或者引入一些中间件来缓解,所以想到了是不是可以从索引入手,尝试做一些性能优化。

结果,你猜怎么着,性能提升不多,也就10倍左右吧🚀

定位问题

发现性能瓶颈

首先,我发现这个问题的时候,在业务系统上做一次复杂的数据检索最多竟然需要1-2秒左右才能返回,在遇到网络不稳定的情况,返回时长还会更久,总之体验非常不好。

数据库性能查询

由于账号权限的问题,我没办法使用DTA(Database Engine Tuning Advisor)来具体的定位问题,所以使用了更直接的工具–执行计划(Excution Plan)来辅助。

具体步骤如下

  1. 首先,在本地环境下,确定此接口生成的查询语句;
  2. 将Sql直接放到数据库中执行,并开启执行计划和实时查询统计信息
  3. 获取SSMS给出的优化建议。

按此操作执行后,我们能看到当前的查询,究竟落到数据库中的链条有多长,也就是这个查询复杂度有多高,同时也能获得一个优化建议。

这里,我们先不管复杂度的问题,因为当前情况下,修改业务代码肯定是不可能的,所以目前只关注索引。

验证问题

编写测试接口

这里,我把这个常用的接口改造了一下,去掉了验证,鉴权等环节,也把一些额外的属性关掉了,确保每次请求都是直接打到服务端,且查询数据库

[AllowAnonymous]
//[ResponseCache(Duration = 10, VaryByQueryKeys = new string[] { "whereJsonStr", "adminId", "pageIndex", "pageSize", "rd" })]
public IActionResult GetApplyStatusListForTest(string whereJsonStr, int adminId, int pageIndex = 1, int pageSize = 10, int rd = 0)
{
    // 复杂查询业务
    // ... 代码省略
}

编写测试脚本

这里还是使用Grafana的K6工具进行测试。

关于K6的内容,大家可以参见其官方文档👉:https://grafana.com/docs/k6/latest/

笔者之前也写过一篇类似的博客👉:https://juejin.cn/post/7442535460361109554

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    vus: 10,
    duration: '1m',
    thresholds: {
        checks: ['rate>0.95'],  // 至少 95% 的请求必须成功
        http_req_duration: ['p(95)<500']  // 95% 的请求响应时间小于 500 毫秒
    }
}

const urls = [
    `https://localhost:5001/matchai/getApplyStatusListForTest?pageindex=${Math.floor(Math.random() * 10) + 1}&pagesize=10&whereJsonStr=...`,
    `https://localhost:5001/matchai/getApplyStatusListForTest?pageindex=${Math.floor(Math.random() * 10) + 1}&pagesize=10&whereJsonStr=...`,
    `https://localhost:5001/matchai/getApplyStatusListForTest?pageindex=${Math.floor(Math.random() * 10) + 1}&pagesize=10&whereJsonStr=...`,
    `https://localhost:5001/matchai/getApplyStatusListForTest?pageindex=${Math.floor(Math.random() * 10) + 1}&pagesize=10&whereJsonStr=...`
]
export default function () {
    const url = urls[Math.floor(Math.random() * urls.length)];
    console.log(`-------------------start----------------------`);
    console.log(`Request URL: ${url}`);
    console.log(`--------------------end----------------------`);
    let params = {
        headers: {
            'Content-Type': 'application/json'
        }
    };

    let res = http.get(url, params);
    // 解析响应体
    let response = JSON.parse(res.body);
    // 定义检查点
    let checks = {
        'status code is 200': (r) => r.status === 200,
        'API code is 1': (r) => response.code === 1
    };

    // 执行检查点
    check(res, checks);
    // 记录非成功的响应
    if (response.code !== 1) {
        console.log(`Error: code=${response.code}, msg=${response.msg}, data=${JSON.stringify(response.data)}`);
    }
    console.log(`Response Time: ${res.timings.duration}ms`);

    const sleepTime = Math.floor(Math.random() * 51) + 50; // 随机生成50到100毫秒
    sleep(sleepTime / 1000); // 将毫秒转换为秒

}

简单说明下这段测试脚本的含义

  • 导入必要模块,这里用到http,check,sleep
  • 配置压测参数,模拟10个并发,持续1分钟
  • 设定性能阈值,95%的请求必须成功,且响应时间小于500毫秒
  • 定义URL列表,随机抽取,模拟多种条件下的请求行为
  • 主函数执行,即10个uv随机发起请求,并输出观测日志
  • 定义响应检查行为,时间,并记录失败响应
  • 模拟50-100毫秒的操作间隔

控制变量

首先,在没有索引和有索引的前提下,跑一遍脚本

K6_WEB_DASHBOARD=true K6_WEB_DASHBOARD_EXPORT=html-report.html k6 run simulatescript1.js

通过上述命令,会得到一个比较完整的报表,这里我们只看Performance Overview部分,可以

  • 无索引

  • 有索引

对比可以看到,得到的测试性能提升了10倍,这期间,除了索引的建立,其余条件均一致。

由此,可以判定,根据SSMS执行计划给出的建议,建立索引之后,性能的确可以得到明显的改善。

这里的索引,就是根据SSMS执行计划给出的建议,完成的,比如

CREATE NONCLUSTERED INDEX [IX_索引名称] ON [dbo].[表名称]
(
	{待索引的字段}
)
INCLUDE({包含的字段}) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

需要说明的是,我这里真的就只关注了索引,实际上,执行计划还会给出很多检索执行过程的详细信息,大家可以导出成xml或者直接在ssms窗口查看

其他可行手段

客户端缓存

确定了增加索引,可以显著提高检索性能后,我还引入了客户端缓存,设置一个合理的过期时间,这样,再到正式站点测试后,得到的结果就是这样了👇

虽然现在的结果也并不优秀,但相比之前秒级的响应,已经能明显感觉到丝滑了许多。

索引维护

我们都知道,索引建立之后,是需要定期维护的,否则会产生过多碎片,造成性能下降。在SSMS中,可以使用Sql Agent来辅助完成。

笔者这里是建立了一个存储过程,通过系统定期调用执行。

USE [数据库]
GO
/****** Object:  StoredProcedure [dbo].[IndexMaintenanceProcedure]    Script Date: 2025-05-13 17:36:58 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[IndexMaintenanceProcedure]
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @schemaName NVARCHAR(128);
    DECLARE @tableName NVARCHAR(128);
    DECLARE @indexName NVARCHAR(128);
    DECLARE @avg_fragmentation_in_percent FLOAT;
    DECLARE @startTime DATETIME;
    DECLARE @endTime DATETIME;
    DECLARE @errorMessage NVARCHAR(MAX);
    DECLARE @actionTaken NVARCHAR(50);
    DECLARE @sql NVARCHAR(MAX);

    -- 定义游标,获取所有碎片化超过5%的索引
    DECLARE curIndexFrag CURSOR FOR
    SELECT 
        s.name AS schema_name,
        t.name AS table_name,
        i.name AS index_name,
        ips.avg_fragmentation_in_percent
    FROM 
        sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
    INNER JOIN 
        sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
    INNER JOIN 
        sys.tables t ON i.object_id = t.object_id
    INNER JOIN 
        sys.schemas s ON t.schema_id = s.schema_id
    WHERE 
        i.name IS NOT NULL -- 排除堆(heap)
        AND t.is_ms_shipped = 0 -- 排除系统表
        AND ips.avg_fragmentation_in_percent > 5; -- 设置阈值

    OPEN curIndexFrag;

    FETCH NEXT FROM curIndexFrag INTO @schemaName, @tableName, @indexName, @avg_fragmentation_in_percent;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @startTime = GETDATE();
        SET @errorMessage = NULL;
        
        BEGIN TRY
            IF @avg_fragmentation_in_percent >= 30
            BEGIN
                -- 碎片率高,重建索引
                SET @actionTaken = 'Rebuild';
                SET @sql = N'ALTER INDEX ' + QUOTENAME(@indexName) + N' ON ' 
                           + QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName) + N' REBUILD;';
            END
            ELSE IF @avg_fragmentation_in_percent >= 5 AND @avg_fragmentation_in_percent < 30
            BEGIN
                -- 中度碎片,重组索引
                SET @actionTaken = 'Reorganize';
                SET @sql = N'ALTER INDEX ' + QUOTENAME(@indexName) + N' ON ' 
                           + QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName) + N' REORGANIZE;';
            END
            
            PRINT '执行操作: ' + @sql;
            EXEC sp_executesql @sql;
        END TRY
        BEGIN CATCH
            SET @errorMessage = ERROR_MESSAGE();
            PRINT '错误发生: ' + @errorMessage;
        END CATCH
        
        SET @endTime = GETDATE();
        
        -- 记录日志
        INSERT INTO dbo.IndexMaintenanceLog (SchemaName, TableName, IndexName, Fragmentation, ActionTaken, StartTime, EndTime, ErrorMessage)
        VALUES (@schemaName, @tableName, @indexName, @avg_fragmentation_in_percent, @actionTaken, @startTime, @endTime, @errorMessage);

        FETCH NEXT FROM curIndexFrag INTO @schemaName, @tableName, @indexName, @avg_fragmentation_in_percent;
    END
	SELECT count(1) as cnt FROM dbo.IndexMaintenanceLog WHERE EndTime >= @startTime;
    CLOSE curIndexFrag;
    DEALLOCATE curIndexFrag;
END

至此,本次优化工作基本完成。

一点注意

对索引熟悉的同学,应该都知道,索引有很多类型,包括聚集索引,非聚集索引,唯一索引等等。索引的主要作用,还是提升读性能,尤其在OLAP的场景,而不是和OLTP场景。

索引的建立也不是越多越好,尤其是聚集索引,是会明显影响写入性能的。本篇提到的优化过程,建立的都是非聚集索引,对写性能的影响有限。

其他的,我就不多说了,大家一搜就能得到很多标准答案,我这里再推荐一本书《数据库系统内幕》。笔者也是在有了这次优化经历之后,发现自己在很多基础性的知识还是有欠缺,分享给大伙共勉。

最后,说起性能调优,本篇聊到的内容只是冰山一角,但我们也要注意不要陷入过度优化的陷阱,所以还是看开发人员的综合能力,开发习惯和开发经验,最终找到一条适合自己项目的系统优化之路。总之呢,系统做好了,过度调整肯定不对,完全或者几乎不调整更不对,这也是对团队能力和软件质量的考验。当然了,如果你做的都是那种小项目,就几千上万条数据量,甚至是那种单机应用,一次性应用,那确实不太需要关注这方面,把功能做好,别出低级错误就可以了。

好了,这篇就聊到这里。

*附

最后附上k6测试的完整报表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2376174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

山东大学软件学院软件工程计算机图形学复习笔记(2025)

写在前面&#xff1a; 现在是考完试的第二天&#xff0c;考试的内容还是有一部分没有复习到的…… 根据三角形的3个顶点坐标和内部某点坐标D&#xff0c;写出点D的基于面积的权重坐标Bresenham的算法描述与改进策略&#xff08;这里ppt上很不清晰&#xff09;以及直线反走样的…

【Docker】Docker Compose方式搭建分布式内存数据库(Redis)集群

文章目录 开发环境开发流程运行效果Docker Desktop桌面中的Redis结点启动图Redis结点1的打印日志情况图 配置代码命令行启动配置文件: README.md删除集群信息新建数据目录本地Redis的结点的域名,并添加到/etc/hosts文件的末尾域名映射启动集群结点创建集群关闭集群结点 redis-c…

如何在 Bash 中使用 =~ 操作符 ?

在 Bash 脚本世界中&#xff0c;有各种操作符可供我们使用&#xff0c;使我们能够操作、比较和测试数据。其中一个操作符是 ~ 操作符。这个操作符经常被忽视&#xff0c;但功能非常强大&#xff0c;它为我们提供了一种使用正则表达式匹配字符串模式的方法。 ~ 操作符语法 语法…

科学养生指南:打造健康生活

在快节奏的现代生活中&#xff0c;健康养生成为人们关注的焦点。科学养生无需复杂理论&#xff0c;掌握以下几个关键要素&#xff0c;就能为身体构筑坚实的健康防线。​ 合理饮食是健康的基础。世界卫生组织建议&#xff0c;每天应摄入至少 5 份蔬菜和水果&#xff0c;保证维生…

华为OD机试真题——单词接龙(首字母接龙)(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

计算机网络-MPLS VPN基础概念

前面几篇文章我们学习了MPLS的标签转发原理&#xff0c;有静态标签分发和LDP动态标签协议&#xff0c;可以实现LSR设备基于标签实现数据高效转发。现在开始学习MPLS在企业实际应用的场景-MPLS VPN。 一、MPLS VPN概念 MPLS&#xff08;多协议标签交换&#xff09;位于TCP/IP协…

【Linux系列】bash_profile 与 zshrc 的编辑与加载

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Spring Boot中的拦截器!

每次用户请求到达Spring Boot服务端&#xff0c;你是否需要重复写日志、权限检查或请求格式化代码&#xff1f;这些繁琐的“前置后置”工作让人头疼&#xff01;好在&#xff0c;Spring Boot拦截器如同一道智能关卡&#xff0c;统一处理请求的横切逻辑&#xff0c;让代码优雅又…

基于 Spring Boot 瑞吉外卖系统开发(十五)

基于 Spring Boot 瑞吉外卖系统开发&#xff08;十五&#xff09; 前台用户登录 在登录页面输入验证码&#xff0c;单击“登录”按钮&#xff0c;页面会携带输入的手机号和验证码向“/user/login”发起请求。 定义UserMapper接口 Mapper public interface UserMapper exte…

计算机网络笔记(二十三)——4.5IPv6

4.5.1IPv6的基本首部 IPv6 的基本首部相对于 IPv4 进行了重大简化和优化&#xff0c;固定长度为 40 字节&#xff0c;大幅提升了路由器的处理效率。以下是各字段的详细说明&#xff1a; IPv6 基本首部字段组成 字段名位数作用描述版本 (Version)4 bits固定值为 6&#xff0c…

推荐一个Winform开源的UI工具包

从零学习构建一个完整的系统 推荐一个开源、免费的适合.NET WinForms 控件的套件。 项目简介 Krypton是一套开源的.Net组件&#xff0c;用于快速构建具有丰富UI交互的WinForms应用程序。 丰富的UI控件&#xff0c;提供了48个基础控件&#xff0c;如按钮、文本框、标签、下拉…

位与运算

只有当除数是 2 的幂次方&#xff08;如 2、4、8、16...&#xff09;时&#xff0c;取模运算才可以转换为位运算。 int b 19;int a1 b % 16; // 传统取模运算int a2 b & 15; // 位运算替代取模printf("b %d\n", b);printf("b %% 8 %d\n",…

趣味编程:四叶草

概述&#xff1a;在万千三叶草中寻觅&#xff0c;只为那一抹独特的四叶草之绿&#xff0c;它象征着幸运与希望。本篇博客主要介绍四叶草的绘制。 1. 效果展示 绘制四叶草的过程是一个动态的过程&#xff0c;因此博客中所展示的为绘制完成的四叶草。 2. 源码展示 #define _CR…

城市生命线综合管控系统解决方案-守护城市生命线安全

一、政策背景 国务院办公厅《城市安全风险综合监测预警平台建设指南》‌要求&#xff1a;将燃气、供水、排水、桥梁、热力、综合管廊等纳入城市生命线监测体系&#xff0c;建立"能监测、会预警、快处置"的智慧化防控机制。住建部‌《"十四五"全国城市基础…

# 2-STM32F103-复位和时钟控制RCC

STM32-复位和时钟控制RCC 2-STM32-复位和时钟控制RCC摘要说明本文参考资料如下&#xff1a; 一、STM32最小系统回顾STM32F103C8T6核心板原理图 二、复位三、时钟3.1 时钟树3.2 STM32启动过程3.2 SystemInit()函数3.2.1 SystemInit()第1句&#xff1a;3.2.2 SystemInit()第2句&a…

多模态大语言模型arxiv论文略读(七十五)

PosterLLaVa: Constructing a Unified Multi-modal Layout Generator with LLM ➡️ 论文标题&#xff1a;PosterLLaVa: Constructing a Unified Multi-modal Layout Generator with LLM ➡️ 论文作者&#xff1a;Tao Yang, Yingmin Luo, Zhongang Qi, Yang Wu, Ying Shan, C…

使用frp实现客户端开机自启(含静默运行脚本)

本文整理了如何使用 frp 客户端并实现 Windows 系统下的开机静默自启&#xff0c;适合远程桌面、内网穿透等场景。 &#x1f4c1; 目录结构 我将 frp 客户端文件放置在以下路径&#xff1a; F:\git\frp>tree /f 卷 其它 的文件夹 PATH 列表 卷序列号为 A123-0F4E F:. │ …

list 容器常见用法及实现

文章目录 1. list 的介绍与使用1.1 list 的介绍1.2 list 的使用1.2.1 list 的构造1.2.2 list iterator 的使用1.2.3 list capacity1.2.4 list element access1.2.5 list modifiers1.2.6 迭代器失效问题 2. list 的模拟实现2.1 值得注意的点&#xff1a;2.2 std::initializer_li…

iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)

iOS视频编码详细步骤流程 1. 视频采集阶段 视频采集所使用的代码和之前的相同&#xff0c;所以不再过多进行赘述 初始化配置&#xff1a; 通过VideoCaptureConfig设置分辨率1920x1080、帧率30fps、像素格式kCVPixelFormatType_420YpCbCr8BiPlanarFullRange设置摄像头位置&am…

浅析 Golang 内存管理

文章目录 浅析 Golang 内存管理栈&#xff08;Stack&#xff09;堆&#xff08;Heap&#xff09;堆 vs. 栈内存逃逸分析内存逃逸产生的原因避免内存逃逸的手段 内存泄露常见的内存泄露场景如何避免内存泄露&#xff1f;总结 浅析 Golang 内存管理 在 Golang 当中&#xff0c;堆…