2024年11月

.NET Conf 2024
是一个面向.NET生态系统社区的大型活动,将于2024年11月12日至14日举行。该活动将通过
YouTube
和Twitch进行现场直播,并在
dotnetconf.net
网站上提供直播流。这是一个免费的虚拟事件,旨在为初学者和学习者提供关于AI、Web开发、移动开发和游戏开发等方面的教育内容。

.NET Conf 以 .NET 团队成员和领导者的主题演讲开始,他们向您展示了 .NET 9 版本最酷的新功能。然后,您将享受一整天的现场演示,其中包括一些构建 .NET 9 的人,他们将深入探讨 .NET Aspire、AI 构建基块、C#、ASP.NET Core、Blazor、.NET MAUI 等的功能。

该活动是现场直播的,因此请务必在活动期间通过提问来与演讲者互动。我们的活动版主将寻找您的问题和评论,以便在现场活动期间分享。有关详细的议程和会议时间,请访问
https://www.dotnetconf.net/agenda
的官方时间表。

活动亮点包括:

  1. .NET 9 的正式发布
    :第一天将正式推出 .NET 9,由 .NET 团队主持的会议将深入介绍新版本的特点和功能。

  2. AI 和 .NET
    :活动还将探索 .NET 开发者如何利用 AI 库和功能构建更智能的应用程序、提高生产力,并提供更好的用户体验。

  3. 社区参与和互动
    :.NET Conf 2024 是一个充满知识、创造力和社区参与的活动,旨在提升开发体验。

  4. 内容记录和分享
    :所有内容都将被录制,并在活动结束后在 YouTube 频道上提供,以便错过直播的人可以观看。

此外,.NET Conf 2024 还设有学生区,这是一个面向初学者的虚拟活动,专家们将在活动中教授如何使用C#和.NET构建令人惊叹的项目。学生区会议将在日本时间11月19日凌晨1点和下午1点通过.NET的YouTube频道举行。

在过去的几个月里,我们的社区成员们一直忙于筹备即将于12月举行的第五届 .NET 中国峰会。我们非常高兴地宣布,活动的官网设计与开发工作正在如火如荼地进行中。在国庆节后的第一天,我们正式启动了活动的序幕,确定了会议的地点和时间,并开始了宣传工作,期待与更多的开发者共同参与这一盛会。

.NET Conf China 2024 是您探索 .NET 生态系统前沿进展的绝佳机会。随着 .NET 9 的发布,我们将展示一系列云原生改进和智能应用程序开发的新功能。这些新特性旨在帮助开发者提高生产力、简化部署流程,并加速人工智能的集成,让开发者能够更高效地构建现代化应用。无论您是经验丰富的开发者,还是刚刚开始您的 .NET 之旅的初学者,本次会议都将为您提供丰富的学习与交流机会。

大会将围绕“
.NET x AI
”这一议程展开,汇聚了众多行业专家和资深开发者,涵盖了 .NET 领域的最新技术动态。无论是核心框架的深入解析,还是跨平台应用开发的实战经验,参会者都能在这里找到丰富的内容与灵感。我们将邀请多位知名讲者分享他们在实际项目中遇到的挑战与解决方案,帮助开发者们更好地理解和应用 .NET 技术。

image

image

image

作者:来自 vivo 互联网存储团队- Wang Yuzhi

本文以一次线上故障为基础介绍了使用 glibc 进行内存管理可能碰到问题,进而对库中内存分配与释放机制进行分析,最后提供了相应问题的解决方案。

一、引言

内存对象的分配与释放一直是后端开发人员代码设计中需要考虑的问题,考虑不周极易造成内存泄漏、内存访问越界等问题。在发生内存异常后,开发人员往往花费大量时间排查用户管理层代码,而忽视了C运行时,库层和操作系统层本身的实现也可能会带来内存问题。本文先以一次线上内存事故引出问题,再逐步介绍 glibc 库的内存布局设计、内存分配、释放逻辑,最后给出相应的解决方案。

二、内存告警事件

在一次线上运维过程中发现服务出现内存告警。

【监控系统-自定义监控-告警-持续告警】

检测规则: xxx内存使用率监测:一般异常(>4096)

集群id:xxx

集群名称: xxxxxx

异常对象(当前值): xx.xx.xx.xx-xxxxxxx(11335)

开始时间: 2023-08-10 17:10:30

告警时间: 2023-08-10 18:20:32

持续时间: 1h10m2s

异常比例: 2.1918 (8/365)

异常级别: 一般

备注:-

随即查看服务相关监控,判断是业务流量激增带来的内存短时间增高,或是发生了内存泄漏。

图片

图片

通过查看 OPS 和服务自身统计的内存监控,发现在告警时间内存在业务流量突增现象,但是内存已经下降到正常值了。然而告警持续到了18:20依然没有恢复,跟监控表现不符,登录机器后发现实例的内存并没有恢复,随即怀疑用户层发生内存泄漏。

经过分析,由于内存统计代码每次调用 new、delete 之后才会对统计值进行增减,而监控中服务统计内存已经下降,说明已经正常调用 delete 进行内存释放,而操作系统层面发现内存依然居高不下,怀疑使用的c运行库 glibc 存在内存释放问题。

三、glibc 内存管理机制

3.1 glibc 简介

glibc 全称为 GUN C Library,是一个开源的标准C库,其对操作系统相关调用进行了封装,提供包括数学、字符串、文件 I/O、内存管理、多线程等方面标准函数和系统调用接口供用户使用。

3.2 内存管理布局

以 Linux 内核 v2.6.7 之后的32位模式下的虚拟内存布局方式为例:

图片

  1. Kernel Space(内核空间)— 存储内核和驱动程序的代码和数据;
  2. Stack(栈区)— 存储程序执行期间的本地变量和函数的参数,从高地址向低地址生长;

  3. Memory Mapping Segment(内存映射区)— 简称为 mmap,用来文件或其他对象映射进内存;

  4. Heap(堆区)— 动态内存分配区域,通过 malloc、new、free 和 delete 等函数管理;

  5. BSS segment(未初始化变量区)— 存储未被初始化的全局变量和静态变量;

  6. DATA segment(数据区)— 存储在源代码中有预定义值的全局变量和静态变量;

  7. TEXT segment(代码区)— 存储只读的程序执行代码,即机器指令。

其中 Heap 和 Mmap 区域是可以提供给用户程序使用的虚拟内存空间。

Heap 操作

操作系统提供了 brk() 函数,c运行时库提供了 sbrk() 函数从 Heap 中申请内存,函数声明如下:

int brk(void *addr);
void *sbrk(intptr_t increment);
  • brk() 通过设置进程堆的结束地址进行内存分配与释放,即可以一次性的分配或释放一整段连续的内存空间。比较适合于一次性分配大块内存的情况,如果设置的结束地址过大或过小会造成内存碎片或内存浪费的问题。

  • sbrk 函数通过传入的 increment 参数决定增加或减少堆空间的大小,可以动态的多次分配或释放空间达到需要多少内存就申请多少内存的效果,有效避免了内存碎片和浪费问题。

Mmap 操作

在 Linux 中提供了 mmap() 和 munmap() 函数操作虚拟内存空间,函数声明如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

其中 mmap 能够将文件或者其他对象映射进内存,munmap 能够删除特定地址区域的内存映射。

3.3 内存分配器

开源社区公开了很多现成的内存分配器,包括 dlmalloc、ptmalloc、jemalloc、tcmalloc......,由于 glibc 用的是 ptmalloc 所以本文只对该内存分配器进行介绍。

3.3.1 Arena(分配区)

堆管理结构如下所示:

struct malloc_state {
 mutex_t mutex;                 /* Serialize access. */
 int flags;                       /* Flags (formerly in max_fast). */
 #if THREAD_STATS
 /* Statistics for locking. Only used if THREAD_STATS is defined. */
 long stat_lock_direct, stat_lock_loop, stat_lock_wait;
 #endif
 mfastbinptr fastbins[NFASTBINS];    /* Fastbins */
 mchunkptr top;
 mchunkptr last_remainder;
 mchunkptr bins[NBINS * 2];
 unsigned int binmap[BINMAPSIZE];   /* Bitmap of bins */
 struct malloc_state *next;           /* Linked list */
 INTERNAL_SIZE_T system_mem;
 INTERNAL_SIZE_T max_system_mem;
 };

ptmalloc 对进程内存是通过一个个的分配区进行管理的,而分配区分为主分配区(arena)和非主分配区(narena),两者区别在于主分配区中可以使用 sbrk 和 mmap 向操作系统申请内存,而非主分配区只能通过 mmap 申请内存。

图片

对于一个进程,只有一个主分配区和若干个非主分配区,主分配区只能由第一个线程来创建持有,其和非主分配区由环形链表的形式相互连接,整个分配区中通过变量互斥锁支持多线程访问。

当一个线程调用 malloc 申请内存时,该线程先查看线程私有变量中是否已经存在一个分配区。如果存在,则对该分配区加锁,加锁成功的话就用该分配区进行内存分配;失败的话则搜索环形链表找一个未加锁的分配区。如果所有分配区都已经加锁,那么 malloc 会开辟一个新的分配区加入环形链表并加锁,用它来分配内存。释放操作同样需要获得锁才能进行。

3.3.2 chunk

ptmalloc 通过 malloc_chunk 来管理内存,定义如下:

struct malloc_chunk { 
  INTERNAL_SIZE_T      prev_size;    /* Size of previous chunk (if free).  */ 
  INTERNAL_SIZE_T      size;         /* Size in bytes, including overhead. */ 
   
  struct malloc_chunk* fd;           /* double links -- used only if free. */ 
  struct malloc_chunk* bk; 
   
  /* Only used for large blocks: pointer to next larger size.  */ 
  struct malloc_chunk* fd_nextsize;      /* double links -- used only if free. */ 
  struct malloc_chunk* bk_nextsize;
};
  • prev_size:存储前一个 chunk 的大小。如果前一个 chunk 没有被使用,则 prev_size 的值表示前一个 chunk 的大小,如果前一 chunk 已被使用,则 prev_size 的值没有意义。

  • size:表示当前 chunk 的大小,包括所请求的有效数据大小,以及堆块头部和尾部的管理信息等附加信息的大小。

  • fd 和 bk:表示 chunk 在空闲链表中的前一个和后一个堆块的指针。如果该 chunk 被占用,则这两个指针没有意义。

  • fd_nextsize 和 bk_nextsize:表示同一空闲链表上下一个堆块的指针。fd_nextsize 指向下一个比当前 chunk 大小大的第一个空闲 chunk , bk_nextszie 指向前一个比当前 chunk 大小小的第一个空闲 chunk,增加这两个字段可以加快遍历空闲 chunk ,并查找满足需要的空闲 chunk 。

使用该数据结构能够更快的在链表中查找到空闲 chunk 并分配。

3.3.3 空闲链表(bins)

在 ptmalloc 中,会将大小相似的 chunk 链接起来,叫做空闲链表(bins),总共有128个 bin 供 ptmalloc 使用。用户调用 free 函数释放内存的时候,ptmalloc 并不会立即将其归还操作系统,而是将其放入 bins 中,这样下次再调用 malloc 函数申请内存的时候,就会从 bins 中取出一块返回,这样就避免了频繁调用系统调用函数,从而降低内存分配的开销。

在 ptmalloc 中,bin主要分为以下四种:

  • fast bin

  • unsorted bin

  • small bin

  • large bin

其中根据 bin 的分类,可以分为 fast bin 和 bins,而 bins 又可以分为 unsorted bin、small bin 以及 large bin 。

图片

  • fast bin

程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而, malloc 中在分配过程中引入了 fast bins 。

fast bin 总共有10个,本质上就是10个单链表,每个 fast bin 中所包含的 chunk size 以8字节逐渐递增,即如果第一个 fast bin 中 chunk size 均为16个字节,第二个 fast bin 的 chunk size 为24字节,以此类推,最后一个 fast bin 的 chunk size 为80字节。值得注意的是 fast bin 中 chunk 释放并不会与相邻的空闲 chunk 合并,这是由于 fast bin 设计的初衷就是小内存的快速分配和释放,因此系统将属于 fast bin 的 chunk 的P(未使用标志位)总是设置为1,这样即使当 fast bin 中有某个 chunk 同一个 free chunk 相邻的时候,系统也不会进行自动合并操作。

malloc 操作

在 malloc 申请内存的时候,如果申请的内存大小范围在fast bin 以内,则先在 fast bin 中进行查找,如果 fast bin 中存在空闲 chunk 则返回。否则依次从 small bin、unsorted bin、large bin 中进行查找。

free 操作

