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

背景

这里的索引说的是关系数据库(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
GOALTER PROCEDURE [dbo].[IndexMaintenanceProcedure]
AS
BEGINSET 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 FORSELECT s.name AS schema_name,t.name AS table_name,i.name AS index_name,ips.avg_fragmentation_in_percentFROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ipsINNER JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_idINNER JOIN sys.tables t ON i.object_id = t.object_idINNER JOIN sys.schemas s ON t.schema_id = s.schema_idWHERE 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 = 0BEGINSET @startTime = GETDATE();SET @errorMessage = NULL;BEGIN TRYIF @avg_fragmentation_in_percent >= 30BEGIN-- 碎片率高,重建索引SET @actionTaken = 'Rebuild';SET @sql = N'ALTER INDEX ' + QUOTENAME(@indexName) + N' ON ' + QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName) + N' REBUILD;';ENDELSE IF @avg_fragmentation_in_percent >= 5 AND @avg_fragmentation_in_percent < 30BEGIN-- 中度碎片,重组索引SET @actionTaken = 'Reorganize';SET @sql = N'ALTER INDEX ' + QUOTENAME(@indexName) + N' ON ' + QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName) + N' REORGANIZE;';ENDPRINT '执行操作: ' + @sql;EXEC sp_executesql @sql;END TRYBEGIN CATCHSET @errorMessage = ERROR_MESSAGE();PRINT '错误发生: ' + @errorMessage;END CATCHSET @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;ENDSELECT count(1) as cnt FROM dbo.IndexMaintenanceLog WHERE EndTime >= @startTime;CLOSE curIndexFrag;DEALLOCATE curIndexFrag;
END

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

一点注意

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

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

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

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

好了,这篇就聊到这里。

*附

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

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

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

相关文章

【android bluetooth 框架分析 02】【Module详解 7】【VendorSpecificEventManager 模块介绍】

1. 背景 我们在 gd_shim_module 介绍章节中&#xff0c;看到 我们将 VendorSpecificEventManager 模块加入到了 modules 中。 // system/main/shim/stack.cc modules.add<hci::VendorSpecificEventManager>();在 ModuleRegistry::Start 函数中我们对 加入的所有 module…

小刚说C语言刷题—1080质因子

1.题目描述 任意输入一正整数 N &#xff0c;求出它的所有质因子。如&#xff1a;10&#xff1d;25&#xff0c;20&#xff1d;225。 输入 输入只有一行&#xff0c;包括 11个整数 n (1≤n≤32768) 输出 输出若干行&#xff0c;按从小到大的顺序给出这个数的所有质因子&am…

C语言中的宏

1.防止头文件重复包含 1.#pragma once #pragma once 是一个编译器指令&#xff0c;用于防止头文件被重复包含。它的核心作用是通过简单语法替代传统的头文件保护宏&#xff08;#ifndef/#define/#endif&#xff09;&#xff0c;提升代码简洁性和可维护性。 作用详解 防止重复…

MapReduce 模型

‌引言‌ MapReduce 是分布式计算领域的里程碑式模型&#xff0c;由 Google 在 2004 年论文中首次提出&#xff0c;旨在简化海量数据处理的复杂性。其核心思想是通过函数式编程的 ‌Map‌ &#xff08;映射&#xff09;和 ‌Reduce‌ &#xff08;归约&#xff09;阶段&#x…

Linux文件编程——标准库函数fopen、fread、fwrite等函数

1. fopen — 打开文件 函数原型&#xff1a; FILE *fopen(const char *filename, const char *mode);参数&#xff1a; filename&#xff1a;要打开的文件名&#xff0c;可以是相对路径或绝对路径。 mode&#xff1a;文件打开模式&#xff0c;表示文件的操作方式&#xff08…

从 Git 到 GitHub - 使用 Git 进行版本控制 - Git 常用命令

希望本贴能从零开始带您一起学习如何使用 Git 进行版本控制&#xff0c;并结合远程仓库 GitHub。这会是一个循序渐进的指南&#xff0c;我们开始吧&#xff01; 学习 Git 和 GitHub 的路线图&#xff1a; 理解核心概念&#xff1a;什么是版本控制&#xff1f;Git 是什么&…

2025.05.11拼多多机考真题算法岗-第四题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 04. 记忆碎片重组 问题描述 卢小姐正在开发一款名为"记忆碎片"的游戏,玩家需要分析混乱的记忆数据,推测出形成这些记忆的原始序列。游戏中,记忆数据存储在一个特殊的数…

Android Exoplayer多路不同时长音视频混合播放

在上一篇Android Exoplayer 实现多个音视频文件混合播放以及音轨切换中我们提到一个问题&#xff0c;如果视频和音频时长不一致&#xff0c;特别是想混合多个音频和多个视频时就会出问题&#xff0c;无法播放。报错如下&#xff1a; E/ExoPlayerImplInternal(11191): Playback…

