VFP 学习、开发漫谈 (29)- 索引查询
这一节课与大伙儿聊一聊有关索引查询的话题。一、首先,总结一下索引文件的作用:
1. 提高多记录表的检索速度
对于拥有成千上万条记录的表来说,索引查询肯定要比顺序查询快,而且记录越多,二者的速度差距越明显。
2. 以特定顺序显示表记录
可以按索引表达式值的升序或降序显示记录。
3. 防止字段值重复
在新增或修改记录时,很多用户是通过检测新值是否在表中已存在的方法,来判断记录值是否重复。这种方法在单用户下是可行的,但在多用户下不可行。因为在你检测完成之后、保存数据之前的时间间隔里,其他用户有可能写入新数据。
在多用户环境下,防止字段出现重复值的正确思路是:为表建立主索引或候选索引,将Insert、Replace 或 Update 语句放在一个 TRY...Catch 错误捕获结构里。
下面是一段向员工表添加记录的代码,其中“员工号”建有主索引。
TRY
Insert Into 员工表 (员工号,姓名,部门) Values (nID,cName,cDepart)
CATCH TO oErr WHEN oErr.ErrorNo = 1884
MESSAGEBOX('员工号'+TRAN(nID)+'已经存在!',16,'提示')
ENDTRY
在多表处理中,结合缓冲和事务处理,在出错时执行回滚和还原操作,则代码将更加完善。
在建立主索引或候选索引时,一般应设置过滤条件为 .NOT. DELETE(),否则可能出现这样一种情况:在表中没有查询到拥有某一关键值的记录,但就是无法添加拥有该关键值的记录。造成这种情况的原因是,表中存在拥有该关键值的逻辑删除的记录,所以应该将有删除标记的记录在索引中予以排除。你必须明白:除非在建立索引时指定 FOR !DELETE(),否则,即使设置了SET DELETE ON,逻辑删除的记录仍然会写入索引文件。
当关键字段的数据类型是Integer(AutoInc)时,无需为索引添加 FOR !DELETE(),因为该类型的字段值是不可能重复的,且是只读的。
4. 索引是表之间建立永久关联或临时关联的前提
建立永久关联是为了在表之间建立参照完整性;临时关联可理解为 Set Relation命令,子表记录指针随主表记录的移动而移动。
建立永久关联时,要求一方建立主索引,另一方建立普通索引或其他索引。我们建立的关联绝大部分是一对多关联,因此多数情况下另一方是普通索引。
建立临时关联时,要求相关联的子表必须建立并打开索引,而对主表无要求。
5. 利用唯一性索引代替某些数据字典表
比如,在员工表中有一个“部门”字段,现在想从一个下拉列表中选择部门。通常的思路是再新建一个部门表,并将其设为下拉列表的记录来源。
还有一个思路:不新建部门表,而是为员工表的部门字段建立一个唯一索引(这里的唯一性是指仅对重复字段值的首记录进行索引),将下拉列表的记录来源设定为打开唯一索引的员工表。
将下拉列表的记录来源设定为一个SQL语句也可以,比如:“Select Dist 部门 From 员工表 Into Cursor curTmp Order By 部门”。但是当记录很多时,这种方法刷新较慢,不如设定为带唯一索引的实体表速度快。
一个表可以被多次添加到数据环境中,只是其别名不同,各个“别名表”之间的数据仍保持同步更新,但记录指针、索引、过滤条件互不影响。比如,本例中可以将员工表添加到数据环境两次,第二次添加到数据环境中的别名是“员工表1”,可以指定其 Order 为前面建立的唯一性部门索引,并将其作为下拉列表的记录来源。
二、索引的几个注意事项
1. 索引表达式的限制
VFP 对索引表达式的限制很少,主要有两条:
第一,不能为MEMO、GENERAL型字段建立索引;
第二,在Set Collate To "Machine"状态下,不能对超过240个字符的Character字段建立索引(Character字段最多可容纳254个字符),但在Set Collate To "Pinyin"状态下,无此限制。在Set Collate To "Machine"状态下,可以变通一下,采用 INDEX ON LEFT(字段,240) 建立索引。
2. 索引虽然有很多好处,但也并不是“多多欲善”。你必须明白:在新建或修改记录时,对于结构化复合索引的每个索引标记都是需要同步更新的,索引越多则更新越慢。一般情况下,我们只需对那些查询条件中经常出现的字段,如:“编号”、“日期”等建立索引,不建议对描述性文本字段,如:“公司地址”、“规格型号”等建立索引。
3. 索引文件的类型
大家都知道,索引文件分三类:单入口索引文件、结构化复合索引文件和独立复合索引文件。单入口索引已经过时,不予考虑。我们使用最多的还是结构化复合索引。结构化复合索引随表自动打开和更新,无需人工维护。
在讲述 INDEX 命令时,很多资料上都提到:必须以独占方式打开表。这样的表述并不严谨。严格来说,只有在建立结构化复合索引时,才需要独占打开表,而在建立独立复合索引文件时无此要求,也就是说:可以在共享方式下建立索引。这也是独立索引文件的最大优势。
在多用户环境下,使用独立复合索引应该小心:即使索引已打开,也不会显示其他用户新增的记录;若其他用户更改了关键字,也不会按更改后的关键字重新排序。若要在共享方式下更新独立复合索引,不能使用 REINDEX,应该使用“INDEX ON 表达式 TAG 索引标识 OF 索引文件”重建索引。
总之,独立复合索引文件可作为当前表的一个临时快照使用,仅影响当前用户,但在多用户环境下应该谨慎使用。
三、索引查询技术
如果要查找的字段本身就建有索引,一般用户都能想到直接使用 SEEK()函数或 SEEK 命令。但是对于要查找的字段没有建立索引而相关联的其他字段建立了索引时,很多用户就忽略了索引的使用。
举个例子:工资表的结构是:年月 C(6),工资号 I,性别 C(2),工资 N(10,2),其中,对“年月”字段建立了普通索引。现在要统计2014年5月份男员工的工资总额。
如果不使用索引,可使用如下命令:
Calculate SUM(工资) To nSalary For 年月='201405' AND 性别='男'
或者:Sum 工资 To nSalary For 年月='201405' AND 性别='男'
当数据量很大时,这种方法就比较慢。如果使用索引,速度就快多了,代码如下:
USE 工资表 ORDER 年月
SEEK '201405'
Calculate SUM(工资) To nSalary For 性别='男' WHILE 年月='201405'
上述代码利用索引排序仅搜索了一个月的记录,当然要比搜索整个表快得多。记得我在以前的讲座中对WHILE条件有过专门的论述,其主要特点是:
1. WHILE 条件多用于对索引表的查询统计
2. 当 WHILE 与 FOR 同时出现时,WHILE 条件优先
3. 存在 WHILE 条件时,命令的默认范围是 REST
理解了这3点,就理解了上面的代码,这也是一个 VFP程序员必须掌握的技巧。
实际应用中,下面的结构也很常用:
USE 工资表 ORDER 年月
SEEK '201405'
SCAN FOR 性别='男' WHILE 年月='201405'
……
ENDSCAN
VFP还有一个 INDEXSEEK()函数也很常用。与 SEEK()函数相比,该函数主要有两个特点:
1. 不移动表指针检测字段值是否存在。
大家都知道,若表已设置了记录缓冲,则移动记录指针会导致数据提交,这时,不能使用SEEK(),而只能使用 INDEXSEEK(eExpression,.f.)检测字段值是否存在。
2. 采用 INDEXSEEK(eExpression,.t.)的形式,在找到记录时将指针移动到该记录,这一点与 SEEK()函数的功能相同,但是,未找到记录时,则不移动指针,指针仍保持在原记录。
举例如下:在按关键字“材料代码”定位记录时,原来的代码如下:
程序代码:
cMatid = ALLT(THISFORM.txtSearch.Value) nSelect = SELECT() nRec = IIF(EOF(),0,RECN()) SELECT 材料表 IF SEEK(cMatid,'材料表','材料代码') THISFORM.Grid1.SetFocus ELSE IF nRec # 0 GO nRec ENDIF MESSAGEBOX('未找到编码为“'+cMatid+'”的材料记录!',48,'提示') ENDIF SELECT (nSelect)
现在改用 INDEXSEEK()函数,实现相同的功能而代码却精简了许多:
程序代码:
cMatid = ALLT(THISFORM.txtSearch.Value) IF INDEXSEEK(cMatid,.t.,'材料表','材料代码') THISFORM.Grid1.SetFocus ELSE MESSAGEBOX('未找到编码为“'+cMatid+'”的材料记录!',48,'提示') ENDIF
VFP 还有一个函数 LOOKUP(),它将记录查找与返回字段值合二为一,也很常用。比如:在员工表中查找编号为“880287”的员工姓名。如果不使用 LOOKUP()函数,代码为:
SELECT 员工表
SEEK "880287" ORDER 员工号
? 姓名
而使用 LOOKUP()函数,代码可改写为:
SELECT 员工表
? LOOKUP(姓名,"880287",员工号,"员工号")
实际应用中,应注意以下三个问题:
1. 第一个参数(表示要返回的字段,本例是“姓名”)和第三个参数(表示要查找的字段,本例是“员工号”)没有字符串定界符。
2. 第二个参数(表示要查找的值,本例是"880287")的数据类型要与第三个参数提供的搜索字段的类型相匹配。
3. 若省略最后一个参数(第4个参数,表示索引标识,本例是"员工号"),则是按顺序查找,相当于执行 Locate For命令,即:Locate For <参数3> = <参数2>;否则,是按索引查找,相当于执行 Seek命令,即:Seek <参数2> Order <参数4>。
LOOKUP() 函数有两点不足:
1. 不支持别名参数,只能对当前工作区打开的表进行操作,否则,示例中的“SELECT 员工表”也可以省略了。
2. 该函数会移动记录指针。因为该函数已经返回了所要的结果,因此,不移动记录指针更符合常规需求。
针对上述问题,我编写了一个新的 NewLookUp()函数,代码如下:
该函数不改变当前工作区、不改变目标表的记录指针。
应用示例:返回员工号为“880287”的员工姓名。
用法一:顺序查找
? NewLookUp('姓名','880287','员工号','员工表')
用法二:索引查找
? NewLookUp('姓名','880287','员工号','员工表','员工号')
[ 本帖最后由 liuxingang28 于 2014-9-29 14:19 编辑 ]