先通过 chunksize 函数根据传入的地址指针获取该指针对应的 chunk 的大小;然后根据这个 chunk 大小获取该 chunk 所属的 fast bin,然后再将此 chunk 添加到该 fast bin 的链尾。

  • unsorted bin

是 bins 的缓冲区,顾名思义,unsorted bin 中的 chunk 无序,这种设计能够让 glibc 的 malloc 机制有第二次机会重新利用最近释放的 chunk 从而加快内存分配的时间。

与 fast bin 不同,unsorted bin 采用的是 FIFO 的方式。

malloc 操作

当需要的内存大小大于 fast bin 的最大大小,则先在 unsorted 中寻找,如果找到了合适的 chunk 则直接返回,否则继续在 small bin 和l arge bin中搜索。

free 操作

当释放的内存大小大于fast bin的最大大小,则将释放的 chunk 写入 unsorted bin。

  • small bin

大小小于512字节的 chunk 被称为 small chunk,而保存 small chunks 的 bin 被称为 small bin。62个 small bin 中,每个相邻的的 small bin 之间相差8字节,同一个 small bin 中的 chunk 拥有相同大小。

small bin 指向的是包含空闲区块的双向循环链表。内存分配和释放逻辑如下:

malloc 操作

当需要的内存不存在于 fast bin 和 unsorted bin 中,并且大小小于512字节,则在 small bin 中进行查找,如果找到了合适的 chunk 则直接返回。

free 操作

free 一个 chunk 时会检查该 chunk 相邻的 chunk 是否空闲,如果空闲则需要先合并,然后将合并的 chunk 先从所属的链表中删除然后合并成一个新的 chunk,新的 chunk 会被添加在 unsorted bin 链表的前端。

  • large bin

大小大于等于512字节的 chunk 被称为 large chunk,而保存 large chunks 的 bin 被称为 large bin。large bins 中每一个 bin 分别包含了一个给定范围内的 chunk,其中的 chunk 按大小递减排序,大小相同则按照最近使用时间排列。63 large bin 中的每一个都与 small bin 的操作方式大致相同,但不是存储固定大小的块,而是存储大小范围内的块。每个 large bin 的大小范围都设计为不与 small bin  的块大小或其他large bin 的范围重叠。

malloc 操作

首先确定用户请求的大小属于哪一个 large bin,然后判断该 large bin 中最大的 chunk 的 size 是否大于用户请求的 size。如果大于,就从尾开始遍历该 large bin,找到第一个 size 相等或接近的 chunk,分配给用户。如果该 chunk 大于用户请求的 size 的话,就将该 chunk 拆分为两个 chunk:前者返回给用户,且 size 等同于用户请求的 size;剩余的部分做为一个新的 chunk 添加到 unsorted bin 中。

free 操作

large bin 的 fee 操作与 small bin 一致,此处不再赘述。

3.3.4 特殊 chunk

  • top chunk

top chunk 是堆最上面的一段空间,它不属于任何 bin,当所有的 bin 都无法满足分配要求时,就要从这块区域里来分配,分配的空间返回给用户,剩余部分形成新的 top chunk,如果 top chunk 的空间也不满足用户的请求,就要使用 brk 或者 mmap 来向系统申请更多的堆空间(主分配区使用 brk、sbrk,非主分配区使用 mmap)。

  • mmaped chunk

当分配的内存非常大(大于分配阀值,默认128K)的时候需要被 mmap 映射,则会放到 mmaped chunk 上,释放 mmaped chunk 上的内存的时候会将内存直接交还给操作系统。(chunk 中的M标志位置1)

  • last remainder chunk

如果用户申请的 size 属于 small bin 的,但是又不能精确匹配的情况下,这时候采用最佳匹配(比如申请128字节,但是对应的bin是空,只有256字节的 bin 非空,这时候就要从256字节的 bin 上分配),这样会 split chunk 成两部分,一部分返给用户,另一部分形成 last remainder chunk,插入到 unsorted bin 中。

3.3.5 hunk 的合并与切分

  • 合并

当 chunk 释放时,如果前后两个相邻的 chunk 均空闲,则会与前后两个相邻 chunk 合并,随后将合并结果放入 unsorted bin 中。

  • 切分

当需要分配的内存小于待分配的 chunk 块,则会将待分配 chunk 块切割成两个 chunk 块,其中一个 chunk 块大小等同于用户需要分配内存的大小。需要注意的是分裂后的两个 chunk 必须均大于 chunk 的最小大小,否则不会进行拆分。

3.4 内存分配

内存分配流程可以分为三步:

第一步:根据用户请求大小转换为实际需要分配 chunk 空间的大小;

第二步:在 bins 中搜索还没有归还给操作系统的 chunk 块,具体流程如下图所示。

图片

  • 如果所需分配的 chunk 大小小于等于 max_fast (fast bins 中要求的最大 chunk 大小,默认为64B),则尝试在 fast bins 中获取 chunk,如果获取 chunk 则返回。否则进入下一步。
  • 判断所需大小是否可能处于 small bins 中,即判断 chunk_size < 512B是否成立。如果 chunk 大小处在 small bins 中则在 small bins 中搜索合适的 chunk,即找到合适的 small bin,然后从该 bin 的尾部摘取一个满足大小要求的 chunk 返回。如果 small bins 中无法找到合适的 chunk 则进入下一步。

  • 到这一步说明待分配的内存块要么是一个大的 chunk,要么只是没有在 small bin 中找到。分配器先在 fast bin 中尝试合并 chunk,并将 chunk 写入 unsorted chunk 中,此时再遍历 unsorted chunk 如果能够找到合适的 chunk 则按需将该 chunk 切分(可能不需要),将生成的 chunk 中其中一个放入 small bins 或者 large bins 中,另一个与待分配内存块相同大小的 chunk 则返回。

  • 在 large bins 中搜索合适的 chunk,如果能够找到则将该 chunk 切分成需要分配的内存大小,另一部分则继续写入 bins 中。如果无法找到合适的 chunk,则进入下一步。

  • 尝试从 top chunk 中分配一块内存给用户,剩下一部分生成新的 top chunk 。

第三步:如果 top chunk 依然无法满足分配请求,通过 sbrk 或 mmap 增加 top chunk 的大小并分配内存给用户。

3.5 内存释放

图片

  1. 判断当前 chunk 是否是 mmap 映射区域映射的内存,如果是则直接使用 munmap 释放这块内存映射(内存映射的内存能够通过标记进行识别);
  2. 判断 chunk 是否与 top chunk 相邻,如果相邻则直接与 top chunk 合并;

  3. 如果 chunk 的大小大于 max_fast(64B),则将其放入 unsorted bin,

    并检查是否有合并,如果能够合并则将 chunk 合并后根据大小加入合适的 bin 中;

  4. 如果 chunk 的大小小于

    max_fast(64B),则直接放入 fast bin 中,如果没有合并情况则 free 内存。如果在当前 chunk 相邻的 chunk 空闲,则触发合并,并将合并后的结果写入 unsorted bin 中,此时如果合并后的结果大于 max_fast(64B),则触发整个 fast bins 的合并操作,此时 fast bins 将会被遍历,将所有相邻的空闲 chunk 进行合并,然后将合并后的 chunk 写入 unsorted bin 中,fast bin 此时会变为空。如果合并后的 chunk 与 top chunk 相邻则会合并到 top chunk 中;

  5. 如果 top chunk 大小大于 mmap 收缩阈值(默认128KB),如果是,则对于主分配区则会试图归还 top chunk 中一部分给操作系统,此时 free 结束。

3.6 内存碎片

图片

按照 glibc 的内存分配策略,我们考虑下如下场景:

  1. 假设 brk 起始地址为512k

  2. malloc 40k 内存,即 chunk A,brk = 512k + 40k = 552k

  3. malloc 50k 内存,即 chunk B,brk = 552k + 50k = 602k

  4. malloc 60k 内存,即 chunk C,brk = 602k + 60k = 662k

  5. free chunk A。

此时 chunk A 为空闲块,但是如果 chunk C 和 chunk B 一直不释放无法直接通过移动brk指针来释放 chunk A 的内存,必须等待 chunk B 和 chunk C 释放才能和 top chunk 合并并将内存归还给操作系统。

四、问题分析与解决

通过前面的内存分配器运行原理能够很容易得出原因,由于程序中连续调用 free/delete 释放内存仅仅只是将内存写入内存分配器的 bins 中,并没有将其归还给操作系统,所以会出现疑似内存未回收的情况。并且如果每次 delete 的内存都不与 top chunk 相邻,会导致 chunk 块长时间留在空闲链表中无法合并到 top chunk,从而出现内存无法释放给操作系统的现象。

4.1 优化办法

  1. 通过限制服务端内存最大大小能够有效避免内存被c运行库撑的太高,导致服务器 OOM 的情况。

  2. c运行库替换成 jemalloc,jemalloc 与 glibc 的实现方式不同,能够更快将内存归还给操作系统。

4.2 效果对比测试

为了验证优化后的内存使用效果,编写测试代码,模拟线上 pipline 模式下的3000万次连续请求,对比请求过程中的内存峰值、连接断开后的内存使用状况:

  • glibc内存分配器

内存峰值

图片

连接断开后内存占用

图片

  • jemalloc内存分配器

内存峰值

图片

连接断开后内存占用

图片

根据测试结果,jemalloc 相较于 glibc 释放空闲内存速度快12%。

参考链接:

  1. https://www.gnu.org/software/libc/manual/html_node/

  2. https://github.com/hustfisher/ptmalloc/blob/master/README

  3. https://stackoverflow.com/questions/13480235/libc-memory-management

  4. https://zhuanlan.zhihu.com/p/637659294

1. 前言

本篇我们讲解
2个月搞定计算机二级C语言
——真题10

真题10-程序评分

2. 程序填空题

2.1 题目要求

真题10-程序填空

2.2 提供的代码

#include  <stdio.h>
#pragma warning (disable:4996)
double  fun(double x[], int n)
{
	int i, k = 0;
	double avg = 0.0, sum = 0.0;
	for (i = 0; i < n; i++)
		avg += x[i];
	/**********************found***********************/
	avg /= ____(1)____;
	for (i = 0; i < n; i++)
		if (x[i] > avg)
		{
			/**********************found***********************/
			____(2)____ += x[i];
			k++;
		}
	/**********************found***********************/
	return  ____(3)____;
}
main()
{
	double score[12] = { 50,60,70,80,90,100,55,65,75,85,95,99 };
	double aa;
	aa = fun(score, 12);
	printf("%f\n", aa);
}

2.3 解题思路

第(1)处填空:

在这条语句之前,程序使用
for
循环将所有学生的成绩加到了变量
avg
中,这里我们要实现的是求平均成绩,所以需要使用
avg
除学生的数量
n
,即可得到所有学生的平均成绩,并赋值给
avg

其中
avg /= n;
等价于
avg = avg / n;

avg /= n;

第(2)处填空:

经过
if (x[i] > avg)
判断,进来以后的
x[i]
都是大于平均成绩
avg
的,这里要执行的是将符合条件的
x[i]
累加至
sum
(程序已给出),并且每次使
k++
以便于后续求平均值。

sum += x[i];

第(3)处填空:

这里我们需要返回的是高于平均成绩的学生成绩的平均值,其中
sum
为高于平均成绩的学生总成绩,
k
为高于平均成绩的学生人数,用
sum / k
即可得到高于平均成绩的学生成绩的平均值。

return  (sum / k);

2.4 代码实现

填写完整的代码:

#include  <stdio.h>
#pragma warning (disable:4996)
double  fun(double x[], int n)
{
	int i, k = 0;
	double avg = 0.0, sum = 0.0;
	for (i = 0; i < n; i++)
		avg += x[i];
	/**********************found***********************/
	avg /= n;
	for (i = 0; i < n; i++)
		if (x[i] > avg)
		{
			/**********************found***********************/
			sum += x[i];
			k++;
		}
	/**********************found***********************/
	return  (sum / k);
}
main()
{
	double score[12] = { 50,60,70,80,90,100,55,65,75,85,95,99 };
	double aa;
	aa = fun(score, 12);
	printf("%f\n", aa);
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

3. 程序修改题

3.1 题目要求

真题10-程序修改

3.2 提供的代码

#include <stdio.h>
void  fun(char* s)
{
    int  i, j;
    for (i = 0, j = 0; s[i] != '\0'; i++)
        if (s[i] >= '0' && s[i] <= '9')
            /**********found**********/
            s[j] = s[i];
    /**********found**********/
    s[j] = "\0";
}
main()
{
    char  item[80];
    printf("\nEnter a string  :  "); gets(item);
    printf("\n\nThe  string  is  : \"%s\"\n", item);
    fun(item);
    printf("\n\nThe string of changing is  : \"%s\"\n", item);
    getchar();
}

3.3 解题思路

第(1)处修改:

这里需要将取出的数字字符形成新的字符串,并取代原字符串。

只使用
s[j]
是不行的,因为在程序中
j
始终没有改变值,一直为 0,导致每次的数字字符都会存储到
s[0]
的地址中,并没有形成新的字符串,所以我们要在这里让
j++
,从而改变存储的地址。

s[j++]=s[i];

第(2)处修改:

""
是用来表示字符串的,而这里的
s[j]
只能存储单个字符,所以需要使用
''
来括起来。

这里给
s[j]
赋值
'\0'
是应为字符串是以
'\0'
作为结尾的。

s[j]='\0';

3.4 代码实现

修改后的代码:

#include <stdio.h>
void  fun(char  *s)
{  int  i,j;
   for(i=0,j=0; s[i]!='\0'; i++)
        if(s[i]>='0' && s[i]<='9')
/**********found**********/
            s[j++]=s[i];
/**********found**********/
        s[j]='\0';
}
main()
{  char  item[80];
   printf("\nEnter a string  :  ");gets(item);
   printf("\n\nThe  string  is  : \"%s\"\n",item);
   fun(item);
   printf("\n\nThe string of changing is  : \"%s\"\n",item );
  getchar();
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

4. 程序设计题

4.1 题目要求

真题10-程序设计

4.2 提供的代码

#include <stdio.h>
#include <string.h>

void  fun(char* s, char  t[])
{



}

main()
{
    char   s[100], t[100]; void NONO();
    printf("\nPlease enter string S:"); scanf("%s", s);
    fun(s, t);
    printf("\nThe result is: %s\n", t);
    NONO();
    getchar();
}

void NONO()
{/* 本函数用于打开文件,输入数据,调用函数,输出数据,关闭文件。 */
    char s[100], t[100];
    FILE* rf, * wf;
    int i;

    rf = fopen("in.dat", "r");
    wf = fopen("out.dat", "w");
    for (i = 0; i < 10; i++) {
        fscanf(rf, "%s", s);
        fun(s, t);
        fprintf(wf, "%s\n", t);
    }
    fclose(rf);
    fclose(wf);
}

4.3 解题思路

这道题其实蛮容易的,无非就是遍历数组,再加个奇偶数的判断。

奇偶数判断前面也讲过,这里简单提一下:一个数除 2 取余等于 0,则为偶数,等于 1 则为奇数。

在函数开头要先将
t
所指的数组清空,然会遍历指针
s
所指的字符串,判断 ASCII 值为偶数则存到
t
中,其中
t[j++]
的作用和上一题一样。

题目要求将奇数的字符删除后,将剩余的字符放入
t
,实质就是把偶数的字符放入
t
,可以直接对偶数的字符操作,减少程序的复杂度。

4.4 代码实现

填写完整的代码:

#include <stdio.h>
#include <string.h>

void  fun(char* s, char  t[])
{
    int i = 0,j = 0;

    for (i = 0; i < (sizeof(t) / sizeof(t[0])); i++)	// 清空数组 t,防止出现垃圾值
    {
        t[i] = 0;
    }

    for (i = 0; i < strlen(s); i++)
    {
        if ((s[i] % 2) == 0)	// 等于 0 说明是 ASCII值为偶数
        {
            t[j++] = s[i];		// 保存到 t 所指的数组中
        }
    }
}

main()
{
    char   s[100], t[100]; void NONO();
    printf("\nPlease enter string S:"); scanf("%s", s);
    fun(s, t);
    printf("\nThe result is: %s\n", t);
    NONO();
    getchar();
}

void NONO()
{/* 本函数用于打开文件,输入数据,调用函数,输出数据,关闭文件。 */
    char s[100], t[100];
    FILE* rf, * wf;
    int i;

    rf = fopen("in.dat", "r");
    wf = fopen("out.dat", "w");
    for (i = 0; i < 10; i++) {
        fscanf(rf, "%s", s);
        fun(s, t);
        fprintf(wf, "%s\n", t);
    }
    fclose(rf);
    fclose(wf);
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

5. 后记

本篇博客到这就结束了,如果您有疑问或建议欢迎您在留言区留言。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Online Learning via Memory: Retrieval-Augmented Detector Adaptation

创新点


提出一种通过检索增强分类过程的创新在线学习框架
RAC
,与传统的基于离线训练/微调的方法相比,具有以下优点:

  1. 在线和持续学习能力。
  2. 最少的标注需求。
  3. 对视觉领域适应的计算无需求。

内容概述


目标检测器已经从闭集模型演变为开放世界模型,但将这些模型应用于新领域往往会导致较差的检测性能。为此,论文提出了一种新颖的方法,可以在线调整任何现成的目标检测模型,以适应新的领域,而无需重新训练检测器模型。

受到人类快速学习新主题(例如,记忆)方式的启发,论文允许检测器在测试时从记忆中查找相似的物体概念。这是通过一种检索增强分类(
RAC
)模块与一个可以灵活更新新领域知识的记忆库来实现的。

对各种现成的开放集检测器和闭集检测器进行了实验。仅使用一个小型记忆库(例如,每类
10
张图像)并且无需训练,
RAC
显著优于基线,在将检测器适应新的领域方面表现突出。

检索增强的检测器适应


在线学习框架由以下主要模块组成:

  1. 一个可在线更新的记忆库,其中包含用于提供在线适应新概念的目标领域图像
  2. 一个来自现成模型的物体(前景)提议模型,可以是开放世界检测器、在具有不同本体的相似领域数据上训练的任何检测器,或者简单的区域提议网络(
    RPN
    )。
  3. 一个上下文检索模块,用于将记忆库中的图像上下文与推理图像关联。
  4. 一个实例检索模块,用于将提议的物体实例与检索到的相似上下文中的实例关联。

对于查询图像,上下文级
RAC
首先从记忆库中选择相似的上下文图像。然后基于查询图像中的物体提议,对每个提议,实例级
RAC
在选定的相似上下文图像中执行实例匹配。最后,每个提议根据来自检索实例的投票分配一个类别。

物体(前景)提议模型

采用预训练的检测器作为物体提议网络,用于定位子任务,并专注于解决新概念分类子任务。

提议网络可以有多种形式,例如现成的开放集检测器、在不同数据集上训练的检测器(例如,具有不同本体的检测器),或者简单的区域提议网络(
RPN
),只要它能够提供有意义的前景提议。即使是没有任何语义能力的二元
RPN
网络,也可以使其具备分类能力。

记忆库

RAC
仅需最少量的数据来构建记忆库,例如每个类别
10
张图像,这些图像可在在线学习环境中由最终用户轻松标注。为了构建一个高效的记忆库,论文提出了一种无监督的图像选择方法,利用图像级特征聚类来最大化覆盖率以及最小化标注工作。

  • 无监督种子图像聚类

使用强大的图像特征提取主干(例如
CLIP
)从未标注的目标域图像中提取嵌入,这些嵌入随后根据用户标注的图像数量进行聚类(例如,使用
k-means
),形成目标数量的聚类。每个聚类中的中心图像是由用户标注的,代表了多样化和具有代表性的场景。该方法能够通过每个类别仅标注
10
张图像就实现良好的检测性能。

检索增强(
RAC
)模块

通过在记忆库中存储标注的种子对象和图像,检索增强模块可以使物体检测器通过将目标检测到的提议与种子对象匹配来获得新的语义分类能力。

物体匹配的一个重大挑战是目标域中存在外观相似的不同类别的物体。为了解决这些混淆问题,论文构建了一个多阶段的上下文匹配过程。第一阶段,上下文检索,通过过滤掉无关场景(例如,过滤掉船只的海事场景)来缩小搜索范围。第二阶段,实例检索,则是在上下文匹配的图像中进行。通过同时考虑实例外观和上下文,该方法最大限度地减少了分类混淆并提高了检索准确性。

对于检索增强模型,强大的特征提取器是必要的。然而,它并不需要在目标域上进行训练即可实现良好的语义分类准确性。因此,任何强大的预训练特征提取器,例如
DINOV2

CLIP
,都可以以无训练的方式使用,或者在提供的记忆库上进行微调以获得最佳性能。

具体来说,在第一阶段进行图像级语义匹配,使用现成的
CLIP
模型来提取图像级特征,然后计算查询图像与记忆库图像之间的相似性。在第二阶段进行实例级匹配,从图像级匹配结果中选择前 k 张图像(k=
20
,
50
,
100
),使用现成或微调的
CLIP
模型提取边界框级特征,然后计算实例之间的相似性选择的前 k 张图像。因此,最终的实例分类结果是边界框级匹配和全局上下文匹配的结合,有效地减少了外观引起的混淆。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

大家好,我是 V 哥。讲了很多数据库,有小伙伴说,SQL Server 也讲一讲啊,好吧,V 哥做个听话的门童,今天要聊一聊 SQL Server。

在 SQL Server 中,当数据量增大时,数据库的性能可能会受到影响,导致查询速度变慢、响应时间变长等问题。为了应对大量数据,以下是一些常用的优化策略和案例详解,写着写着又上1万5了,原创不易,先赞后看,养好习惯:

1. 索引优化

  • 创建索引
    :索引可以显著提高查询速度,特别是在使用
    WHERE

    JOIN

    ORDER BY
    子句时。为常用的查询字段(尤其是筛选条件字段)创建合适的索引。
  • 选择合适的索引类型
    :使用聚集索引(Clustered Index)和非聚集索引(Non-clustered Index)来优化查询性能。聚集索引适用于排序、范围查询等,而非聚集索引适用于单一列或组合列的查询。
  • 避免过多索引
    :虽然索引能提高查询性能,但过多的索引会增加更新、插入和删除操作的成本,因此要平衡索引的数量和性能。

在 SQL Server 中,索引优化是提高查询性能的重要手段。以下是一个具体的业务场景,假设我们有一个销售订单系统,订单表
Orders
需要根据不同的查询需求来进行索引优化。

业务场景

  • 查询需求1:按
    CustomerID

    OrderDate
    查询订单信息。
  • 查询需求2:按
    ProductID
    查询所有相关的订单。
  • 查询需求3:查询某一订单的详细信息(通过
    OrderID
    )。

基于这些需求,我们将为
Orders
表创建索引,并展示如何选择合适的索引类型。

1. 创建表
Orders

CREATE TABLE Orders (
    OrderID INT PRIMARY KEY,         -- 主键索引,自动创建聚集索引
    CustomerID INT,                  -- 客户ID
    OrderDate DATETIME,              -- 订单日期
    ProductID INT,                   -- 产品ID
    TotalAmount DECIMAL(18, 2),      -- 订单总金额
    Status VARCHAR(20)               -- 订单状态
);

2. 创建索引

2.1. 创建聚集索引(Clustered Index)

聚集索引通常是基于主键或唯一约束创建的。它将数据按照索引顺序存储,因此在
OrderID
上创建聚集索引能够加速按
OrderID
查找的查询。

-- OrderID 是主键,默认会创建聚集索引
-- 所以在这种情况下不需要额外创建聚集索引

2.2. 创建非聚集索引(Non-clustered Index)

对于
CustomerID

OrderDate
组合字段的查询需求,我们可以为其创建一个复合非聚集索引。这样可以加速基于
CustomerID

OrderDate
的查询。

CREATE NONCLUSTERED INDEX idx_Customer_OrderDate
ON Orders (CustomerID, OrderDate);
  • 使用场景
    :该索引有助于加速按
    CustomerID

    OrderDate
    查询的性能,特别是当订单数据量较大时。

2.3. 创建单列非聚集索引

对于查询需求2,如果我们需要按
ProductID
查找所有相关订单,我们可以为
ProductID
创建单列非聚集索引。这样可以提高查询效率。

CREATE NONCLUSTERED INDEX idx_ProductID
ON Orders (ProductID);
  • 使用场景
    :查询某个产品相关的所有订单时,通过该索引可以显著提高查询性能。

3. 删除冗余索引

如果发现某个查询经常访问多个列,而我们在这些列上创建了多个单列索引,可能会导致性能下降。比如,创建多个针对单列的非聚集索引,可能会降低插入和更新操作的效率。为了避免这种情况,可以定期检查并删除冗余的索引。

假设我们发现
ProductID

CustomerID
常常一起出现在查询条件中,我们可以考虑删除
idx_ProductID
索引,改为创建一个组合索引。

-- 删除冗余的单列索引
DROP INDEX idx_ProductID ON Orders;

4. 查询优化

现在,假设我们有以下几个查询,我们将展示如何利用创建的索引来优化查询性能。

4.1. 按
CustomerID

OrderDate
查询

-- 使用 idx_Customer_OrderDate 索引
SELECT OrderID, ProductID, TotalAmount
FROM Orders
WHERE CustomerID = 1001 AND OrderDate BETWEEN '2024-01-01' AND '2024-12-31';

4.2. 按
ProductID
查询

-- 使用 idx_ProductID 索引
SELECT OrderID, CustomerID, TotalAmount
FROM Orders
WHERE ProductID = 500;

4.3. 查询特定订单详细信息

-- 按 OrderID 查询,使用默认的聚集索引
SELECT CustomerID, ProductID, TotalAmount, Status
FROM Orders
WHERE OrderID = 123456;

5. 注意事项

  • 索引的维护成本
    :虽然索引能显著提高查询性能,但每当进行
    INSERT

    UPDATE

    DELETE
    操作时,索引也需要维护。这会增加操作的成本。因此,索引不宜过多,需要根据查询需求进行优化。
  • 索引覆盖
    :尽量创建覆盖索引,即索引包含查询所需的所有列,这样可以避免查询时回表操作,提高查询效率。

小结一下

通过为
Orders
表创建合适的索引,我们可以显著优化查询性能。在索引优化中,需要综合考虑查询需求、索引类型(聚集索引、非聚集索引)、索引的数量及其维护成本。

2. 查询优化

  • 优化 SQL 查询
    :确保 SQL 查询尽量高效。避免在查询中使用
    SELECT *
    ,而是只选择需要的列;避免重复的计算,尽量减少子查询。
  • 使用执行计划
    :利用 SQL Server Management Studio (SSMS) 的执行计划工具查看查询的执行计划,分析和优化查询中的瓶颈部分。
  • 避免复杂的嵌套查询
    :复杂的子查询可能会导致性能问题,考虑使用连接(
    JOIN
    )来代替。

查询优化是通过精心设计 SQL 查询语句和优化索引来提高查询性能的过程。根据你提供的业务场景,我们将基于一个订单系统的
Orders
表,展示几种常见的查询优化方法。

业务场景

假设我们有一个销售订单系统,
Orders
表包括以下字段:

  • OrderID
    :订单ID,主键。
  • CustomerID
    :客户ID。
  • OrderDate
    :订单日期。
  • ProductID
    :产品ID。
  • TotalAmount
    :订单总金额。
  • Status
    :订单状态(如已支付、未支付等)。

我们有以下几种查询需求:

  1. 查询某个客户在某段时间内的所有订单。
  2. 查询某个产品在所有订单中的销售情况。
  3. 查询某个订单的详细信息。
  4. 查询多个客户的订单信息。

1.
查询优化:按
CustomerID

OrderDate
查询订单

查询需求:

查询某个客户在某段时间内的所有订单。

查询语句:

SELECT OrderID, ProductID, TotalAmount, Status
FROM Orders
WHERE CustomerID = 1001
  AND OrderDate BETWEEN '2024-01-01' AND '2024-12-31';

优化建议:

  • 索引优化
    :为
    CustomerID

    OrderDate
    创建复合索引,因为这是常见的查询模式。复合索引可以加速基于这两个字段的查询。
CREATE NONCLUSTERED INDEX idx_Customer_OrderDate
ON Orders (CustomerID, OrderDate);

执行计划优化:

  • 使用
    EXPLAIN

    SET STATISTICS IO ON
    来查看执行计划,确认查询是否使用了索引。

2.
查询优化:按
ProductID
查询所有相关订单

查询需求:

查询某个产品的所有订单。

查询语句:

SELECT OrderID, CustomerID, TotalAmount, Status
FROM Orders
WHERE ProductID = 500;

优化建议:

  • 索引优化
    :为
    ProductID
    创建索引,因为这个字段经常作为查询条件。
CREATE NONCLUSTERED INDEX idx_ProductID
ON Orders (ProductID);

执行计划优化:

  • 确保查询能够利用
    idx_ProductID
    索引,避免全表扫描。

3.
查询优化:查询某个订单的详细信息

查询需求:

查询某个订单的详细信息。

查询语句:

SELECT CustomerID, ProductID, TotalAmount, Status
FROM Orders
WHERE OrderID = 123456;

优化建议:

  • 索引优化
    :因为
    OrderID
    是主键字段,SQL Server 会自动创建聚集索引。查询
    OrderID
    字段时,查询会直接利用聚集索引。
-- 聚集索引已自动创建,无需额外创建

执行计划优化:

  • 确保查询只扫描一行数据,利用
    OrderID
    主键索引。

4.
查询优化:查询多个客户的订单信息

查询需求:

查询多个客户的订单信息。

查询语句:

SELECT OrderID, CustomerID, ProductID, TotalAmount, Status
FROM Orders
WHERE CustomerID IN (1001, 1002, 1003);

优化建议:

  • 索引优化
    :为
    CustomerID
    创建索引,以便快速过滤出目标客户的订单。
CREATE NONCLUSTERED INDEX idx_CustomerID
ON Orders (CustomerID);

执行计划优化:

  • 确保
    IN
    子句使用了
    idx_CustomerID
    索引来优化查询。

5.
查询优化:避免使用
SELECT *

查询需求:

查询所有字段(不推荐,通常用来调试或检查表结构)。

查询语句:

SELECT * FROM Orders;

优化建议:

  • 明确选择需要的列
    :避免使用
    SELECT *
    ,明确列出查询需要的字段,避免读取不必要的列。
SELECT OrderID, CustomerID, TotalAmount FROM Orders;

6.
查询优化:使用
JOIN
进行多表查询

查询需求:

查询某个客户的订单信息以及相关的产品信息。假设有一个
Products
表,包含
ProductID

ProductName

查询语句:

SELECT o.OrderID, o.TotalAmount, p.ProductName
FROM Orders o
JOIN Products p ON o.ProductID = p.ProductID
WHERE o.CustomerID = 1001
  AND o.OrderDate BETWEEN '2024-01-01' AND '2024-12-31';

优化建议:

  • 索引优化
    :为
    Orders
    表的
    CustomerID

    OrderDate

    ProductID
    创建复合索引,为
    Products
    表的
    ProductID
    创建索引,以加速
    JOIN
    查询。
CREATE NONCLUSTERED INDEX idx_Orders_Customer_OrderDate_Product
ON Orders (CustomerID, OrderDate, ProductID);

CREATE NONCLUSTERED INDEX idx_Products_ProductID
ON Products (ProductID);

执行计划优化:

  • 确保执行计划中使用了
    JOIN
    的相关索引,避免全表扫描。

7.
查询优化:分页查询

查询需求:

查询某个时间段内的客户订单,并实现分页功能。

查询语句:

SELECT OrderID, CustomerID, TotalAmount, Status
FROM Orders
WHERE OrderDate BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY OrderDate
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

优化建议:

  • 索引优化
    :确保在
    OrderDate
    上有合适的索引,能够加速排序操作。
  • 使用
    OFFSET

    FETCH
    语句实现分页查询,避免一次性加载大量数据。
CREATE NONCLUSTERED INDEX idx_OrderDate
ON Orders (OrderDate);

8.
避免过多的子查询

查询需求:

查询某个客户在某段时间内的订单总金额。

查询语句:

SELECT CustomerID, 
       (SELECT SUM(TotalAmount) FROM Orders WHERE CustomerID = 1001 AND OrderDate BETWEEN '2024-01-01' AND '2024-12-31') AS TotalSpent
FROM Customers
WHERE CustomerID = 1001;

优化建议:

  • 避免使用子查询
    :尽量避免在
    SELECT
    语句中使用子查询,可以改为
    JOIN

    GROUP BY
    来提高效率。
SELECT o.CustomerID, SUM(o.TotalAmount) AS TotalSpent
FROM Orders o
WHERE o.CustomerID = 1001
  AND o.OrderDate BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY o.CustomerID;

小结一下

通过优化 SQL 查询语句、合理使用索引以及减少不必要的操作,我们能够显著提高查询性能。具体做法包括:

  • 创建合适的索引(单列索引和复合索引)。
  • 优化查询语句,避免使用
    SELECT *
    和过多的子查询。
  • 使用合适的分页技术和
    JOIN
    优化多表查询。
  • 分析查询执行计划,确保查询高效执行。

这些优化措施可以帮助 SQL Server 在面对大量数据时保持高效的查询性能。

3. 数据分区和分表

  • 表分区
    :对于非常大的表,可以考虑使用表分区。表分区可以根据某些条件(例如时间、ID 范围等)将数据分割到多个物理文件中,这样查询时只访问相关的分区,减少了全表扫描的开销。
  • 水平拆分(Sharding)
    :将数据分散到多个独立的表或数据库中,通常基于某种规则(如区域、日期等)。每个表包含数据的一个子集,可以提高查询效率。

数据分区(Partitioning)和分表(Sharding)是优化数据库性能的关键手段,尤其在处理大数据量时。通过数据分区或分表,可以有效地减少查询和写入的压力,提高数据访问效率。以下是基于业务场景的具体代码案例,展示如何使用数据分区和分表来优化 SQL Server 的性能。

业务场景

假设我们有一个订单系统,
Orders
表记录了所有订单信息。随着订单量的增加,单表的查询和维护变得越来越困难。因此,我们需要使用分区和分表技术来优化数据库的性能。

1.
数据分区(Partitioning)

数据分区是在单一表上进行逻辑分区,它允许将一个大的表按某个规则(如时间范围、数值区间等)分成多个物理段(分区)。每个分区可以独立管理,查询可以在特定的分区内进行,从而提高查询性能。

业务需求

  • 按照订单日期(
    OrderDate
    )将
    Orders
    表分区,以便在查询时快速定位到特定时间段内的订单。

步骤:

  1. 创建分区函数(Partition Function)和分区方案(Partition Scheme)。

  2. Orders
    表上应用分区。

创建分区函数(Partition Function)

-- 创建分区函数:按年度分区
CREATE PARTITION FUNCTION OrderDatePartitionFunc (DATE)
AS RANGE RIGHT FOR VALUES ('2023-01-01', '2024-01-01', '2025-01-01');

该分区函数将根据订单日期(
OrderDate
)把数据分为多个区间,每个区间的范围是按年划分的。

创建分区方案(Partition Scheme)

-- 创建分区方案:将分区函数应用到物理文件组
CREATE PARTITION SCHEME OrderDatePartitionScheme
AS PARTITION OrderDatePartitionFunc
TO ([PRIMARY], [FG_2023], [FG_2024], [FG_2025]);

此方案为每个分区指定一个物理文件组(如
PRIMARY

FG_2023
等)。

创建分区表

-- 创建分区表:应用分区方案
CREATE TABLE Orders
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
)
ON OrderDatePartitionScheme (OrderDate);

Orders
表按
OrderDate
字段进行分区,数据会根据日期分布到不同的物理文件组中。

查询优化

-- 查询 2024 年的订单,查询仅会访问相应的分区,提高查询效率
SELECT OrderID, CustomerID, ProductID, TotalAmount
FROM Orders
WHERE OrderDate BETWEEN '2024-01-01' AND '2024-12-31';

通过分区,查询只会扫描相关分区的数据,从而提高查询速度。

2.
数据分表(Sharding)

分表是将数据水平拆分到多个物理表中,每个表存储一部分数据。常见的分表策略包括按范围分表、按哈希值分表等。分表可以显著提升查询性能,但需要管理多个表及其关系。

业务需求


  • CustomerID

    Orders
    表进行分表,客户ID为基础将数据分配到不同的表中。
  • 客户ID的范围是均匀的,因此我们可以使用哈希分表策略。

步骤:

  1. 创建多个分表。
  2. 在应用层处理分表逻辑。

创建分表

假设我们决定将
Orders
表按
CustomerID
的哈希值分成 4 个表。可以通过以下方式创建 4 个分表:

-- 创建 Orders_1 分表
CREATE TABLE Orders_1
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

-- 创建 Orders_2 分表
CREATE TABLE Orders_2
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

-- 创建 Orders_3 分表
CREATE TABLE Orders_3
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

-- 创建 Orders_4 分表
CREATE TABLE Orders_4
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

分表逻辑

在应用层,我们需要实现一个分表路由逻辑,通过哈希值来确定应该向哪个表插入数据或查询数据。

-- 示例:根据 CustomerID 哈希值选择分表
DECLARE @CustomerID INT = 1001;
DECLARE @TableSuffix INT;

-- 使用哈希算法来决定表
SET @TableSuffix = @CustomerID % 4;

-- 插入数据
IF @TableSuffix = 0
BEGIN
    INSERT INTO Orders_1 (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
    VALUES (123456, 1001, '2024-01-01', 101, 150.00, 'Paid');
END
ELSE IF @TableSuffix = 1
BEGIN
    INSERT INTO Orders_2 (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
    VALUES (123457, 1002, '2024-01-02', 102, 250.00, 'Pending');
END
ELSE IF @TableSuffix = 2
BEGIN
    INSERT INTO Orders_3 (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
    VALUES (123458, 1003, '2024-01-03', 103, 350.00, 'Shipped');
END
ELSE
BEGIN
    INSERT INTO Orders_4 (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
    VALUES (123459, 1004, '2024-01-04', 104, 450.00, 'Delivered');
END

查询逻辑

为了查询某个客户的订单,我们也需要在应用层决定查询哪个分表:

-- 查询某个客户的订单
DECLARE @CustomerID INT = 1001;
DECLARE @TableSuffix INT;
SET @TableSuffix = @CustomerID % 4;

-- 查询数据
IF @TableSuffix = 0
BEGIN
    SELECT * FROM Orders_1 WHERE CustomerID = @CustomerID;
END
ELSE IF @TableSuffix = 1
BEGIN
    SELECT * FROM Orders_2 WHERE CustomerID = @CustomerID;
END
ELSE IF @TableSuffix = 2
BEGIN
    SELECT * FROM Orders_3 WHERE CustomerID = @CustomerID;
END
ELSE
BEGIN
    SELECT * FROM Orders_4 WHERE CustomerID = @CustomerID;
END

3.
分区和分表的选择

  • 分区
    :适用于对一个表进行物理划分,但仍然保持数据的逻辑统一性。例如,按时间(如订单日期)分区可以有效提高时间范围查询的性能。
  • 分表
    :适用于数据量特别大的情况,将数据拆分到多个表中,以减少单个表的查询压力。通常采用哈希分表或者范围分表。

小结一下

  • 分区
    可以让你在一个大的表上进行逻辑划分,在查询时只访问相关的分区,提高性能。
  • 分表
    则是将数据水平拆分到多个物理表,通常用于处理极大数据量的场景。
  • 在 SQL Server 中实现分区和分表需要对表的设计、索引设计和查询策略进行综合考虑,以确保数据访问效率和维护的便利性。

4. 数据归档

  • 归档旧数据
    :对于已经不常查询的数据,可以将其归档到独立的历史表或数据库中,从而减轻主数据库的负担。只保留近期数据在主表中,优化查询性能。
  • 压缩旧数据
    :可以通过压缩技术来存储归档数据,节省存储空间。

数据归档是指将不再频繁访问的历史数据从主数据库中移除,并将其存储在归档系统或表中,从而提高主数据库的性能。数据归档通常用于老旧数据、历史记录等不再活跃但需要保留的数据。

业务场景

假设我们有一个订单系统,
Orders
表记录了所有订单信息。随着时间的推移,订单数据量急剧增加,但在实际业务中,超过一定时间的订单数据查询频率下降。为了提高数据库性能,我们决定将超过 1 年的订单数据从主表中移除并存档到归档表中。

步骤:

  1. 创建主表(
    Orders
    )和归档表(
    ArchivedOrders
    )。
  2. 定期将超过 1 年的订单数据从
    Orders
    表移到
    ArchivedOrders
    表。
  3. 确保归档数据的查询不会影响到主表的性能。

1. 创建主表和归档表

-- 创建主订单表 Orders
CREATE TABLE Orders
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

-- 创建归档表 ArchivedOrders
CREATE TABLE ArchivedOrders
(
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
);

2. 归档操作(将超过 1 年的订单移至归档表)

为了定期将过期的订单移至归档表,可以使用定时任务(如 SQL Server Agent 作业)来执行这个操作。

-- 将超过 1 年的订单数据从 Orders 表移到 ArchivedOrders 表
INSERT INTO ArchivedOrders (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
SELECT OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status
FROM Orders
WHERE OrderDate < DATEADD(YEAR, -1, GETDATE());

-- 删除 Orders 表中超过 1 年的订单数据
DELETE FROM Orders
WHERE OrderDate < DATEADD(YEAR, -1, GETDATE());

这段代码会将
Orders
表中
OrderDate
小于当前日期 1 年的订单数据插入到
ArchivedOrders
表,并将这些数据从
Orders
表中删除。

3. 定时归档任务(使用 SQL Server Agent)

我们可以使用 SQL Server Agent 来创建一个定时任务,定期执行数据归档操作。例如,每天运行一次,将 1 年前的订单数据归档:

-- 在 SQL Server Agent 中创建作业来执行归档操作
USE msdb;
GO

EXEC sp_add_job
    @job_name = N'ArchiveOldOrders';
GO

EXEC sp_add_jobstep
    @job_name = N'ArchiveOldOrders',
    @step_name = N'ArchiveOrdersStep',
    @subsystem = N'TSQL',
    @command = N'
        INSERT INTO ArchivedOrders (OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status)
        SELECT OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status
        FROM Orders
        WHERE OrderDate < DATEADD(YEAR, -1, GETDATE());

        DELETE FROM Orders
        WHERE OrderDate < DATEADD(YEAR, -1, GETDATE());
    ',
    @database_name = N'VGDB';
GO

-- 设置作业的调度,例如每天运行一次
EXEC sp_add_schedule
    @schedule_name = N'ArchiveOrdersDaily',
    @enabled = 1,
    @freq_type = 4, -- 每天
    @freq_interval = 1, -- 每天执行一次
    @active_start_time = 0;
GO

EXEC sp_attach_schedule
    @job_name = N'ArchiveOldOrders',
    @schedule_name = N'ArchiveOrdersDaily';
GO

-- 启动作业
EXEC sp_start_job @job_name = N'ArchiveOldOrders';
GO

4. 查询归档数据

归档后的数据依然可以查询,但不会影响主表的查询性能。为了查找某个客户的历史订单,可以查询归档表:

-- 查询某个客户的历史订单
SELECT OrderID, CustomerID, OrderDate, ProductID, TotalAmount, Status
FROM ArchivedOrders
WHERE CustomerID = 1001
ORDER BY OrderDate DESC;

5. 优化与注意事项

  • 归档策略
    :可以根据实际业务需求选择合适的时间范围(例如,3 个月、6 个月或 1 年)。可以通过调整
    WHERE
    条件来修改归档规则。
  • 性能优化
    :定期归档操作可以减轻主表的负担,提高查询性能。定期删除旧数据也能减少主表的存储空间。
  • 归档数据的备份和恢复
    :归档数据同样需要定期备份,并能够在需要时恢复。确保归档表也包括足够的备份策略。

6. 归档与清理数据的另一个选项:软删除

在某些情况下,数据归档后并没有从数据库中完全删除,而是标记为“已归档”或“已删除”。这种方法的优点是可以随时恢复数据,而不会丢失。

-- 在 Orders 表中添加 Archived 标志
ALTER TABLE Orders
ADD Archived BIT DEFAULT 0;

-- 将数据标记为已归档
UPDATE Orders
SET Archived = 1
WHERE OrderDate < DATEADD(YEAR, -1, GETDATE());

-- 查询未归档的数据
SELECT * FROM Orders WHERE Archived = 0;

-- 查询归档数据
SELECT * FROM Orders WHERE Archived = 1;

通过这种方法,归档的订单仍然保留在主表中,但通过
Archived
字段可以区分已归档和未归档的订单。

小结一下

数据归档操作是管理大数据量数据库的一种有效策略。通过定期将历史数据从主数据库表中迁移到归档表,可以显著提高数据库的查询性能,同时确保历史数据得以保留,便于以后查询和审计。

5. 存储和硬件优化

  • 磁盘 I/O 优化
    :数据库的性能受到磁盘 I/O 的限制,尤其是在处理大量数据时。使用 SSD 存储比传统的硬盘(HDD)提供更快的 I/O 性能。
  • 增加内存
    :增加 SQL Server 的内存,可以使数据库缓冲池更大,从而减少磁盘 I/O,提升查询性能。
  • 使用 RAID 配置
    :使用 RAID 10 或其他 RAID 配置,确保数据读写的高效性和可靠性。

存储和硬件优化是提升数据库性能的关键部分,尤其是在大规模数据处理的环境中。通过合理的硬件资源分配、存储结构优化以及数据库配置,可以显著提高性能。下面我们将针对一个电商平台的订单系统来讲解如何在存储和硬件层面优化 SQL Server。

业务场景:

假设你有一个电商平台,订单数据存储在 SQL Server 中,订单数量日益增加,导致查询性能下降。在此场景中,我们可以通过以下方法进行存储和硬件优化。

优化策略:

  1. 磁盘 I/O 优化


    • 使用 SSD 替代传统硬盘(HDD)以提高读写速度。
    • 将数据文件、日志文件和临时文件存储在不同的物理磁盘上。
  2. 表和索引存储


    • 使用适当的存储格式和文件组织方式,如分区表和表压缩。
    • 将频繁访问的表和索引放置在高性能的磁盘上。
  3. 硬件资源配置


    • 增加内存以支持更多的数据缓存,减少磁盘访问。
    • 使用多核 CPU 以提高并发查询的处理能力。
  4. 数据压缩


    • 在 SQL Server 中启用数据压缩,以减少磁盘空间的使用并提高 I/O 性能。

1. 创建表并优化存储

首先,我们创建订单表,并为订单表的
OrderID
列创建聚集索引。

-- 创建 Orders 表并优化存储
CREATE TABLE Orders
(
    OrderID INT PRIMARY KEY CLUSTERED,  -- 聚集索引
    CustomerID INT,
    OrderDate DATETIME,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
) 
ON [PRIMARY]
WITH (DATA_COMPRESSION = PAGE);  -- 启用数据页压缩以节省空间

-- 启用非聚集索引,用于优化查询
CREATE NONCLUSTERED INDEX idx_OrderDate
ON Orders(OrderDate)
WITH (DATA_COMPRESSION = PAGE);  -- 同样启用数据压缩

通过使用
DATA_COMPRESSION = PAGE
,我们启用了 SQL Server 的数据压缩功能,以节省存储空间并提高磁盘 I/O 性能。
PAGE
压缩比
ROW
压缩更高效,适合大型数据表。

2. 分区表优化

在订单数据量不断增加的情况下,我们可以将订单表进行分区。根据
OrderDate
列将数据划分为不同的分区,以减少查询时的扫描范围,提高查询效率。

-- 创建分区函数
CREATE PARTITION FUNCTION pf_OrderDate (DATETIME)
AS RANGE RIGHT FOR VALUES ('2022-01-01', '2023-01-01', '2024-01-01');

-- 创建分区方案
CREATE PARTITION SCHEME ps_OrderDate
AS PARTITION pf_OrderDate
TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]);

-- 创建分区表
CREATE TABLE Orders
(
    OrderID INT PRIMARY KEY CLUSTERED, 
    CustomerID INT,
    OrderDate DATETIME,
    ProductID INT,
    TotalAmount DECIMAL(10, 2),
    Status VARCHAR(20)
) 
ON ps_OrderDate(OrderDate);  -- 按 OrderDate 列进行分区

在此代码中,我们根据
OrderDate
列的年份划分了不同的分区(如 2022 年、2023 年和 2024 年的订单数据)。这样可以使查询在某一特定时间范围内的性能更高,因为 SQL Server 只需要扫描相关分区的数据,而不是整个表。

3. 硬件优化配置

3.1. 确保使用 SSD 磁盘

SSD 磁盘比传统硬盘的读写速度快,因此将数据库的主要数据文件、日志文件和临时文件分别存储在不同的磁盘上(最好是 SSD)可以提高性能。

-- 将 SQL Server 数据文件 (.mdf) 存储在 SSD 磁盘
-- 将日志文件 (.ldf) 存储在 SSD 磁盘
-- 将临时数据库文件 (.ndf) 存储在 SSD 磁盘

3.2. 配置 SQL Server 内存

将 SQL Server 的内存设置为最大化,以便更多数据可以缓存在内存中,从而减少磁盘 I/O。以下为如何设置 SQL Server 的最大内存配置:

-- 查看当前内存设置
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'max server memory (MB)';

-- 设置最大内存为 16 GB
EXEC sp_configure 'max server memory (MB)', 16384;
RECONFIGURE;

通过适当的内存配置,SQL Server 可以将更多数据缓存在内存中,从而减少对磁盘的访问,提高查询响应速度。

3.3. 配置 SQL Server 并行处理

如果服务器具有多核 CPU,可以通过设置 SQL Server 允许更多的并行查询操作,从而提高多线程查询的处理能力。

-- 查看当前并行度配置
EXEC sp_configure 'max degree of parallelism';

-- 设置为 4,允许最多 4 个 CPU 并行处理查询
EXEC sp_configure 'max degree of parallelism', 4;
RECONFIGURE;

4. 磁盘 I/O 优化:分开存储数据文件、日志文件和临时文件

磁盘 I/O 是数据库性能的瓶颈之一。为了提高数据库的性能,最好将数据文件、日志文件和临时文件存储在不同的物理磁盘上。

-- 数据文件 (.mdf) 存储在磁盘 A
-- 日志文件 (.ldf) 存储在磁盘 B
-- 临时数据库文件 (.ndf) 存储在磁盘 C

5. 数据备份和恢复优化

确保定期备份数据,并使用增量备份、差异备份等方式以减少备份时的磁盘负担。

-- 进行完整备份
BACKUP DATABASE VGDB TO DISK = 'D:\Backups\VGDB_full.bak';

-- 进行差异备份
BACKUP DATABASE WGDB TO DISK = 'D:\Backups\VGDB_diff.bak' WITH DIFFERENTIAL;

-- 进行事务日志备份
BACKUP LOG VGDB TO DISK = 'D:\Backups\VGDB_log.trn';

通过这种方法,可以在系统崩溃时快速恢复数据,同时减少备份过程中对硬盘 I/O 性能的影响。

6. 监控和维护

定期监控 SQL Server 的性能,并根据硬件和存储需求做出相应的调整。通过 SQL Server 的动态管理视图(DMV)来监控 I/O 性能、查询执行计划、索引使用情况等。

-- 查看磁盘 I/O 状况
SELECT * FROM sys.dm_io_virtual_file_stats(NULL, NULL);

-- 查看查询执行计划的缓存
SELECT * FROM sys.dm_exec_query_stats;

-- 查看当前的索引使用情况
SELECT * FROM sys.dm_db_index_usage_stats;

小结一下

通过存储和硬件优化,可以显著提升 SQL Server 数据库的性能。关键的优化措施包括使用 SSD 磁盘、将数据文件、日志文件和临时文件分开存储、启用数据压缩、使用分区表来提高查询效率以及调整内存和并行处理配置等。定期的维护和监控也能帮助你发现性能瓶颈并作出相应调整。

6. 数据库参数和配置优化

  • 调整最大并发连接数
    :确保 SQL Server 配置了足够的最大并发连接数,避免过多连接时导致性能下降。
  • 设置合适的内存限制
    :为 SQL Server 配置足够的内存(
    max server memory
    ),避免内存溢出或过度使用磁盘交换。
  • 自动更新统计信息
    :确保 SQL Server 自动更新查询的统计信息(
    AUTO_UPDATE_STATISTICS
    ),以便查询优化器选择最优执行计划。

数据库参数和配置优化是确保数据库系统性能达到最佳状态的重要步骤。在高并发、高负载的场景下,合理的配置可以显著提高数据库性能,减少响应时间和延迟。以下是基于一个电商平台订单系统的业务场景,如何通过优化数据库的参数和配置来提升性能的完整代码案例。

业务场景:

假设电商平台的订单量非常大,系统每天处理数百万个订单,数据库的性能和响应速度是系统正常运行的关键。为确保数据库性能,在 SQL Server 中进行参数和配置优化至关重要。

优化策略:

  1. 调整内存配置
    :通过配置 SQL Server 使用更多的内存来缓存数据,减少磁盘 I/O。
  2. 设置最大并行度
    :根据 CPU 核心数,调整 SQL Server 的并行查询处理能力。
  3. 优化磁盘和存储配置
    :确保日志文件、数据文件和临时文件分开存储。
  4. 启用自动数据库优化
    :确保数据库能够自动进行碎片整理、更新统计信息等任务。
  5. 调整事务日志和恢复模式
    :确保数据库在发生故障时能够快速恢复。

1. 调整内存配置

内存配置优化是提高 SQL Server 性能的关键部分。通过增加 SQL Server 的最大内存,可以保证查询操作不会因为磁盘 I/O 的瓶颈而导致性能问题。

-- 查看当前最大内存配置
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'max server memory (MB)';

-- 设置最大内存为 16 GB
EXEC sp_configure 'max server memory (MB)', 16384;  -- 16 GB
RECONFIGURE;

在上述代码中,我们将 SQL Server 的最大内存设置为 16 GB。适当配置内存可以提高查询性能,减少磁盘的访问。

2. 设置最大并行度

SQL Server 可以利用多个 CPU 核心进行并行查询处理。通过合理设置并行度,可以提高大查询的处理能力。

-- 查看当前的最大并行度设置
EXEC sp_configure 'max degree of parallelism';

-- 设置最大并行度为 4(适用于 4 核 CPU 的机器)
EXEC sp_configure 'max degree of parallelism', 4;
RECONFIGURE;

通过此设置,SQL Server 可以在查询时利用最多 4 个 CPU 核心进行并行处理。如果你的服务器有更多核心,可以根据实际情况调整这个参数。

3. 调整事务日志和恢复模式

对于电商平台而言,事务日志的优化至关重要。确保在进行大规模事务操作时,日志文件能够高效地处理,并且确保恢复模式符合业务需求。

-- 查看数据库的恢复模式
SELECT name, recovery_model_desc
FROM sys.databases
WHERE name = 'VGDB';

-- 设置恢复模式为简单恢复模式
ALTER DATABASE VGDB
SET RECOVERY SIMPLE;

对于不需要完整备份的数据库,使用简单恢复模式可以减少日志文件的增长,减轻磁盘 I/O 压力。

4. 配置自动数据库优化

确保数据库能够定期执行自动优化任务,如重建索引、更新统计信息等。定期优化可以提高数据库的查询性能,避免碎片化问题。

-- 启用自动更新统计信息
EXEC sp_configure 'auto update statistics', 1;
RECONFIGURE;

-- 启用自动创建统计信息
EXEC sp_configure 'auto create statistics', 1;
RECONFIGURE;

通过启用自动更新统计信息和自动创建统计信息,可以确保 SQL Server 在执行查询时能够使用最新的执行计划,减少查询优化器的负担。

5. 配置磁盘和存储

确保 SQL Server 的数据文件、日志文件和临时文件存储在不同的磁盘上,特别是将日志文件和数据文件存储在高速磁盘(如 SSD)上。

-- 将数据文件 (.mdf) 存储在磁盘 A(SSD)
-- 将日志文件 (.ldf) 存储在磁盘 B(SSD)
-- 将临时数据库文件 (.ndf) 存储在磁盘 C(SSD)

通过将数据文件、日志文件和临时文件分别存储在不同的磁盘上,可以避免磁盘 I/O 争用,提升数据库的整体性能。

6. 启用数据库压缩

对于需要存储大量数据的电商平台,启用数据压缩可以减少存储空间并提高查询性能,尤其是在磁盘 I/O 上。

-- 启用表压缩
ALTER TABLE Orders REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = PAGE);

-- 启用索引压缩
ALTER INDEX ALL ON Orders REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = PAGE);

通过启用数据压缩,我们可以有效节省存储空间,减少磁盘 I/O 操作,并提高查询速度。

7. 配置自动维护任务

SQL Server 提供了自动维护任务,如索引重建、数据库碎片整理等,可以通过 SQL Server Agent 定时任务来自动执行这些任务,保持数据库的高效运行。

-- 创建一个定期执行的作业,执行索引重建任务
EXEC sp_add_job @job_name = 'RebuildIndexes', @enabled = 1;
EXEC sp_add_jobstep @job_name = 'RebuildIndexes', 
    @step_name = 'RebuildIndexStep', 
    @subsystem = 'TSQL', 
    @command = 'ALTER INDEX ALL ON Orders REBUILD',
    @retry_attempts = 3, 
    @retry_interval = 5;

-- 设置作业运行频率:每天凌晨 2 点执行
EXEC sp_add_schedule @schedule_name = 'RebuildIndexSchedule',
    @enabled = 1,
    @freq_type = 4, 
    @freq_interval = 1, 
    @active_start_time = 20000;

EXEC sp_attach_schedule @job_name = 'RebuildIndexes', @schedule_name = 'RebuildIndexSchedule';

这个作业将在每天凌晨 2 点执行,重建
Orders
表上的所有索引,从而避免因索引碎片而降低查询性能。

8. 启用即时日志备份

对于生产环境,尤其是电商平台,确保日志备份及时执行至关重要。启用日志备份可以保证在数据库发生故障时进行快速恢复。

-- 设置事务日志备份
BACKUP LOG VGDB TO DISK = 'D:\Backups\YourDatabase_log.trn';

通过定期执行事务日志备份,可以确保在发生故障时,数据库能够恢复到最新的状态。

9. 启用数据库缓存

SQL Server 会缓存查询结果和数据页,通过调整缓存策略来优化性能。

-- 查看缓存的页面数量
DBCC SHOW_STATISTICS('Orders');

-- 强制清除缓存(有时可以用于测试)
DBCC FREEPROCCACHE;
DBCC DROPCLEANBUFFERS;

在日常操作中,我们不建议经常清除缓存,但可以在需要时清除缓存来测试性能优化效果。

小结一下

通过优化 SQL Server 的配置和参数,可以显著提升电商平台的数据库性能。关键的优化措施包括调整内存和并行度、优化磁盘存储和日志配置、启用数据压缩、定期执行自动数据库优化任务、配置数据库压缩和定期备份等。根据业务需求和硬件资源进行合理配置,以确保数据库在高并发、高负载的环境中能够稳定高效地运行。

7. 批量数据处理

  • 批量插入/更新操作
    :在处理大量数据时,可以使用批量插入或更新操作,而不是一行一行地进行。这能显著提高数据的加载速度。
  • 避免大事务
    :对于大量的数据修改,避免使用大事务,因为大事务可能会导致锁竞争、日志文件过大等问题。使用小批次事务进行操作。

批量数据处理在大规模应用中是不可避免的,尤其是像电商平台、金融系统等业务场景,通常需要进行大批量的订单、用户信息处理等。批量操作能够显著提高数据处理效率,但也需要谨慎设计,以确保性能和稳定性。

业务场景:

假设在电商平台中,订单信息需要进行批量处理,比如批量更新订单状态、批量删除失效订单、批量插入订单数据等。通过设计合适的批量操作,能够有效减少单次操作的数据库访问次数,提升系统的响应能力。

优化方案:

  1. 批量插入数据
    :通过
    BULK INSERT
    或者
    INSERT INTO
    多行插入方式,减少多次单独插入操作带来的性能瓶颈。
  2. 批量更新数据
    :使用
    UPDATE
    操作一次性更新多条记录。
  3. 批量删除数据
    :批量删除过期的订单,或者批量删除无效的用户信息。

以下是具体的 SQL Server 批量数据处理的代码案例。

1. 批量插入数据

批量插入可以减少大量单独插入操作的时间开销,通过
INSERT INTO
语句一次插入多条数据。

示例:批量插入订单数据

-- 假设 Orders 表结构如下:OrderID INT, CustomerID INT, OrderDate DATETIME, OrderStatus VARCHAR(20)
DECLARE @OrderData TABLE (OrderID INT, CustomerID INT, OrderDate DATETIME, OrderStatus VARCHAR(20));

-- 将订单数据插入临时表
INSERT INTO @OrderData (OrderID, CustomerID, OrderDate, OrderStatus)
VALUES
    (1, 101, '2024-11-01', 'Pending'),
    (2, 102, '2024-11-02', 'Shipped'),
    (3, 103, '2024-11-03', 'Delivered'),
    (4, 104, '2024-11-04', 'Cancelled');

-- 批量插入数据到 Orders 表
INSERT INTO Orders (OrderID, CustomerID, OrderDate, OrderStatus)
SELECT OrderID, CustomerID, OrderDate, OrderStatus
FROM @OrderData;

在此例中,我们先将数据插入临时表
@OrderData
,然后通过
INSERT INTO SELECT
语句批量插入
Orders
表。这种方式可以大大减少数据库访问的次数。

2. 批量更新数据

批量更新操作通常用于修改多个记录中的某些字段,避免多次单独更新。

示例:批量更新订单状态

假设需要批量更新所有未发货的订单状态为 "Shipped",可以通过如下 SQL 来实现:

-- 批量更新订单状态
UPDATE Orders
SET OrderStatus = 'Shipped'
WHERE OrderStatus = 'Pending' AND OrderDate < '2024-11-01';

该操作会一次性更新所有符合条件的记录,避免多次单独更新操作带来的性能问题。

3. 批量删除数据

在某些场景下,我们需要批量删除某些过期或无效的数据。例如,删除 30 天之前的过期订单。

示例:批量删除过期订单

-- 删除过期的订单
DELETE FROM Orders
WHERE OrderDate < DATEADD(DAY, -30, GETDATE()) AND OrderStatus = 'Completed';

在这个例子中,我们删除所有已完成且订单日期超过 30 天的订单。这种批量删除操作比逐个删除要高效得多。

4. 批量处理逻辑优化

有时批量操作的数据量非常大,直接处理可能导致性能问题或数据库锁争用。可以考虑分批次执行操作来减轻系统负担。

示例:按批次处理订单数据

DECLARE @BatchSize INT = 1000;
DECLARE @StartRow INT = 0;
DECLARE @TotalRows INT;

-- 计算总记录数
SELECT @TotalRows = COUNT(*) FROM Orders WHERE OrderStatus = 'Pending';

-- 循环批量处理数据
WHILE @StartRow < @TotalRows
BEGIN
    -- 批量更新 1000 条数据
    UPDATE TOP (@BatchSize) Orders
    SET OrderStatus = 'Shipped'
    WHERE OrderStatus = 'Pending' AND OrderDate < '2024-11-01' AND OrderID > @StartRow;

    -- 更新已处理的行数
    SET @StartRow = @StartRow + @BatchSize;
END

通过分批次处理(每次处理 1000 条记录),可以避免一次性处理大量数据时造成的性能瓶颈或数据库锁的问题。适用于需要批量更新大量记录的情况。

5. 使用事务保证数据一致性

对于批量操作来说,通常需要使用事务来保证数据一致性,即要么全部成功,要么全部失败。

示例:批量插入订单并使用事务

BEGIN TRANSACTION;

BEGIN TRY
    -- 假设 Orders 表结构:OrderID INT, CustomerID INT, OrderDate DATETIME, OrderStatus VARCHAR(20)
    DECLARE @OrderData TABLE (OrderID INT, CustomerID INT, OrderDate DATETIME, OrderStatus VARCHAR(20));

    -- 批量插入订单数据
    INSERT INTO @OrderData (OrderID, CustomerID, OrderDate, OrderStatus)
    VALUES
        (5, 105, '2024-11-05', 'Pending'),
        (6, 106, '2024-11-06', 'Pending');

    INSERT INTO Orders (OrderID, CustomerID, OrderDate, OrderStatus)
    SELECT OrderID, CustomerID, OrderDate, OrderStatus
    FROM @OrderData;

    -- 提交事务
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- 错误处理并回滚事务
    ROLLBACK TRANSACTION;
    PRINT 'Error occurred: ' + ERROR_MESSAGE();
END CATCH;

在这个例子中,批量插入操作被包含在一个事务中,确保插入操作的原子性,即要么全部成功,要么全部失败。如果在执行过程中发生错误,会回滚事务,避免数据不一致的情况。

小结一下

批量数据处理是提高 SQL Server 性能的有效手段,尤其是在数据量庞大的电商平台等业务场景中。通过合理使用批量插入、批量更新和批量删除操作,可以大幅度提高数据库的处理效率,减少数据库的 I/O 操作次数和锁竞争。在执行批量操作时,记得通过事务保证数据的一致性,分批处理可以进一步优化大规模数据的处理性能。

8. 清理无用数据

  • 删除过期数据
    :定期清理过期或不再需要的数据,减少数据库的大小和查询的复杂性。
  • 清理数据库碎片
    :随着数据的增删,表和索引的碎片会增加,影响性能。定期重建索引或重新组织索引,减少碎片。

清理无用数据是数据库维护中的常见任务,特别是在处理历史数据、过期记录或冗余数据时。定期清理无用数据不仅能够节省存储空间,还能提高数据库性能,避免无用数据对查询、索引等造成不必要的影响。

业务场景:

假设我们在一个电商平台中,用户的订单数据每年都会生成大量记录。为了避免订单表过于庞大,且不再使用的订单记录(比如 3 年之前的订单)会占用大量存储空间,我们需要定期清理这些过期订单数据。

优化方案:

  1. 删除过期数据
    :定期删除超过一定时间的订单数据(比如 3 年前的订单)。
  2. 归档过期数据
    :将过期的订单数据移到一个历史表或外部存储中,保留必要的历史信息。

代码示例

1. 定期删除过期数据

假设我们的
Orders
表有字段
OrderDate
来记录订单的创建时间,
OrderStatus
来标识订单状态。我们可以每月清理 3 年前的已完成或已取消的订单。

-- 删除 3 年前已完成或已取消的订单
DELETE FROM Orders
WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
    AND OrderStatus IN ('Completed', 'Cancelled');

在这个例子中,
DATEADD(YEAR, -3, GETDATE())
会计算出当前日期 3 年前的日期,所有在此日期之前且状态为
'Completed'

'Cancelled'
的订单将被删除。

2. 定期归档过期数据

如果删除数据不符合业务需求,可以选择将数据归档。比如,将 3 年前的订单转移到
ArchivedOrders
表。

-- 将 3 年前的已完成或已取消的订单移动到 ArchivedOrders 表
INSERT INTO ArchivedOrders (OrderID, CustomerID, OrderDate, OrderStatus)
SELECT OrderID, CustomerID, OrderDate, OrderStatus
FROM Orders
WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
    AND OrderStatus IN ('Completed', 'Cancelled');

-- 删除已归档的订单
DELETE FROM Orders
WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
    AND OrderStatus IN ('Completed', 'Cancelled');

首先将符合条件的订单数据插入到
ArchivedOrders
表,然后再删除原
Orders
表中的这些数据。这样可以保持主表的清洁,减少存储压力,并保留历史数据。

3. 使用触发器自动清理无用数据

为了自动化清理操作,可以使用数据库触发器(Trigger),例如,在每次插入数据时检查数据是否超期,如果超期则触发清理操作。触发器可以周期性地执行清理任务。

-- 创建触发器,每天检查并删除 3 年前的订单
CREATE TRIGGER CleanOldOrders
ON Orders
AFTER INSERT, UPDATE
AS
BEGIN
    -- 清理过期订单:删除 3 年前的已完成或已取消订单
    DELETE FROM Orders
    WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
        AND OrderStatus IN ('Completed', 'Cancelled');
END;

此触发器将在
Orders
表每次执行插入或更新操作时触发,自动检查并清理过期的订单。

4. 分批次清理无用数据

如果订单数据量非常大,直接删除可能会导致性能瓶颈或数据库锁定问题。在这种情况下,可以分批次删除数据,以减少单次删除操作的负载。

DECLARE @BatchSize INT = 1000;
DECLARE @StartRow INT = 0;
DECLARE @TotalRows INT;

-- 计算需要删除的记录数
SELECT @TotalRows = COUNT(*) FROM Orders
WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
    AND OrderStatus IN ('Completed', 'Cancelled');

-- 分批次删除
WHILE @StartRow < @TotalRows
BEGIN
    -- 批量删除 1000 条数据
    DELETE TOP (@BatchSize) FROM Orders
    WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
        AND OrderStatus IN ('Completed', 'Cancelled')
        AND OrderID > @StartRow;

    -- 更新已删除的行数
    SET @StartRow = @StartRow + @BatchSize;
END

通过分批次处理删除操作,每次删除少量记录,减少对数据库性能的影响,并避免长时间锁定表。

5. 使用作业调度器定期清理无用数据

如果您使用的是 SQL Server,可以使用作业调度器(SQL Server Agent)定期执行清理任务。首先,您可以创建一个存储过程来执行数据清理操作。

CREATE PROCEDURE CleanOldOrders
AS
BEGIN
    DELETE FROM Orders
    WHERE OrderDate < DATEADD(YEAR, -3, GETDATE()) 
        AND OrderStatus IN ('Completed', 'Cancelled');
END;

然后,在 SQL Server Management Studio 中设置定期作业(例如每天午夜运行该存储过程),这样可以确保无用数据定期清理。

小结一下

清理无用数据不仅有助于节省存储空间,还能提高数据库性能。根据实际业务需求,我们可以选择删除、归档或分批处理的方式来清理数据。特别是对于大数据量的表,分批清理和定期作业调度可以有效减少系统的负担。

9. 使用缓存

  • 缓存常用查询结果
    :对于高频次查询,可以将查询结果缓存到内存中,避免每次查询都去数据库中查找。
  • 应用层缓存
    :使用 Redis 或 Memcached 等缓存系统,将一些常用数据缓存在内存中,从而减少数据库访问频率。

在实际业务中,缓存是提高系统性能的常用手段,特别是对于高频访问的热点数据,通过将其存储在缓存中,可以减少数据库查询的次数和压力,提高响应速度。

业务场景

假设我们有一个电商平台,用户在浏览商品详情时,频繁地查询商品的基本信息(如价格、库存、描述等)。由于商品信息变化较少,而查询请求频繁,因此将商品信息缓存起来能够有效提高系统的性能。

我们使用 Redis 作为缓存数据库,常见的做法是:当查询某个商品时,首先检查缓存中是否存在该商品的详情,如果存在,则直接返回缓存中的数据;如果缓存中没有,则从数据库中查询,并将查询结果存入缓存中,以备下次使用。

解决方案

  1. 使用 Redis 存储商品信息。
  2. 设置适当的过期时间(TTL,Time To Live),避免缓存数据过期。
  3. 使用适当的缓存更新策略(例如:每次更新商品信息时更新缓存)。

代码示例

1. 设置 Redis 缓存

首先,使用 Redis 的客户端库(如
redis-py
)连接 Redis 服务。假设商品信息表为
Products
,有字段
ProductID
,
ProductName
,
Price
,
Stock
,
Description

# 安装 Redis 客户端
pip install redis

2. 商品查询和缓存逻辑

import redis
import mysql.connector
import json

# 连接 Redis
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True)

# 连接 MySQL 数据库
def get_db_connection():
    return mysql.connector.connect(
        host="localhost",
        user="root",
        password="password",
        database="ecommerce"
    )

# 获取商品详情
def get_product_details(product_id):
    # 检查缓存
    cached_product = redis_client.get(f"product:{product_id}")
    
    if cached_product:
        print("从缓存中获取商品信息")
        return json.loads(cached_product)  # 反序列化 JSON 数据
    
    # 如果缓存中没有,查询数据库
    print("从数据库中获取商品信息")
    connection = get_db_connection()
    cursor = connection.cursor(dictionary=True)
    cursor.execute("SELECT * FROM Products WHERE ProductID = %s", (product_id,))
    product = cursor.fetchone()
    
    # 如果商品存在,缓存到 Redis 中
    if product:
        redis_client.setex(f"product:{product_id}", 3600, json.dumps(product))  # 缓存 1 小时
    cursor.close()
    connection.close()
    
    return product

# 更新商品信息并更新缓存
def update_product_details(product_id, name, price, stock, description):
    # 更新数据库
    connection = get_db_connection()
    cursor = connection.cursor()
    cursor.execute("""
        UPDATE Products
        SET ProductName = %s, Price = %s, Stock = %s, Description = %s
        WHERE ProductID = %s
    """, (name, price, stock, description, product_id))
    connection.commit()
    cursor.close()
    connection.close()
    
    # 更新缓存
    updated_product = {
        "ProductID": product_id,
        "ProductName": name,
        "Price": price,
        "Stock": stock,
        "Description": description
    }
    redis_client.setex(f"product:{product_id}", 3600, json.dumps(updated_product))  # 缓存 1 小时

# 示例:查询商品 101 的信息
product_info = get_product_details(101)
print(product_info)

# 示例:更新商品 101 的信息
update_product_details(101, "New Product Name", 199.99, 50, "Updated description")

代码说明

  1. 连接 Redis 和 MySQL:
    使用
    redis-py
    连接 Redis,使用
    mysql.connector
    连接 MySQL 数据库。
  2. 查询商品:

    get_product_details
    方法中,我们首先查询 Redis 缓存,看是否已经缓存了商品信息。如果缓存中存在,则直接返回缓存中的数据;如果缓存中没有,则从 MySQL 数据库中查询,并将查询结果缓存到 Redis 中。
  3. 更新商品信息:
    当商品信息发生变化时(例如商品名称、价格、库存等更新),我们在数据库中更新商品信息后,同时更新 Redis 缓存,以确保缓存数据的最新性。
  4. 缓存设置过期时间:
    使用
    setex
    方法将商品信息缓存到 Redis 中,并为缓存数据设置过期时间(TTL)。这样可以避免缓存过期数据的存在。

进一步优化

  1. 缓存穿透:
    在查询时,除了检查缓存是否存在外,还可以添加一些防止缓存穿透的机制,如查询数据库时检查是否存在该商品。如果商品不存在,可以将其设置为
    None
    或空值,避免多次查询数据库。
  2. 缓存淘汰策略:
    Redis 有多种缓存淘汰策略(如 LRU、LFU),可以根据实际业务需求配置 Redis 实例的缓存策略,确保热点数据可以长时间保持在缓存中。
  3. 异步更新缓存:
    在高并发的场景下,更新缓存的操作可能导致性能问题,可以使用队列和异步处理来优化缓存更新的时机,避免频繁更新缓存。

小结一下

通过使用 Redis 缓存,电商平台能够有效提高查询商品信息的性能,减轻数据库负担。根据业务需求,我们可以进一步优化缓存策略和更新机制。

10. 并行查询与并发

  • 启用并行查询
    :SQL Server 允许在查询中使用多个 CPU 核心来并行处理。适当调整并行查询的设置(如
    max degree of parallelism
    )可以提高查询性能,尤其是在处理大量数据时。
  • 优化锁策略
    :确保数据库的锁策略合理,避免长时间的锁竞争。可以使用行级锁而不是表级锁,减少阻塞。

在高并发场景下,使用并行查询可以显著提升数据查询的速度。并行查询的核心思想是将复杂的查询拆分成多个子任务,利用多个 CPU 核心同时处理这些子任务,从而提高整体查询性能。并发则是指在多个任务之间进行切换,使得 CPU 更高效地利用,在某些场景下,通过并发执行多个查询任务可以实现较高的性能。

业务场景

假设我们有一个电商平台,其中存储了大量的订单数据。用户查询订单数据时,可能涉及到多个表的联接、多个条件的筛选等复杂的查询操作。为了提高查询性能,我们可以通过并行查询和并发的方式,针对不同的查询任务进行优化。

例如,查询订单数据时,查询条件包括订单状态、订单日期范围和用户 ID 等。我们将该查询拆分为多个并行查询,分别查询不同的条件,再将结果合并返回。

解决方案

  1. 并行查询:
    将查询任务拆分成多个子任务,利用多线程或者多进程并行执行每个子任务。
  2. 并发查询:
    使用异步 IO 或者线程池来并发执行多个查询操作。

我们将使用 Python 的
concurrent.futures
库来实现并行查询,并利用 MySQL 数据库来执行查询操作。

代码示例

1. 并行查询

我们将查询条件分为多个部分,并行地执行查询操作。例如:分别查询订单状态为
Completed

Pending
的订单数据,并行查询。

# 安装 MySQL 客户端库
pip install mysql-connector-python
import mysql.connector
from concurrent.futures import ThreadPoolExecutor
import time

# 连接 MySQL 数据库
def get_db_connection():
    return mysql.connector.connect(
        host="localhost",
        user="root",
        password="123123",
        database="VGDB"
    )

# 执行查询:查询订单状态为指定状态的订单
def query_orders_by_status(status):
    connection = get_db_connection()
    cursor = connection.cursor(dictionary=True)
    query = "SELECT * FROM Orders WHERE OrderStatus = %s"
    cursor.execute(query, (status,))
    result = cursor.fetchall()
    cursor.close()
    connection.close()
    return result

# 执行并行查询
def fetch_orders():
    statuses = ['Completed', 'Pending']  # 定义我们需要查询的订单状态
    # 使用 ThreadPoolExecutor 并行查询
    with ThreadPoolExecutor(max_workers=2) as executor:
        # 提交查询任务
        futures = [executor.submit(query_orders_by_status, status) for status in statuses]
        # 获取查询结果
        results = [future.result() for future in futures]
    
    return results

# 示例:执行查询
if __name__ == "__main__":
    start_time = time.time()
    orders = fetch_orders()
    print("查询结果:", orders)
    print(f"查询用时: {time.time() - start_time}秒")

代码说明

  1. query_orders_by_status
    :该方法执行数据库查询,查询指定状态的订单。
  2. fetch_orders
    :该方法使用
    ThreadPoolExecutor
    来并行执行多个查询任务。在这里,我们将订单状态
    Completed

    Pending
    分别作为任务提交到线程池中并行查询。
  3. ThreadPoolExecutor
    :我们创建了一个最大工作线程数为 2 的线程池,并使用
    submit
    提交查询任务。每个查询会在一个独立的线程中执行。
  4. future.result()
    :获取并行查询任务的返回结果。

2. 并发查询

我们可以通过异步查询或多线程来执行并发查询,适用于数据库查询不会互相依赖的情况。

import asyncio
import mysql.connector
from concurrent.futures import ThreadPoolExecutor

# 异步查询数据库
async def query_orders_by_status_async(status, loop):
    # 使用 ThreadPoolExecutor 让数据库查询异步执行
    result = await loop.run_in_executor(None, query_orders_by_status, status)
    return result

# 执行查询:查询订单状态为指定状态的订单
def query_orders_by_status(status):
    connection = get_db_connection()
    cursor = connection.cursor(dictionary=True)
    query = "SELECT * FROM Orders WHERE OrderStatus = %s"
    cursor.execute(query, (status,))
    result = cursor.fetchall()
    cursor.close()
    connection.close()
    return result

# 异步并发查询
async def fetch_orders_concurrently():
    loop = asyncio.get_event_loop()
    statuses = ['Completed', 'Pending', 'Shipped']  # 查询多个状态的订单
    tasks = [query_orders_by_status_async(status, loop) for status in statuses]
    orders = await asyncio.gather(*tasks)  # 等待所有任务完成
    return orders

# 示例:执行并发查询
if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(fetch_orders_concurrently())
    print(f"查询用时: {time.time() - start_time}秒")

代码说明

  1. query_orders_by_status_async
    :此方法使用
    loop.run_in_executor
    来将数据库查询操作异步化。通过这种方式,尽管数据库查询是阻塞操作,我们可以并发地执行多个查询。
  2. asyncio.gather
    :将多个异步任务组合在一起,等待所有任务完成后再返回结果。
  3. asyncio.run
    :用于启动事件循环并执行异步查询。

进一步优化

  1. 线程池大小
    :根据业务需求,调整
    ThreadPoolExecutor
    中的
    max_workers
    参数。如果任务非常多,可以适当增加线程池大小,但要注意不要过多,以免影响系统性能。
  2. 连接池
    :对于数据库操作,可以使用数据库连接池来优化数据库连接的管理。这样可以避免每次查询都建立新的数据库连接,提高性能。
  3. 分页查询
    :如果查询结果非常庞大,可以通过分页查询来减小每次查询的数据量,进一步提高性能。

总结

  • 并行查询
    :通过将查询任务拆分为多个子任务,并行地处理,可以显著提高查询性能。
  • 并发查询
    :适用于在多个查询任务之间进行并发执行,无需等待每个查询任务逐个完成,可以加快整体查询速度。

通过结合并行查询和并发查询策略,我们可以显著提高电商平台或其他业务系统的查询响应速度,尤其是在高并发的环境中,保证系统的高效性。

11. SQL Server 实例优化

  • 定期重启 SQL Server 实例
    :如果 SQL Server 长时间运行,可能会导致缓存过多或内存泄漏等问题,定期重启可以帮助释放资源并优化性能。
  • 启用压缩
    :SQL Server 提供数据压缩功能,可以节省存储空间,并提高查询性能,尤其是在读取数据时。

SQL Server 实例优化是提升数据库整体性能的一个重要方面。在大型业务系统中,SQL Server 的性能往往直接影响到整个应用的响应速度和稳定性。实例优化包括硬件资源的合理配置、SQL Server 配置参数的优化、内存和 I/O 管理、查询优化以及监控等方面。

假设我们有一个在线电商平台,业务量很大,包含大量的商品、订单、用户等数据。我们需要对 SQL Server 实例进行优化,以确保高效的查询性能、稳定的事务处理和快速的数据读取能力。

1. 硬件配置优化

SQL Server 实例的性能在很大程度上取决于底层硬件的配置,尤其是内存、CPU、磁盘等资源。

  • 内存
    :SQL Server 是一个内存密集型应用,内存越大,缓存命中率越高,查询性能也越好。
  • CPU
    :更多的 CPU 核心可以处理更多并发请求。
  • 磁盘
    :SSD 驱动器在磁盘 I/O 性能方面要优于传统硬盘,尤其是在大型数据库的读写操作中。

2. SQL Server 配置优化

SQL Server 提供了很多配置参数来调整实例的行为,可以通过这些参数来优化性能。

配置参数示例

  • max degree of parallelism
    :控制 SQL Server 查询的并行度。通过合理设置并行度,可以提高多核 CPU 系统的查询效率。
  • max server memory
    :限制 SQL Server 使用的最大内存量,防止 SQL Server 占用过多内存导致操作系统性能下降。
  • cost threshold for parallelism
    :设置查询执行的代价阈值,只有当查询的成本超过该值时,SQL Server 才会使用并行执行。

3. 索引优化

索引是提高查询性能的关键,可以根据业务场景为频繁查询的字段创建索引。但过多的索引会影响插入、更新和删除操作的性能,因此需要在查询性能和维护成本之间找到平衡。

4. 查询优化

对于大型业务系统,查询优化尤为重要。优化查询可以减少数据库的负担,提升响应速度。

业务场景

假设电商平台需要处理大量的订单数据,查询常常涉及到联接多个表,比如查询某个用户在某个时间段内的所有订单。我们可以通过优化 SQL 查询来提高查询速度。

代码示例

1. 设置 SQL Server 实例配置参数

在 SQL Server 实例中,我们可以通过以下 T-SQL 语句来设置一些基本的优化参数:

-- 设置最大内存使用量为 16 GB
EXEC sp_configure 'max server memory', 16384;  -- 单位:MB
RECONFIGURE;

-- 设置最大并行度为 8 核 CPU
EXEC sp_configure 'max degree of parallelism', 8;
RECONFIGURE;

-- 设置查询的成本阈值为 10
EXEC sp_configure 'cost threshold for parallelism', 10;
RECONFIGURE;

2. 查询优化

为了提高查询性能,可以在查询时使用以下技巧:

  • 避免 SELECT *,仅选择需要的字段。
  • 使用 JOIN 替代子查询,避免不必要的嵌套查询。
  • 创建适当的索引来加速查询。
  • 利用分页查询减少单次查询的数据量。

以下是一个优化后的查询示例:

-- 假设我们需要查询某个用户的订单信息,优化后的 SQL 查询
SELECT o.OrderID, o.OrderDate, o.TotalAmount, u.UserName
FROM Orders o
JOIN Users u ON o.UserID = u.UserID
WHERE o.OrderDate BETWEEN '2024-01-01' AND '2024-12-31'
  AND u.UserID = 12345
ORDER BY o.OrderDate DESC;

3. 索引优化

为了优化查询,我们可以在
Orders
表的
UserID

OrderDate
字段上创建索引:

-- 为 UserID 列创建索引
CREATE INDEX idx_user_id ON Orders(UserID);

-- 为 OrderDate 列创建索引
CREATE INDEX idx_order_date ON Orders(OrderDate);

-- 为 UserID 和 OrderDate 的组合创建复合索引
CREATE INDEX idx_user_order_date ON Orders(UserID, OrderDate);

4. 数据库备份和维护

定期备份和维护数据库可以确保系统在高负载下保持高效。定期的数据库优化任务包括:

  • 备份数据。
  • 更新统计信息。
  • 重建索引。

以下是一个定期重建索引的示例:

-- 重建所有表的索引
ALTER INDEX ALL ON Orders REBUILD;
ALTER INDEX ALL ON Users REBUILD;

5. 使用 SQL Server 的性能监控工具

SQL Server 提供了一些性能监控工具来帮助识别性能瓶颈。例如,
SQL Server Profiler

Dynamic Management Views (DMVs)
可以帮助我们实时监控 SQL Server 实例的性能,并根据实际情况进行调优。

-- 查看 SQL Server 实例当前的资源使用情况
SELECT * FROM sys.dm_exec_requests;

-- 查看 SQL Server 实例的内存使用情况
SELECT * FROM sys.dm_os_memory_clerks;

-- 查看 SQL Server 实例的磁盘 I/O 使用情况
SELECT * FROM sys.dm_io_virtual_file_stats(NULL, NULL);

小结一下

  1. 硬件优化
    :合理配置 CPU、内存和磁盘,提升 SQL Server 实例的性能。
  2. 实例配置优化
    :通过配置 SQL Server 的参数,如内存限制、并行度等,优化性能。
  3. 索引优化
    :合理设计索引结构,提高查询效率。
  4. 查询优化
    :使用高效的 SQL 查询语句,避免不必要的计算和 I/O 操作。
  5. 定期维护和备份
    :定期进行数据库维护和备份,确保系统稳定运行。

通过对 SQL Server 实例的优化,可以显著提升数据库的性能,确保电商平台在高并发、高负载的情况下仍能保持高效响应。

最后

以上11种优化方案供你参考,优化 SQL Server 数据库性能得从多个方面着手,包括硬件配置、数据库结构、查询优化、索引管理、分区分表、并行处理等。通过合理的索引、查询优化、数据分区等技术,可以在数据量增大时保持较好的性能。同时,定期进行数据库维护和清理,保证数据库高效运行。关注威哥爱编程,V哥做你的技术门童。