Datawhale PyPOTS时间序列5月第1次笔记

课程原地址&#xff1a; https://github.com/WenjieDu/PyPOTS&#xff08;Package地址&#xff09; https://github.com/WenjieDu/BrewPOTS/tree/datawhale/202505_datawhale&#xff08;Tutorial地址&#xff09; 2.1 PyPOTS简介 PyPOTS 是一个专为处理部分观测时间序列&a…

网安学途—流量分析 attack.pcap

attack.pacp 使用Wireshark查看并分析虚拟机windows 7桌面下的attack.pcapng数据包文件&#xff0c;通过分析数据包attack.pcapng找出黑客的IP地址&#xff0c;并将黑客的IP地址作为FLAG &#xff08;形式&#xff1a;[IP地址]&#xff09;提交&#xff1a; 过滤器筛选&#x…

【大模型】DeepResearcher:通用智能体通过强化学习探索优化

DeepResearcher&#xff1a;通过强化学习在真实环境中扩展深度研究 一、引言二、技术原理&#xff08;一&#xff09;强化学习与深度研究代理&#xff08;二&#xff09;认知行为的出现&#xff08;三&#xff09;模型架构 三、实战运行方式&#xff08;一&#xff09;环境搭建…

go语言实现IP归属地查询

效果: 实现代码main.go package mainimport ("encoding/json""fmt""io/ioutil""net/http""os" )type AreaData struct {Continent string json:"continent"Country string json:"country"ZipCode …

基于STM32、HAL库的SGTL5000XNLA3R2音频接口芯片驱动程序设计

一、简介: SGTL5000XNLA3R2 是 Cirrus Logic 推出的高性能、低功耗音频编解码器,专为便携式和电池供电设备设计。它集成了立体声 ADC、DAC、麦克风前置放大器、耳机放大器和数字信号处理功能,支持 I2S/PCM 音频接口和 I2C 控制接口,非常适合与 STM32 微控制器配合使用。 二…

window 显示驱动开发-报告图形内存(一)

计算图形内存 在 VidMm 能够向客户端报告准确的帐户之前&#xff0c;它必须首先计算图形内存的总量。 VidMm 使用以下内存类型和公式来计算图形内存&#xff1a; 系统总内存 此值是操作系统可访问的系统内存总量。 BIOS 分配的内存不会出现在此数字中。 例如&#xff0c;一台…

[FA1C4] 博客链接

Blog Link 博客已经从 CSDN 转移 高情商&#xff1a;博客是给人看的 低情商&#xff1a;CSDN 已经烂了根本不能看 链接: https://fa1c4.github.io/

python通过curl访问deepseek的API调用案例

废话少说&#xff0c;开干&#xff01; API申请和充值 下面是deepeek的API网站 https://platform.deepseek.com/ 进去先注册&#xff0c;是不是手机账号密码都不重要&#xff0c;都一样&#xff0c;完事充值打米&#xff0c;主要是打米后左侧API Keys里面创建一个API Keys&am…

【计算机视觉】OpenCV项目实战:基于face_recognition库的实时人脸识别系统深度解析

基于face_recognition库的实时人脸识别系统深度解析 1. 项目概述2. 技术原理与算法设计2.1 人脸检测模块2.2 特征编码2.3 相似度计算 3. 实战部署指南3.1 环境配置3.2 数据准备3.3 实时识别流程 4. 常见问题与解决方案4.1 dlib安装失败4.2 人脸检测性能差4.3 误识别率高 5. 关键…

第6章: SEO与交互指标

第6章: SEO与交互指标 在当今的SEO环境中&#xff0c;Google越来越重视用户交互指标&#xff0c;如页面停留时长、交互性能等。本章将深入探讨如何优化网页速度和用户交互体验&#xff0c;以提升SEO效果和用户满意度。 1. Google的新时代SEO指标 随着互联网技术的发展&#xff…

Starrocks的主键表涉及到的MOR Delete+Insert更新策略

背景 写这个文章的作用主要是做一些总结和梳理&#xff0c;特别是正对大数据场景下的实时写入更新策略 COW 和 MOR 以及 DeleteInsert 的技术策略的演进&#xff0c; 这也适用于其他大数据的计算存储系统。该文章主要参考了Primary Key table. 分析总结 Starrocks 的主键表主…

C 语言_常见排序算法全解析

排序算法是计算机科学中的基础内容,本文将介绍 C 语言中几种常见的排序算法,包括实现代码、时间复杂度分析、适用场景和详细解析。 一、冒泡排序(Bubble Sort) 基本思想:重复遍历数组,比较相邻元素,将较大元素交换到右侧。 代码实现: void bubbleSort(int arr[], i…