海量数据处理算法总结【超详解】
Bloom Filter(BF)是⼀种空间效率很⾼的随机,它利⽤位数组很简洁地表⽰⼀个集合,并能判断⼀个元素是否属于这个集合。它是⼀个判断元素是否存在集合的快速的概率算法。Bloom Filter有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter判断元素不再集合,那肯定不在。如果判断元素存在集合中,有⼀定的概率判断错误。因此,Bloom Filter不适合那些“零错误”的应⽤场合。
⽽在能容忍低错误率的应⽤场合下,Bloom Filter⽐其他常见的算法(如hash,折半查)极⼤节省了空间。
Bloom Filter的详细介绍:
【适⽤范围】
可以⽤来实现数据字典,进⾏数据的判重,或者集合求交集
【基本原理及要点】
原理要点:⼀是位数组,⽽是k个独⽴hash函数。
1)位数组:
假设Bloom Filter使⽤⼀个m⽐特的数组来保存信息,初始状态时,Bloom Filter是⼀个包含m位的位数组,每⼀位都置为0,即BF整个数组的元素都设置为0。
2)k个独⽴hash函数
为了表达S={x1, x2,…,x n}这样⼀个n个元素的集合,Bloom Filter使⽤k个相互独⽴的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。
当我们往Bloom Filter中增加任意⼀个元素x时候,我们使⽤k个哈希函数得到k个哈希值,然后将数组中对应的⽐特位设置为1。即第i个哈希函数映射的位
置hash i(x)就会被置为1(1≤i≤k)。注意,如果⼀个位置多次被置为1,那么只有第⼀次会起作⽤,后⾯⼏次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同⼀个位置(从左边数第五位,即第⼆个“1“处)。
3)判断元素是否存在集合
在判断y是否属于这个集合时,我们只需要对y使⽤k个哈希函数得到k个哈希值,如果所有hash i(y)的位置都是1(1≤i≤k),即k个位置都被设置为1了,那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y1就不是集合中的元素(因为y1有⼀处指向了“0”位)。y2或者属于这个集合,或者
刚好是⼀个false positive。
显然这个判断并不保证查的结果是100%正确的。
Bloom Filter的缺点:
1)Bloom Filter⽆法从Bloom Filter集合中删除⼀个元素。因为该元素对应的位会牵动到其他的元素。所以⼀个简单的改进就是 counting Bloom filter,⽤⼀个counter数组代替位数组,就可以⽀持删除了。此外,Bloom Filter的hash函数选择会影响算法的效果。
2)还有⼀个⽐较重要的问题,如何根据输⼊元素个数n,确定位数组m的⼤⼩及hash函数个数,即hash函数选择会影响算法的效果。当hash函数个数k= (ln2)*(m/n)时错误率最⼩。在错误率不⼤于E的情况下,m⾄少要等于n*lg(1/E) 才能表⽰任意n个元素的集合。但m还应该更⼤些,因为还要保证bit数组⾥⾄少⼀半为0,则m应该>=nlg(1/E)*lge ,⼤概就是nlg(1/E)1.44倍(lg表⽰以2为底的对数)。
举个例⼦我们假设错误率为0.01,则此时m应⼤概是n的13倍。这样k⼤概是8个。
注意:
这⾥m与n的单位不同,m是bit为单位,⽽n则是以元素个数为单位(准确的说是不同元素的个数)。通常单个元素的长度都是有很多bit的。所以使⽤bloom filter内存上通常都是节省的。
⼀般BF可以与⼀些key-value的⼀起使⽤,来加快查询。由于BF所⽤的空间⾮常⼩,所有BF可以常驻内存。这样⼦的话,对于⼤部分不存在的元素,我们只需要访问内存中的BF就可以判断出来了,只有⼀⼩部分,我们需要访问在硬盘上的key-value数据库。从⽽⼤⼤地提⾼了效率。
【扩展】
Bloom filter将集合中的元素映射到位数组中,⽤k(k为哈希函数个数)个映射位是否全1表⽰元素在不在这个集合中。Counting bloom filter(CBF)将位数组中的每⼀位扩展为⼀个counter,从⽽⽀持了元素的删除操作。Spectral Bloom Filter(SBF)将其与集合元素的出现次数关联。SBF采⽤counter中的最⼩值来近似表⽰元素的出现频率。
【问题实例】
给你A,B两个⽂件,各存放50亿条URL,每条URL占⽤64字节,内存限制是4G,让你出A,B⽂件共同的URL。如果是三个乃⾄n个⽂件呢?
根据这个问题我们来计算下内存的占⽤,4G=2^32⼤概是40亿*8⼤概是340亿bit,n=50亿,如果按出错率0.01算需要的⼤概是650亿个bit。现在可⽤的是340亿,相差并不多,这样可能会使出错率上升些。另外如果这些urlip是⼀⼀对应的,就可以转换成ip,则⼤⼤简单了。
2. Hash
【什么是Hash】
Hash,⼀般翻译做“散列”,也有直接⾳译为“哈希”的,就是把任意长度的输⼊(⼜叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该
输出就是散列值。这种转换是⼀种压缩映射,也就是,散列值的空间通常远⼩于输⼊的空间,不同的输⼊可能会散列成相同的输出,⽽不可能从散列值来唯⼀的确定输⼊值。简单的说就是⼀种将任意长度的消息压缩到某⼀固定长度的消息摘要的函数。
HASH主要⽤于信息安全领域中加密算法,它把⼀些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,hash就是到⼀种数据内容和数据存放地址之间的映射关系。
数组的特点是:寻址容易,插⼊和删除困难;⽽链表的特点是:寻址困难,插⼊和删除容易。那么我们能不能综合两者的特性,做出⼀种寻址容易,插⼊删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现⽅法,我接下来解释的是最常⽤的⼀种⽅法——拉链法,(也是树的⼀种存储结构,称为⼆叉链表)我们可以理解为“链表的数组”,如图:
左边很明显是个数组,数组的每个成员包括⼀个指针,指向⼀个链表的头,当然这个链表可能为空,也
可能元素很多。我们根据元素的⼀些特征把元素分配到不同的链表中去,也是根据这些特征,到正确的链表,再从链表中出这个元素。
元素特征转变为数组下标的⽅法就是散列法。
散列法当然不⽌⼀种,下⾯列出三种⽐较常⽤的:
1,除法散列法(求模数)
最直观的⼀种,上图使⽤的就是这种散列法,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过⼀个除法运算得到的,所以叫“除法散列法”。
2,平⽅散列法
求index是⾮常频繁的操作,⽽乘法的运算要⽐除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和⼀个位移操作。公式:
index = (value * value) >> 28
如果数值分配⽐较均匀的话这种⽅法能得到不错的结果,但我上⾯画的那个图的各个元素的值算出来的index都是0——⾮常失败。也许你还有个问题,value如果很⼤,value * value不会溢出吗?答案是会的,但我们这个乘法不关⼼溢出,因为我们根本不是为了获取相乘结果,⽽是为了获取index。
3,斐波那契(Fibonacci)散列法
平⽅散列法的缺点是显⽽易见的,所以我们能不能出⼀个理想的乘数,⽽不是拿value本⾝当作乘数呢?答案是肯定的。
1,对于16位整数⽽⾔,这个乘数是40503
2,对于32位整数⽽⾔,这个乘数是2654435769
3,对于64位整数⽽⾔,这个乘数是11400714819323198485
这⼏个“理想乘数”是如何得出来的呢?这跟⼀个法则有关,叫黄⾦分割法则,⽽描述黄⾦分割法则的最经典表达式⽆疑就是著名的斐波那契数列,如果你还有兴趣,就到⽹上查⼀下“斐波那契数列”等关键字,我数学⽔平有限,不知道怎么描述清楚为什么,另外斐波那契数列的值居然和太阳系⼋⼤⾏星的轨道半径的⽐例出奇吻合,很神奇,对么?
对我们常见的32位整数⽽⾔,公式:
i ndex = (value * 2654435769) >> 28
如果⽤这种斐波那契散列法的话,那我上⾯的图就变成这样了:
很明显,⽤斐波那契散列法调整之后要⽐原来的取摸散列法好很多。
【适⽤范围】
快速查,删除的基本数据结构,通常需要总数据量可以放⼊内存。
【基本原理及要点】
hash函数选择,针对字符串,整数,排列,具体相应的hash⽅法。
碰撞处理:
⼀种是open hashing,也称为拉链法;
另⼀种就是closed hashing,也称开地址法,opened addressing。
【扩展】
d-left hashing中的d是多个的意思,我们先简化这个问题,看⼀看2-left hashing。2-left hashing指的是将⼀个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备⼀个哈希函数,h1和h2。在存储⼀个新的key时,同时⽤两个哈希函数进⾏计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的
h1[key]位置和T2中的h2[key]位置,哪⼀个位置已经存储的(有碰撞的)key⽐较多,然后将新key存储在负载少的位置。如果两边⼀样多,⽐如两个位置都为空或者都存储了⼀个key,就把新key 存储在左边的T1⼦表中,2-left也由此⽽来。在查⼀个key时,必须进⾏两次hash,同时查两个位置。
【问题实例】
1).海量⽇志数据,提取出某⽇访问百度次数最多的那个IP。
IP的数⽬还是有限的,最多2^32个,所以可以考虑使⽤hash将ip直接存⼊内存,然后进⾏统计。
3. Bit-map
【什么是Bit-map】
所谓的就是⽤⼀个bit位来标记某个元素对应的Value,⽽Key即是该元素。由于采⽤了Bit为单位来存储数据,因此在存储空间⽅⾯,可以⼤⼤节省。
如果说了这么多还没明⽩什么是Bit-map,那么我们来看⼀个具体的例⼦,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这⾥假设这些元素没有重复)。那么我们就可以采⽤Bit-map的⽅法来达到排序的⽬的。要表⽰8个数,我们就只需要8个Bit(1Bytes),⾸先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:)
然后遍历这5个元素,⾸先第⼀个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0x01<<(i%8)) 当然了这⾥的操作涉及到Big-ending和Little-ending 的情况,这⾥默认为Big-ending),因为是从零开始的,所以要把第五位置为⼀(如下图):
然后再处理第⼆个元素7,将第⼋位置为1,,接着再处理第三个元素,⼀直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:
然后我们现在遍历⼀遍Bit区域,将该位是⼀的位的编号输出(2,3,4,5,7),这样就达到了排序的⽬的。下⾯的代码给出了⼀个BitMap的⽤法:排序。
C代码
1//定义每个Byte中有8个Bit位
2 #include <memory.h>
3#define BYTESIZE 8
4void SetBit(char *p, int posi)
5 {
6for(int i=0; i < (posi/BYTESIZE); i++)
7 {
8 p++;
9 }
10
11 *p = *p|(0x01<<(posi%BYTESIZE));//将该Bit位赋值1
12return;
13 }
14
15void BitMapSortDemo()
16 {
17//为了简单起见,我们不考虑负数
18int num[] = {3,5,2,10,6,12,8,14,9};
19
20//BufferLen这个值是根据待排序的数据中最⼤值确定的
21//待排序中的最⼤值是14,因此只需要2个Bytes(16个Bit) 22//就可以了。
23const int BufferLen = 2;
24char *pBuffer = new char[BufferLen];
25
26//要将所有的Bit位置为0,否则结果不可预知。
27 memset(pBuffer,0,BufferLen);
28for(int i=0;i<9;i++)
29 {
30//⾸先将相应Bit位上置为1
31 SetBit(pBuffer,num[i]);
32 }
33
34//输出排序结果
35for(int i=0;i<BufferLen;i++)//每次处理⼀个字节(Byte)
36 {
37for(int j=0;j<BYTESIZE;j++)//处理该字节中的每个Bit位
38 {
39//判断该位上是否是1,进⾏输出,这⾥的判断⽐较笨。40//⾸先得到该第j位的掩码(0x01<<j),将内存区中的41//位和此掩码作与操作。最后判断掩码是否和处理后的42//结果相同
43if((*pBuffer&(0x01<<j)) == (0x01<<j))
44 {
45 printf("%d ",i*BYTESIZE + j);
46 }
47 }
48 pBuffer++;
49 }
50 }
51
52int _tmain(int argc, _TCHAR* argv[])
53 {
54 BitMapSortDemo();
55return0;
56 }
【适⽤范围】
字符串数组怎么转成byte可进⾏数据的快速查,判重,删除,⼀般来说数据范围是int的10倍以下
【基本原理及要点】
使⽤bit数组来表⽰某些元素是否存在,⽐如8位电话号码
【扩展】
可以看做是对的扩展
【问题实例】
1)已知某个⽂件内包含⼀些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,⼤概需要99m个bit(1024*1024 *99个bit ),⼤概10⼏m字节的内存即可。
申请内存空间的⼤⼩为:int a[1 + N/32] =((99 999 999/32 +1)*4 个字节/1024/1024 = 12M
(可以理解为从0-99 999 999的数字,每个数字对应⼀个Bit位,所以只需要99M个Bit==12MBytes,这样,就⽤了⼩⼩的12M左右的内存表⽰了所有的8位数的电话)
2)2.5亿个整数中出不重复的整数的个数,内存空间不⾜以容纳这2.5亿个整数。
将bit-map扩展⼀下,⽤2bit表⽰⼀个数即可,0表⽰未出现,1表⽰出现⼀次,2表⽰出现2次及以上,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不⽤2bit来进⾏表⽰,我们⽤两个bit-map即可模拟实现这个2bit-map,都是⼀样的道理。
4. 堆
【什么是堆】
在⼋⼤排序⾥⾯有堆的详细介绍:
概念:堆是⼀种特殊的⼆叉树,具备以下两种性质
1)每个节点的值都⼤于(或者都⼩于,称为最⼩堆)其⼦节点的值
2)树是完全平衡的,并且最后⼀层的树叶都在最左边
这样就定义了⼀个最⼤堆。如下图⽤⼀个数组来表⽰堆:
那么下⾯介绍⼆叉堆:⼆叉堆是⼀种完全⼆叉树,其任意⼦树的左右节点(如果有的话)的键值⼀定⽐根节点⼤,上图其实就是⼀个⼆叉堆。
你⼀定发觉了,最⼩的⼀个元素就是数组第⼀个元素,那么⼆叉堆这种有序队列如何⼊队呢?看图:
假设要在这个⼆叉堆⾥⼊队⼀个单元,键值为2,那只需在数组末尾加⼊这个元素,然后尽可能把这个元素往上挪,直到挪不动,经过了这种复杂度为Ο(logn)的操作,⼆叉堆还是⼆叉堆。
那如何出队呢?也不难,看图
出队⼀定是出数组的第⼀个元素,这么来第⼀个元素以前的位置就成了空位,我们需要把这个空位挪⾄叶⼦节点,然后把数组最后⼀个元素插⼊这个空位,把这个“空位”尽量往上挪。这种操作的复杂度也是Ο(logn)。
【适⽤范围】
海量数据前n⼤,并且n⽐较⼩,堆可以放⼊内存
【基本原理及要点】
最⼤堆求前n⼩,最⼩堆求前n⼤。⽅法,⽐如求前n⼩,我们⽐较当前元素与最⼤堆⾥的最⼤元素,如果它⼩于最⼤元素,则应该替换那个最⼤元素。这样最后得到的n个元素就是最⼩的n个。适合量,求前n⼩,n的⼤⼩⽐较⼩的情况,这样可以扫描⼀遍即可得到所有的前n元素,效率很⾼。
【扩展】
双堆,⼀个最⼤堆与⼀个最⼩堆结合,可以⽤来维护中位数。
【问题实例】
1)100w个数中最⼤的前100个数。
⽤⼀个100个元素⼤⼩的最⼩堆即可。
5. 双层桶
【什么是双层桶】
事实上,与其说双层桶划分是⼀种数据结构,不如说它是⼀种算法设计思想。⾯对⼀堆⼤量的数据我们⽆法处理的时候,我们可以将其分成⼀个个⼩的单元,然后根据⼀定的策略来处理这些⼩单元,从⽽达到⽬的。
【适⽤范围】
第k⼤,中位数,不重复或重复的数字
【基本原理及要点】
因为元素范围很⼤,不能利⽤直接寻址表,所以通过多次划分,逐步确定范围,然后最后在⼀个可以接受的范围内进⾏。可以通过多次缩⼩,双层只是⼀个例⼦,分治才是其根本(只是“只分不治”)。
【扩展】
当有时候需要⽤⼀个⼩范围的数据来构造⼀个⼤数据,也是可以利⽤这种思想,相⽐之下不同的,只是其中的逆过程。
【问题实例】
1).2.5亿个整数中出不重复的整数的个数,内存空间不⾜以容纳这2.5亿个整数。
有点像鸽巢原理,整数个数为2^32,也就是,我们可以将这2^32个数,划分为2^8=256个区域(⽐如⽤单个⽂件代表⼀个区域),然后将数据分离到不同的区域,
然后不同的区域在利⽤bitmap就可以直接解决了。也就是说只要有⾜够的磁盘空间,就可以很⽅便的解决。当然这个题也可以⽤我们前⾯讲过的BitMap⽅法解决,正所谓条条⼤道通罗马~~~
2).5亿个int它们的中位数。
这个例⼦⽐上⾯那个更明显。⾸先我们将int划分为2^16个区域,然后读取数据统计落到各个区域⾥的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第⼏⼤数刚好是中位数。然后第⼆次扫描我们只统计落在这个区域中的那些数就可以了。
实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int6
4分成2^24个区域,然后确定区域的第⼏⼤数,在将该区域分成2^20个⼦区域,然后确定是⼦区域的第⼏⼤数,然后⼦区域⾥的数的个数只有2^20,就可以直接利⽤direct addr table进⾏统计了。
3).现在有⼀个0-30000的随机数⽣成器。请根据这个随机数⽣成器,设计⼀个抽奖范围是0-350000中奖号码列表,其中要包含20000个中奖号码。
这个题刚好和上⾯两个思想相反,⼀个0到3万的随机数⽣成器要⽣成⼀个0到35万的随机数。那么我们完全可以将0-35万的区间分成35/3=12个区间,然后每个区间的长度都⼩于等于3万,这样我们就可以⽤题⽬给的随机数⽣成器来⽣成了,然后再加上该区间的基数。那么要每个区间⽣成多少个随机数呢?计算公式就是:区间长度*随机数密度,在本题⽬中就是30000*(20000/350000)。最后要注意⼀点,该题⽬是有隐含条件的:,这意味着你⽣成的随机数⾥⾯不能有重复,这也是我为什么⽤双层桶划分思想的另外⼀个原因。
6. 数据库索引及优化
索引是对数据库表中⼀列或多列的值进⾏排序的⼀种结构,使⽤索引可快速访问数据库表中的特定信息。
数据库索引
什么是索引
数据库索引好⽐是⼀本书前⾯的⽬录,能加快数据库的查询速度。
例如这样⼀个查询:select * from table1 where id=44。如果没有索引,必须遍历整个表,直到ID等于44的这⼀⾏被到为⽌;有了索引之后(必须是在ID 这⼀列上建⽴的索引),直接在索引⾥⾯44(也就是在ID这⼀列),就可以得知这⼀⾏的位置,也就是到了这⼀⾏。可见,索引是⽤来定位的。
索引分为聚簇索引和⾮聚簇索引两种,聚簇索引是按照数据存放的物理位置为顺序的,⽽⾮聚簇索引就不⼀样了;聚簇索引能提⾼多⾏检索的速度,⽽⾮聚簇索引对于单⾏的检索很快。
概述
建⽴索引的⽬的是加快对表中记录的查或排序。
为表设置索引要付出代价的:⼀是增加了数据库的存储空间,⼆是在插⼊和修改数据时要花费较多的时间(因为索引也要随之变动)。
B树索引-Sql Server索引⽅式
为什么要创建索引
创建索引可以⼤⼤提⾼系统的性能。
第⼀,通过创建唯⼀性索引,可以保证数据库表中每⼀⾏数据的唯⼀性。
第⼆,可以⼤⼤加快数据的检索速度,这也是创建索引的最主要的原因。
第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性⽅⾯特别有意义。
第四,在使⽤分组和排序⼦句进⾏数据检索时,同样可以显著减少查询中分组和排序的时间。
第五,通过使⽤索引,可以在查询的过程中,使⽤优化隐藏器,提⾼系统的性能。
也许会有⼈要问:增加索引有如此多的优点,为什么不对表中的每⼀个列创建⼀个索引呢?因为,增加索引也有许多不利的⽅⾯。
第⼀,创建索引和维护索引要耗费时间,这种时间随着数据量的增加⽽增加。
第⼆,索引需要占物理空间,除了数据表占数据空间之外,每⼀个索引还要占⼀定的物理空间,如果要建⽴聚簇索引,那么需要的空间就会更⼤。
第三,当对表中的数据进⾏增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维
护速度。
在哪建索引
索引是建⽴在数据库表中的某些列的上⾯。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。⼀般来说,应该在这些列上创建索引:
在经常需要搜索的列上,可以加快搜索的速度;
在作为主键的列上,强制该列的唯⼀性和组织表中数据的排列结构;
在经常⽤在连接的列上,这些列主要是⼀些外键,可以加快连接的速度;在经常需要根据范围进⾏搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利⽤索引的排序,加快排序查询时间;
在经常使⽤在WHERE⼦句中的列上⾯创建索引,加快条件的判断速度。
同样,对于有些列不应该创建索引。⼀般来说,不应该创建索引的的这些列具有下列特点:
第⼀,对于那些在查询中很少使⽤或者参考的列不应该创建索引。这是因为,既然这些列很少使⽤到,因此有索引或者⽆索引,并不能提⾼查询速度。相反,由于增加了索引,反⽽降低了系统的维护速度和增⼤了空间需求。
第⼆,对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如⼈事表的性别列,在查询的结果中,结果集的数据⾏占了表中数据⾏的很⼤⽐例,即需要在表中搜索的数据⾏的⽐例很⼤。增加索引,并不能明显加快检索速度。
第三,对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当⼤,要么取值很少,不利于使⽤索引。
第四,当修改性能远远⼤于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相⽭盾的。当增加索引时,会提⾼检索性能,但是会降低修改性能。当减少索引时,会提⾼修改性能,降低检索性能。因此,当修改操作远远多于检索操作时,不应该创建索引。
数据库优化
此外,除了数据库索引之外,在LAMP结果如此流⾏的今天,数据库(尤其是)性能优化也是海量数据处理的⼀个热点。下⾯就结合⾃⼰的经验,聊⼀聊MySQL数据库优化的⼏个⽅⾯。
⾸先,在数据库设计的时候,要能够充分的利⽤索引带来的性能提升,⾄于如何建⽴索引,建⽴什么样的索引,在哪些字段上建⽴索引,上⾯已经讲的很清楚了,这⾥不在赘述。另外就是设计数据库的原则就是尽可能少的进⾏数据库写操作(插⼊,更新,删除等),查询越简单越好。如下:
数据库设计:
. 创建索引
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论