清除过滤器
文章
姚 鑫 · 三月 7, 2021
# 第五章 SQL定义表(二)
# 主键
InterSystems IRIS提供了两种方法来唯一标识表中的行:`RowID`和主键。
可选的主键是一个有意义的值,应用程序可以使用该值唯一地标识表中的行(例如,联接中的行)。主键可以是用户指定的数据字段,也可以是多个数据字段的组合。主键值必须是唯一的,但不必是整数值。 `RowID`是一个内部用于标识表中行的整数值。通常,主键是由应用程序生成的值,而`RowID`是由InterSystems IRIS生成的唯一整数值。
系统会自动创建一个主map,以使用`RowID`字段访问数据行。如果定义主键字段,系统将自动创建并维护主键索引。
显然,具有两个不同的字段和索引来标识行的双重性不一定是一件好事。可以通过以下两种方式之一解析为单个行标识符和索引:
- 使用应用程序生成的主键值作为`IDKEY`。
可以通过使用关键字`PrimaryKey`和`IdKey`在类定义中标识主键索引来实现这一点(如果为此目的设置了`PKey is IdKey`标志,也可以在DDL中实现这一点)。
这使得主键索引成为表的主映射。
因此,主键将被用作行的主要内部地址。
如果主键包含多个字段,或者主键值不是整数,那么这种方法的效率会较低。
- 不要使用应用程序生成的主键值,而应在应用程序中使用系统生成的`RowID`整数作为应用程序使用的主键(例如,在`joins`中)。这样做的好处是,整数`RowID`有助于进行更有效的处理,包括使用位图索引。
根据应用程序的性质,可能希望解析为单个行标识符和索引,或者为应用程序生成的主键和系统生成的`RowID`具有单独的索引。
# RowVersion,AutoIncrement和串行计数器字段
InterSystems SQL支持三种专用数据类型,用于自动增加计数器值。这三种数据类型都是扩展`%Library.BigInt`数据类型类的子类。
- `%Library.RowVersion`:计算在命名空间范围内所有`RowVersion`表的插入和更新。只有在包含`ROWVERSION`字段的表中进行插入和更新时,此计数器才会递增。 `ROWVERSION`值是唯一的且不可修改。此名称空间范围的计数器永远不会重置。
- `%Library.Counter`(也称为SERIAL计数器字段):对表中的插入进行计数。默认情况下,此字段接收一个自动递增的整数。但是,用户可以为此字段指定一个非零的整数值。用户可以指定重复值。如果用户提供的值大于系统提供的最高值,则将自动递增计数器设置为从用户指定的值开始递增。
- `%Library.AutoIncrement`:计数插入到表中的次数。默认情况下,此字段接收一个自动递增的整数。但是,用户可以为此字段指定一个非零的整数值。用户可以指定重复值。指定用户值对自动增量计数器无效。
这三个字段以及`IDENTITY`字段均返回`AUTO_INCREMENT = YES`,如以下示例所示:
```java
SELECT COLUMN_NAME,AUTO_INCREMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MyTable'
```
## RowVersion Field
`RowVersion`字段是一个可选的用户定义字段,它提供行级版本控制,使可以确定对每个命名空间范围内的行中的数据进行更改的顺序。 InterSystems IRIS维护一个整个命名空间范围的计数器,并在每次修改行数据(插入,更新或`%Save`)时向该字段分配一个唯一的增量正整数。因为此计数器是整个名称空间范围的,所以对具有`ROWVERSION`字段的一个表进行的操作将设置`ROWVERSION`计数器的增量点,该值将用于同一名称空间中具有`ROWVERSION`字段的所有其他表。
通过指定数据类型为`ROWVERSION(%Library.RowVersion)`的字段来创建`RowVersion`字段。每个表只能指定一个`ROWVERSION`数据类型字段。尝试创建具有多个`ROWVERSION`字段的表会导致5320编译错误。
该字段可以具有任何名称,并且可以出现在任何列位置。 `ROWVERSION(%Library.RowVersion)`数据类型映射到`BIGINT(%Library.BigInt)`。
此字段从自动递增计数器接收一个从1开始的正整数。只要通过插入,更新或`%Save`操作修改了任何启用`ROWVERSION`的表中的数据,此计数器就会递增。递增的值记录在已插入或更新的行的`ROWVERSION`字段中。
名称空间可以包含具有`RowVersion`字段的表和不具有该字段的表。仅对具有`RowVersion`字段的表的数据更改会增加整个命名空间范围的计数器。
当用数据填充表时,InterSystems IRIS会为每个插入的行将此字段分配连续的整数。如果使用`ALTER TABLE`将`ROWVERSION`字段添加到已经包含数据的表中,则该字段将被创建为`NULL`以用于预先存在的字段。对该表的任何后续插入或更新都会为该行的`RowVersion`字段分配一个顺序整数。该字段是只读的;尝试修改`RowVersion`值会生成`SQLCODE -138`错误:无法为只读字段插入/更新值。因此,`RowVersion`字段被定义为唯一且不可修改,但不是必需字段或非`null`。
`RowVersion`值始终递增。它们不被重用。因此,插入和更新按时间顺序分配唯一的`RowVersion`值。删除操作从该序列中删除数字。因此,`RowVersion`值可能在数字上不连续。
此计数器永远不会重置。删除所有表数据不会重置`RowVersion`计数器。即使删除名称空间中包含`ROWVERSION`字段的所有表,也不会重置此计数器。
`RowVersion`字段不应包含在唯一键或主键中。 `RowVersion`字段不能是`IDKey`索引的一部分。
分片表不能包含`RowVersion`字段。
`RowVersion`字段未隐藏(通过`SELECT *`显示)。
在同一名称空间中的三个表的以下示例中显示了这一点。
1. 创建`表1`和`表3`,每个都有一个`ROWVERSION`字段,并创建表2没有一个`ROWVERSION`字段。
2. 在`Table1`中插入十行。这些行的`ROWVERSION`值是接下来的十个计数器增量。由于以前未使用过计数器,因此它们是1到10。
3. 在`Table2`中插入十行。由于`Table2`没有`ROWVERSION`字段,因此计数器不会增加。
4. 更新`表1`的行。该行的`ROWVERSION`值将更改为下一个计数器增量(在这种情况下为11)。
5. 在`Table3`中插入十行。这些行的`ROWVERSION`值是接下来的十个计数器增量(12到21)。
6. 更新`表1`的行。该行的`ROWVERSION`值更改为下一个计数器增量(在这种情况下为22)。
7. 删除`表1`的行。 `ROWVERSION`计数器不变。
8. 更新`Table3`的一行。该行的`ROWVERSION`值将更改为下一个计数器增量(在这种情况下为23)。
## Serial Counter Field
可以使用`SERIAL`数据类型(在持久性类表定义中为`%Library.Counter`)来指定一个或多个可选的整数计数器字段,以记录在表中插入记录的顺序。每个串行计数器字段都维护自己的独立计数器。
每当将一行插入表中时,串行计数器字段都会从其自动增量计数器接收一个正整数,该行没有提供任何值(`NULL`)或值为0。但是,用户可以指定非零整数值插入期间针对此字段的值,将覆盖表计数器的默认值。
- 如果`INSERT`没有为计数器字段指定非零整数值,则计数器字段将自动接收正整数计数器值。计数从1开始。每个连续值都是从为此字段分配的最高计数器值开始的1增量。
- 如果`INSERT`为`counter`字段指定了一个非零的整数值,则该字段将接收该值。它可以是正整数或负整数,可以低于或高于当前计数器值,并且可以是已经分配给该字段的整数。如果该值大于任何分配的计数器值,它将自动增量计数器的增量起始点设置为该值。
尝试更新计数器字段值会导致`SQLCODE -105`错误。
`TRUNCATE TABLE`命令将该计数器重置为1。即使使用`DELETE`命令删除表中的所有行,也不会通过DELETE命令将其重置。
分片表不能包含串行计数器字段。
## AutoIncrement Field
可以使用`%Library.AutoIncrement`数据类型(或`BIGINT AUTO_INCREMENT`)来指定一个整数计数器字段,以记录在表中插入记录的顺序。每个表只能指定一个`%AutoIncrement`数据类型字段。每当将一行插入表中时,此字段都会从自动增量计数器接收一个正整数,该行没有提供任何值(`NULL`)或值为0。但是,用户可以为此指定非零整数值插入过程中的字段,将覆盖表计数器的默认值。
- 如果`INSERT`没有为计数器字段指定非零整数值,则计数器字段将自动接收正整数计数器值。计数从1开始。每个连续值都是从为此字段分配的最高计数器值开始的1增量。
- 如果`INSERT`为`counter`字段指定了一个非零的整数值,则该字段将接收该值。它可以是正整数或负整数,可以低于或高于当前计数器值,并且可以是已经分配给该字段的整数。用户分配的值对自动增量计数器无效。
尝试更新计数器字段值会导致`SQLCODE -105`错误。
`TRUNCATE TABLE`命令将该计数器重置为1。即使使用`DELETE`命令删除表中的所有行,也不会通过`DELETE`命令将其重置。
分片表可以包含一个`AutoIncrement`字段。
# 通过创建持久性类来定义表
在InterSystems IRIS中定义表的主要方法是使用Studio创建持久性类定义。当这些类在InterSystems IRIS数据库中保存并编译时,它们会自动投影到与类定义相对应的关系表中:每个类代表一个表;每个类代表一个表。每个属性代表一列,依此类推。可为一个类(表)定义的属性(列)的**最大数量为1000**。
例如,以下定义了持久类`MyApp.Person`:
```java
Class MyApp.Person Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property SSN As %String(MAXLEN=15) [InitialExpression = "Unknown"];
Property DateOfBirth As %Date;
Property Sex As %String(MAXLEN=1);
}
```
编译后,这将在MyApp模式中创建`MyApp.Person`持久类和相应的SQL表`Person`。
在此示例中,指定了程序包名称`MyApp`。定义持久类时,未指定的程序包名称默认为`User`。这对应于默认的SQL模式名称`SQLUser`。例如,将名为`“Students”`的表定义为持久类将创建类`User.Students`,以及相应的`SQL schema.table`名称`SQLUser.Students`。
在此示例中,持久类名称`Person`是默认的SQL表名称。可以使用`SqlTableName`类关键字来提供其他SQL表名称。
可以使用`DDL CREATE TABLE`语句(指定`SQL schema.table`名称)定义相同的`MyApp.Person`表。成功执行此SQL语句会生成一个相应的持久性类,其包名称为MyApp,类名称为Person:
```java
CREATE TABLE MyApp.Person (
Name VARCHAR(50) NOT NULL,
SSN VARCHAR(15) DEFAULT 'Unknown',
DateOfBirth DATE,
Sex VARCHAR(1)
)
```
`CREATE TABLE`在相应的类定义中未指定显式的`StorageStrategy`。相反,它将采用已定义的默认存储策略。
默认情况下,`CREATE TABLE`在相应的类定义中指定`Final class`关键字,指示它不能具有子类。
请注意,诸如上图所示的持久性类定义在编译时会创建相应的表,但是无法使用SQL DDL命令(或通过使用Management Portal Drop操作)来修改或删除此表定义,这会向显示消息“未为类'schema.name'启用DDL ...”)。必须在表类定义中指定[DdlAllowed]才能进行以下操作:
```java
Class MyApp.Person Extends %Persistent [DdlAllowed]
```
可以在类定义中指定`%Populate`以启用使用测试数据自动填充表。
```java
Class MyApp.Person Extends (%Persistent,%Populate) [DdlAllowed]
```
这为该类提供了`Populate()`方法。运行此方法将在表中填充十行测试数据。
## 定义数据值参数
每个属性(字段)定义都必须指定一个数据类型类,该类指定该属性所基于的类。指定的数据类型将字段的允许数据值限制为该数据类型。定义投影到表的持久类时,必须使用`%Library`包中的类指定此数据类型。可以将此类指定为`%Library.Datatype`或`%Datatype`。
许多数据类型类提供的参数使可以进一步定义允许的数据值。这些参数特定于单个数据类型。以下是一些较常见的数据定义参数:
- 数据值物理限制
- 允许的数据值:枚举或模式匹配
- 通过定义唯一索引来唯一数据值
- 通过定义`SqlComputeCode`计算数据值
### 数据值限制
**对于数字数据类型,可以指定`MAXVAL`和`MINVAL`参数以限制允许值的范围。根据定义,数字数据类型具有最大支持值(正数和负数)。可以使用`MAXVAL`和`MINVAL`进一步限制允许的范围。**
对于字符串数据类型,可以指定`MAXLEN`和`MINLEN`参数以限制允许的长度(以字符为单位)。根据定义,字符串数据类型具有最大支持的长度。可以使用`MAXLEN`和`MINLEN`进一步限制允许的范围。默认情况下,超过`MAXLEN`的数据值会生成字段验证错误:`INSERT`的`SQLCODE -104`或`UPDATE`的`SQLCODE -105`。可以指定`TRUNCATE = 1`以允许超过`MAXLEN`的字符串数据值。指定的字符串将被截断为`MAXLEN`长度。
### 允许的数据值
可以通过两种方式限制实际数据值:
- **允许值的列表(带有`VALUELIST`和`DISPLAYLIST`的枚举值)。**
- **允许值的匹配模式(`PATTERN`)。**
#### 枚举值
通过将表定义为持久类,可以定义仅包含某些指定值的属性(字段)。这是通过指定`VALUELIST`参数来完成的。 `VALUELIST`(指定逻辑存储值的列表)通常与`DISPLAYLIST`(指定相应的显示值的列表)一起使用。这两个列表都以列表定界符开头。几种数据类型可以指定`VALUELIST`和`DISPLAYLIST`。下面的示例定义两个带有枚举值的属性:
```java
Class Sample.Students Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property ChoiceStr As %String(VALUELIST=",0,1,2",DISPLAYLIST=",NO,YES,MAYBE");
Property ChoiceODBCStr As %EnumString(VALUELIST=",0,1,2",DISPLAYLIST=",NO,YES,MAYBE");
}
```
如果指定了`VALUELIST`,则`INSERT`或`UPDATE`只能指定`VALUELIST`中列出的值之一,或者不提供值(`NULL`)。 `VALUELIST`有效值区分大小写。指定与`VALUELIST`值不匹配的数据值会导致字段值验证失败:`INSERT`的`SQLCODE -104`或`UPDATE`的`SQLCODE -105`。
在ODBC模式下显示时,`%String`和`%EnumString`数据类型的行为不同。使用上面的示例,当以逻辑模式显示时,`ChoiceStr`和`ChoiceODBCStr`都显示其`VALUELIST`值。在“显示”模式下显示时,`ChoiceStr`和`ChoiceODBCStr`均显示其DISPLAYLIST值。当以ODBC模式显示时,`ChoiceStr`显示`VALUELIST`值;否则显示`VALUELIST`值。 `ChoiceODBCStr`显示`DISPLAYLIST`值。
#### 值的模式匹配
几种数据类型可以指定`PATTERN`参数。 `PATTERN`将允许的数据值限制为与指定的ObjectScript模式匹配的数据值,指定为带引号的字符串,省略前导问号。以下示例使用模式定义属性:
```java
Class Sample.Students Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property Telephone As %String(PATTERN = "3N1""-""3N1""-""4N");
}
```
由于将模式指定为带引号的字符串,因此模式中指定的文字必须将其双引号引起来。请注意,模式匹配是在`MAXLEN`和`TRUNCATE`之前应用的。因此,如果为可能超过`MAXLEN`并被截断的字符串指定了一个模式,则可能希望以“ `.E`”(任何类型的尾随字符数不限)结束该模式。
与`PATTERN`不匹配的数据值会生成字段验证错误:`INSERT`的`SQLCODE -104`或`UPDATE`的`SQLCODE -105`。
### 唯一值
`CREATE TABLE`允许将字段定义为`UNIQUE`。这意味着每个字段值都是唯一(非重复)值。
将表定义为持久类不支持相应的`uniqueness`属性关键字。相反,必须同时定义属性和该属性的唯一索引。下面的示例为每个记录提供唯一的Num值:
```java
Class Sample.CaveDwellers Extends %Persistent [ DdlAllowed ]
{
Property Num As %Integer;
Property Troglodyte As %String(MAXLEN=50);
Index UniqueNumIdx On Num [ Type=index,Unique ];
}
```
索引名称遵循属性的命名约定。可选的`Type`关键字指定索引类型。 `Unique`关键字将属性(字段)定义为唯一。
使用`INSERT`或`UPDATE`语句时,必须具有唯一的值字段。
### 计算值
下面的类定义示例定义一个表,该表包含一个字段(生日),该字段在最初设置`DateOfBirth`字段值时使用`SqlComputed`来计算其值,而在更新`DateOfBirth`字段值时使用`SqlComputeOnChange`来重新计算其值。 `Birthday`字段值包括当前时间戳,以记录该字段值的计算/重新计算时间:
```java
Class Sample.MyStudents Extends %Persistent [DdlAllowed]
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property Birthday As %String
[ SqlComputeCode = {SET {Birthday}=$PIECE($ZDATE({DateOfBirth},9),",")_
" changed: "_$ZTIMESTAMP},
SqlComputed, SqlComputeOnChange = DateOfBirth ];
}
```java
请注意,对`DateOfBirth`的`UPDATE`指定现有的`DateOfBirth`值不会重新计算`Birthday`字段值。
## 嵌入式对象(%SerialObject)
可以通过引用定义属性的嵌入式串行对象类来简化持久表的结构。例如,希望`MyData.Person`包含地址信息,包括街道,城市,州和邮政编码。可以定义一个定义这些属性的串行对象(`%SerialObject`)类,而不是在`MyData.Person`中指定这些属性,然后在`MyData.Person`中指定一个引用该嵌入式对象的`Home`属性。在以下类定义中显示了这一点:
```java
Class MyData.Person Extends (%Persistent) [ DdlAllowed ]
{ Property Name As %String(MAXLEN=50);
Property Home As MyData.Address;
Property Age As %Integer;
}
```
```java
Class MyData.Address Extends (%SerialObject)
{ Property Street As %String;
Property City As %String;
Property State As %String;
Property PostalCode As %String;
}
```
不能直接访问串行对象属性中的数据,必须通过引用它的持久类/表访问它们:
- 要从持久性表中引用单个串行对象属性,请使用下划线。例如,`SELECT名称Home_State FROM MyData.Person`返回状态串行对象属性值作为字符串。串行对象属性值以查询中指定的顺序返回。
- 要引用持久性表中的所有串行对象属性,请指定引用字段。例如,`SELECT Home FROM MyData.Person`以`%List`结构形式返回所有`MyData.Address`属性的值。串行对象属性值以串行对象中指定的顺序返回:`Home_Street,Home_City,Home_State,Home_PostalCode。在Management Portal SQL`界面“目录详细信息”中,此引用字段称为“容器”字段。这是一个Hidden字段,因此`SELECT *`语法不返回。
- 持久类的`SELECT *`单独返回所有串行对象属性,包括嵌套的串行对象。例如,`SELECT * FROM MyData.Person`返回`Age,Name,Home_City,Home_PostalCode,Home_State和Home_Street`值(按此顺序);它不返回`Home%List`结构值。串行对象属性值以排序顺序返回。 `SELECT *`首先按排序顺序(通常按字母顺序)列出持久性类中的所有字段,然后按排序顺序列出嵌套的串行对象属性。
请注意,嵌入式串行对象不必与引用它的持久性表位于同一程序包中。
定义嵌入式对象可以简化持久性表定义:
- 持久表可以包含多个属性,这些属性引用同一嵌入式对象中的不同记录。例如,`MyData.Person`表可以包含`Home`和`Office`属性,这两个属性均引用`MyData.Address`串行对象类。
- 多个持久表可以引用同一嵌入式对象的实例。例如,`MyData.Person`表的`Home`属性和`MyData.Employee WorkPlace`属性都可以引用`MyData.Address`串行对象类。
- 一个嵌入式对象可以引用另一个嵌入式对象。例如,`MyData.Address`嵌入式对象包含引用`MyData.Telephone`嵌入式对象的`Phone`属性,其中包含`CountryCode`,`AreaCode`和`PhoneNum`属性。在持久类中,使用多个下划线来引用嵌套的串行对象属性,例如`Home_Phone_AreaCode`。
编译串行对象类会在存储定义中生成数据规范。编译器通过在串行对象类名称后附加单词`“State”`来为该规范分配数据名称。因此,为`MyData.Address`分配了``。如果此名称(在此示例中为`AddressState`)已经用作属性名称,则编译器将附加一个整数以创建唯一的数据名称:``。
## 类方法
可以将类方法指定为表定义的一部分,如以下示例所示:
```java
Class MyApp.Person Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property SSN As %String(MAXLEN=15) [InitialExpression = "Unknown"];
Property DateOfBirth As %Date;
Property Sex As %String(MAXLEN=1);
ClassMethod Numbers() As %Integer [ SqlName = Numbers, SqlProc ]
{
QUIT 123
}
}
```
在SELECT查询中,可以按以下方式调用此方法:
```java
SELECT Name,SSN,Sample.Numbers() FROM Sample.Person
```

## 通过创建持久性类来定义分片表
必须先建立分片环境,然后才能定义作为分片表投影的持久性类。
要将持久性类定义为分片,请指定类关键字`Sharded = 1`。 (类关键字`Sharded = 2`保留供生成的类内部使用。)
**注意:请勿尝试设置或更改现有类定义的与分片相关的类属性。仅应为不包含数据的新表指定这些属性。这包括设置`Sharded`类关键字和与分片相关的索引关键字。尝试编辑现有类的任何与分片相关的属性都可能导致数据无法访问。**
下例显示了`Sharded = 1`持久类的类定义:
```java
Class Sample.MyShardT Extends %Persistent [ ClassType = persistent, DdlAllowed, Final, Sharded = 1]
{
...
}
```
如果将一个类定义为分片,则它必须是持久性的`ClassType`。如果未将分片类定义为`ClassType`持久类,则在类编译期间将返回错误,例如:`ERROR#5599:分片类'Sample.Address'必须为ClassType'persistent',而不是ClassType'serial'`。分片类使用的存储类必须为`%Storage.Persistent`或其子类`%Storage.Shard`。如果分片类的存储类不是`%Storage.Persistent`,则在类编译期间将返回以下错误:`错误#5598:分片类'Sample.Vendor'必须使用存储类型%Storage.Persistent,而不是存储类型' %Storage.SQL”`。
定义分片类时,应定义参数`DEFAULTCONCURRENCY = 0`。
然后,可以定义`ShardKey`索引。
创建分片表时,将自动生成抽象的分片键索引。分片键索引的目的是用作确定行所在的分片的键。
### 分片类方法
分片类(`Sharded = 1`)支持`%Library.Persistent`方法`%Open(),%OpenId(),%Save(),%Delete()和%DeleteId()`具有以下限制:并发`concurrency参数`被忽略;删除将始终使用`并发concurrency= 0`,而不管用户提供的并发值如何。完全支持回调方法`%OnDelete(),%OnAfterDelete(),%OnOpen(),%OnBeforeSave()和%OnAfterSave()`。这些回调方法在分片主机上执行,而不是在分片服务器上执行。分片本地类(`Sharded = 2`)不支持这些方法。
分片类(`Sharded = 1`)不支持`%Library.Persistent`方法`%LockExtent()`和`%UnlockExtent()`。定义并发参数的对象方法中的所有并发参数都要求值`concurrency = 0`;否则,值为0。可以通过设置`DEFAULTCONCURRENCY = 0`来建立默认值
### 分片类限制
- 分片类不支持的类参数:`CONNECTION`,`DEFAULTGLOBAL`,`DSINTERVAL`,`DSTIME`,`IDENTIFIEDBY`,`OBJJOURNAL`。
- 分片类不支持的类关键字:`language`,`ViewQuery`。
- 分片类不支持的超级类:`%Library.IndexBuilder`,`%DocDB.Document`。
- 分片类不支持的属性数据类型:`%Library.Text`。
- 分片类不支持关系属性。
- 分片类不支持投影。
- 分片类不支持功能索引(无法定义索引`TypeClass`)。
- 分片类不支持使用除`“对象”`以外的语言的任何方法。
- 分片类不支持任何非`%SQLQuery`类型的类查询。
尝试使用任何这些功能来编译分片类都会导致编译时错误。
文章
Jingwei Wang · 一月 19, 2023
什么是网页抓取:
简单来说,网络抓取、网络收获或网络数据提取是从网站收集大数据(非结构化)的自动化过程。用户可以根据需要提取特定站点上的所有数据或特定数据。收集的数据可以以结构化格式存储以供进一步分析。
网页抓取涉及的步骤:
找到您要抓取的网页的 URL
通过检查选择特定元素
编写代码获取被选元素的内容
以需要的格式存储数据
就这么简单!
用于网络抓取的流行库/工具是:
Selenium – 用于测试 Web 应用程序的框架
BeautifulSoup – 用于从 HTML、XML 和其他标记语言中获取数据的 Python 库
Pandas – 用于数据操作和分析的 Python 库
什么是Beautiful Soup?
Beautiful Soup 是一个纯 Python 库,用于从网站中提取结构化数据。它允许您解析来自 HTML 和 XML 文件的数据。它充当辅助模块,并以与使用其他可用开发人员工具以网页交互的方式与 HTML 交互。
它通常可以为程序员节省数小时或数天的工作时间,因为它可以与您最喜欢的解析器(如 lxml 和 html5lib)一起使用,以提供 Python organic方式来导航、搜索、并修改解析树。
beautiful soup 的另一个强大而有用的功能是它可以智能地将正在获取的文档转换为 Unicode 并将传出文档转换为 UTF-8。作为一名开发人员,您不必关心这一点,除非文档内在未指定编码或 Beautiful Soup 无法检测到编码。
与其他一般解析或抓取技术相比,它也被认为更快。
在今天的文章中,我们将使用带有对象脚本的嵌入式 Python 来抓取 ae.indeed.com 上的 Python 职位空缺和公司
第 1 步 -找到您要抓取的网页的 URL。
url = https://ae.indeed.com/jobs?q=python&l=Dubai&start=0
我们要从中抓取数据的网页看起来像这样
为了简单和学习目的,我们将提取“职位”和“公司”,输出将类似于下面的屏幕截图。
我们将使用两个 python 库。
requests :Requests 是 Python 编程语言的 HTTP 库。该项目的目标是使 HTTP 请求更简单、更人性化。
bs4 for BeautifulSoup :BeautifulSoup 是一个用于解析 HTML 和 XML 文档的 Python 包。它为已解析的页面创建一个解析树,可用于从 HTML 中提取数据,这对于网络抓取很有用。
让我们安装这个 python 包 (windows)
irispip install --target C:\InterSystems\IRISHealth\mgr\python bs4 irispip install --target C:\InterSystems\IRISHealth\mgr\python requests
让我们将 python 库导入到 ObjectScript
Class PythonTesting.WebScraper Extends %Persistent { // pUrl = https://ae.indeed.com/jobs?q=python&l=Dubai&start= // pPage = 0 ClassMethod ScrapeWebPage(pUrl, pPage) { // imports the requests python library set requests = ##class ( %SYS.Python ).Import( "requests" ) // import the bs4 python library set soup = ##class ( %SYS.Python ).Import( "bs4" ) // import builtins package which contains all of the built-in identifiers set builtins = ##class ( %SYS.Python ).Import( "builtins" ) }
让我们使用请求收集 html 数据;注意:我们从谷歌搜索“my user agent”中获取的用户代理url为“https://ae.indeed.com/jobs?q=python&l=Dubai&start=”,pPage为页码
我们将使用 requests 对 URL 执行 http get 请求并将响应存储在“req”
set headers = { "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" } set url = "https://ae.indeed.com/jobs?q=python&l=Dubai&start=" _pPage set req = requests.get(url, "headers=" _headers)
req 对象将具有从网页返回的 html。
让我们通过 BeautifulSoup html 解析器运行它,以便我们可以提取作业数据。
set soupData = soup.BeautifulSoup(req.content, "html.parser" ) set title = soupData.title.text W !,title
标题看起来如下
第二步:通过检查选择需要的元素。
在这种情况下,我们对通常位于 <div> 标记中的作业列表感兴趣,在您的浏览器中,您可以检查该元素以找到 div 类。在我们的例子中,所需的信息存储在 <div class="cardOutline tapItem ... </div>
第三步:编写获取选中元素内容的代码
我们将使用 BeautifulSoup 上的 find_all 功能来查找所有包含类名“cardOutline”的 <div> 标签
//parameters to python would be sent as a python dictionary set divClass = { "class" : "cardOutline" } set divsArr = soupData. "find_all" ( "div" ,divClass...)
这将返回一个列表,我们可以遍历该列表并提取职位和公司
第 4 步:以所需格式存储/显示数据。
在下面的示例中,我们将数据写入终端。
set len = builtins.len(divsArr) W !, "Job Title" , $C ( 9 )_ " --- " _ $C ( 9 ), "Company" for i = 1 : 1 :len { Set item = divsArr. "__getitem__" (i - 1 ) set title = $ZSTRIP (item.find( "a" ).text, "<>W" ) set companyClass = { "class_" : "companyName" } set company = $ZSTRIP (item.find( "span" , companyClass...).text, "<>W" ) W !,title, $C ( 9 ), " --- " , $C ( 9 ),company }
请注意,我们正在使用 builtins.len() 来获取 divsArr 列表的长度
标识符名称:命名标识符的规则在 ObjectScript 和 Python 之间是不同的。例如,Python 方法名中允许使用下划线 (_),实际上广泛用于所谓的“dunder”方法和属性(“dunder”是“双下划线”的缩写),例如 __getitem__ 或 __class__ .要从 ObjectScript 使用此类标识符,请将它们括在双引号中:关于标识符名称的系统间文档
类方法示例
类方法 ScrapeWebPage(pUrl, pPage)
// pUrl = https://ae.indeed.com/jobs?q=python&l=Dubai&start= // pPage = 0 ClassMethod ScrapeWebPage(pUrl, pPage) { set requests = ##class ( %SYS.Python ).Import( "requests" ) set soup = ##class ( %SYS.Python ).Import( "bs4" ) set builtins = ##class ( %SYS.Python ).Builtins() set headers = { "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" } set url = pUrl_pPage set req = requests.get(url, "headers=" _headers) set soupData = soup.BeautifulSoup(req.content, "html.parser" ) set title = soupData.title.text W !,title set divClass = { "class_" : "cardOutline" } set divsArr = soupData. "find_all" ( "div" ,divClass...) set len = builtins.len(divsArr) W !, "Job Title" , $C ( 9 )_ " --- " _ $C ( 9 ), "Company" for i = 1 : 1 :len { Set item = divsArr. "__getitem__" (i - 1 ) set title = $ZSTRIP (item.find( "a" ).text, "<>W" ) set companyClass = { "class_" : "companyName" } set company = $ZSTRIP (item.find( "span" , companyClass...).text, "<>W" ) W !,title, $C ( 9 ), " --- " , $C ( 9 ),company } }
下一步..
使用对象脚本和嵌入式 python 以及几行代码;我们可以很容易地从我们最喜欢的工作网站上抓取数据,收集工作名称、公司、薪水、工作描述和电子邮件/链接。例如,如果您有多个页面,您可以使用页面轻松遍历它们可以将此数据添加到 pandas 数据框并删除重复项,可以根据您感兴趣的特定关键字应用过滤器。通过 numpy 运行此数据,并获得一些线图或者对数据执行 One-Hot 编码,并创建/训练您的 ML 模型,如果有您感兴趣的特定职位空缺,请向自己发送通知。 😉快乐编码!
别忘了按赞按钮😃
文章
Michael Lei · 九月 18, 2023
如今,关于大语言模型、人工智能等的消息不绝于耳。向量数据库是其中的一部分,并且已经有非IRIS的技术实现了向量数据库。
为什么是向量?
相似性搜索:向量可以进行高效的相似性搜索,例如在数据集中查找最相似的项目或文档。传统的关系数据库是为精确匹配搜索而设计的,不适合图像或文本相似性搜索等任务。
灵活性:向量表示形式用途广泛,可以从各种数据类型派生,例如文本(通过 Word2Vec、BERT 等嵌入)、图像(通过深度学习模型)等。
跨模态搜索:向量可以跨不同数据模态进行搜索。例如,给定图像的向量表示,人们可以在多模式数据库中搜索相似的图像或相关文本。
还有许多其他原因。
因此,对于这次 pyhon 竞赛,我决定尝试实现这种支持。不幸的是我没能及时完成它,下面我将解释原因。
有几件重要的事情必须完成,才能使其充实
使用 SQL 接受并存储向量化数据,简单的示例(本例中的 3 是维度数量,每个字段都是固定的,并且该字段中的所有向量都必须具有精确的维度)
create table items(embedding vector( 3 )); insert into items (embedding) values ( '[1,2,3]' ); insert into items (embedding) values ( '[4,5,6]' );
相似度函数,相似度有不同的算法,适合对少量数据进行简单搜索,不使用索引
-- Euclidean distance select embedding, vector.l2_distance(embedding, '[9,8,7]' ) distance from items order by distance; -- Cosine similarity select embedding, vector.cosine_distance(embedding, '[9,8,7]' ) distance from items order by distance; -- Inner product select embedding, -vector.inner_product(embedding, '[9,8,7]' ) distance from items order by distance;
自定义索引,有助于更快地搜索大量数据,索引可以使用不同的算法,并使用与上面不同的距离函数,以及其他一些选项
新南威尔士州
倒排文件索引
搜索将使用创建的索引,其算法将找到所请求的信息。
插入向量
该向量应该是一个数值数组,可以是整数或浮点数,也可以是有符号的或无符号的。在IRIS中我们可以将其存储为$listbuild,它具有良好的表示性,已经支持,只需要实现从ODBC到逻辑的转换。
然后,可以使用外部驱动程序(例如 ODBC/JDBC)或使用 ObjectScript 从 IRIS 内部以纯文本形式插入值
普通 SQL
insert into items (embedding) values ( '[1,2,3]' );
来自ObjectScript
set rs = ##class ( %SQL.Statement ). %ExecDirect (, "insert into test.items (embedding) values ('[1,2,3]')" ) set rs = ##class ( %SQL.Statement ). %ExecDirect (, "insert into test.items (embedding) values (?)" , $listbuild ( 2 , 3 , 4 ))
或者嵌入式 SQL
&sql( insert into test.items (embedding ) values ('[ 1 , 2 , 3 ]')) set val = $listbuild ( 2 , 3 , 4 ) &sql( insert into test.items (embedding ) values (:val))
它将始终存储为 $lb(),并在 ODBC 中以文本格式返回
意外行为
在使用 DBeaver 进行测试期间,我发现连接后的第一行插入正确,但所有其他行均按原样插入,没有任何验证或转换。
然后我发现,JDBC默认使用快速插入,在这种情况下,它将插入的数据直接存储到全局变量中,所以我必须手动将其关闭
在 DBeaver 中,在 FeatureOption 字段中选择 optfastSelect
计算
我们需要向量来支持两个向量之间距离的计算
为了这次比赛,我需要使用嵌入式Python,这就带来了一个问题,如何在嵌入式Python中操作$lb。 %SYS.Class中有一个方法ToList,但Python包IRIS没有内置该方法,需要通过ObjectScript方式调用它
ClassMethod l2DistancePy(v1 As dc.vector.type, v2 As dc.vector.type) As %Decimal (SCALE= 10 ) [ Language = python, SqlName = l2_distance_py, SqlProc ] { import iris import math vector_type = iris.cls('dc.vector.type') v1 = iris.cls(' %SYS.Python ').ToList(vector_type.Normalize(v1)) v2 = iris.cls(' %SYS.Python ').ToList(vector_type.Normalize(v2)) return math.sqrt(sum([(val1 - val2) ** 2 for val1, val2 in zip(v1, v2)])) }
它看起来一点也不正确。我希望 $lb 可以在 python 中即时解释为列表,或者在列表内置函数 to_list 和 from_list 中解释
另一个问题是当我尝试使用不同的方式测试此功能时。使用嵌入式Python中的SQL,使用嵌入式Python编写的SQL函数,它会崩溃。因此,我还必须添加 ObjectScript 的功能。
ModuleNotFoundError: No module named 'dc'
SQL Function VECTOR.NORM_PY failed with error: SQLCODE=-400,%msg=ERROR #5002: ObjectScript error: <OBJECT DISPATCH>%0AmBm3l0tudf^%sqlcq.USER.cls37.1 *python object not found
目前在 Python 和 ObjectScript 中实现了计算距离的函数
欧氏距离
[SQL]_system@localhost:USER> select embedding, vector.l2_distance_py(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+----------------------+ | embedding | distance | + -----------+----------------------+ | [4,5,6] | 5.91607978309961613 | | [1,2,3] | 10.77032961426900748 | + -----------+----------------------+ 2 rows in set Time : 0.011 s [ SQL ]_system@localhost: USER > select embedding, vector.l2_distance(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+----------------------+ | embedding | distance | + -----------+----------------------+ | [4,5,6] | 5.916079783099616045 | | [1,2,3] | 10.77032961426900807 | + -----------+----------------------+ 2 rows in set Time : 0.012 s
余弦相似度
[SQL]_system@localhost:USER> select embedding, vector.cosine_distance(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+---------------------+ | embedding | distance | + -----------+---------------------+ | [4,5,6] | .034536677566264152 | | [1,2,3] | .11734101007866331 | + -----------+---------------------+ 2 rows in set Time : 0.034 s [ SQL ]_system@localhost: USER > select embedding, vector.cosine_distance_py(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+-----------------------+ | embedding | distance | + -----------+-----------------------+ | [4,5,6] | .03453667756626421781 | | [1,2,3] | .1173410100786632659 | + -----------+-----------------------+ 2 rows in set Time : 0.025 s
内积
[SQL]_system@localhost:USER> select embedding, vector.inner_product_py(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+----------+ | embedding | distance | + -----------+----------+ | [1,2,3] | 46 | | [4,5,6] | 118 | + -----------+----------+ 2 rows in set Time : 0.035 s [ SQL ]_system@localhost: USER > select embedding, vector.inner_product(embedding, '[9,8,7]' ) distance from items order by distance; + -----------+----------+ | embedding | distance | + -----------+----------+ | [1,2,3] | 46 | | [4,5,6] | 118 | + -----------+----------+ 2 rows in set Time : 0.032 s
另外还实现了数学函数:add、sub、div、mul。 InterSystems 支持创建自己的聚合函数。因此,可以对所有向量求和或求平均值。但不幸的是,InterSystems 不支持使用相同的名称,需要使用自己的名称(和模式)来执行函数。但它不支持聚合函数的非数值结果
简单的 vector_add 函数,返回两个矢量的和
当用作聚合时,它显示 0,而预期矢量也是
建立索引
不幸的是,由于我在实现过程中遇到了一些障碍,我没能完成这一部分。
缺乏内置的 $lb 到 python 列表转换以及当 IRIS 中的矢量存储在 $lb 中时返回,并且所有具有构建索引的逻辑预计都在 Python 中,从 $lb 获取数据并将其设置回全局变量也很重要
缺乏对Global的支持
IRIS 中的 $Order,支持方向,因此可以反向使用,而Python内嵌的 order 实现没有它,因此需要读取所有键并反转它们或将末尾存储在某处
由于对上面提到的从 Python 调用的 Python 的 SQL 函数的不好的体验而产生疑问
在构建索引期间,预计会在图形中存储矢量之间的距离,但在global里保存浮点数时遇到了bug
我在工作中发现了11 个嵌入式 Python 问题,所以大部分时间都是在寻找解决方法来解决问题。在名为iris-dollar-list的 @Guillaume.Rongier7183 项目的帮助下,我成功解决了一些问题。
安装
无论如何,它仍然可用,并且可以与 IPM 一起安装,即使功能有限也可以使用
zpm "install vector"
或者在开发模式下使用 docker-compose
git clone https://github.com/caretdev/iris-vector.git cd iris-vector docker-compose up -d
文章
Lilian Huang · 四月 16
Hi 大家好在本文中,我讲介绍我的应用 iris-AgenticAI .
代理式人工智能的兴起标志着人工智能与世界互动方式的变革性飞跃--从静态响应转变为动态、目标驱动的问题解决方式。参看 OpenAI’s Agentic SDK , OpenAI Agents SDK使您能够在一个轻量级、易用且抽象程度极低的软件包中构建代理人工智能应用程序。它是我们之前的代理实验 Swarm 的生产就绪升级版。
该应用展示了下一代自主人工智能系统,这些系统能够进行推理、协作,并以类似人类的适应能力执行复杂任务。
应用功能
Agent Loop 🔄 一个内置循环,可自主管理工具的执行,将结果发回 LLM,并迭代直至任务完成。
Python-First 🐍 利用本地 Python 语法(装饰器、生成器等)来协调和连锁代理,而无需外部 DSL。
Handoffs 🤝 通过在专业代理之间委派任务,无缝协调多代理工作流程。
Function Tools ⚒️ 用 @tool 修饰任何 Python 函数,可立即将其集成到代理的工具包中。
Vector Search (RAG) 🧠 原生集成向量存储(IRIS),用于 RAG 检索。
Tracing 🔍 内置跟踪功能,可实时可视化、调试和监控代理工作流(想想 LangSmith 的替代方案)。
MCP Servers 🌐 通过 stdio 和 HTTP 支持模型上下文协议(MCP),实现跨进程代理通信。
Chainlit UI 🖥️ 集成 Chainlit 框架,可使用最少的代码构建交互式聊天界面。
Stateful Memory 🧠 跨会话保存聊天历史、上下文和代理状态,以实现连续性和长期任务。
代理
代理是应用程序的核心构件。代理是一个大型语言模型(LLM),配置有指令和工具。基本配置您需要配置的代理最常见的属性,包括:
Instructions (说明):也称为开发人员信息或系统提示。model:要使用的 LLM,以及可选的 model_settings,用于配置温度、top_p 等模型调整参数。tools工具: 代理用来完成任务的工具。
from agents import Agent, ModelSettings, function_tool
@function_tool
def get_weather(city: str) -> str:
return f"The weather in {city} is sunny"
agent = Agent(
name="Haiku agent",
instructions="Always respond in haiku form",
model="o3-mini",
tools=[get_weather],
)
运行代理
您可以通过 Runner 类运行代理。您有 3 个选项:
1.Runner.run():异步运行并返回 RunResult。2.Runner.run_sync(),这是一种同步方法,只是在引擎盖下运行 .run()。3.Runner.run_streamed():异步运行并返回 RunResultStreaming。它以流式模式调用 LLM,并在接收到事件时将其流式传输给您。
from agents import Agent, Runner
async def main():
agent = Agent(name="Assistant", instructions="You are a helpful assistant")
result = await Runner.run(agent, "Write a haiku about recursion in programming.")
print(result.final_output)
# Code within the code,
# Functions calling themselves,
# Infinite loop's dance.
代理架构
该应用程序由 7 个专业代理组成:
1. 分诊代理 🤖
角色: 主要路由器,接收用户输入,并通过切换分配任务
示例: 路由 “显示生产错误” → IRIS 生产代理
2.矢量搜索代理 🤖
作用: 提供 IRIS 2025.1 版本说明详情(RAG 功能)
示例: 路由 “向我提供发行说明摘要”→矢量搜索代理
3. IRIS 仪表板代理 🤖功能: 提供实时管理门户指标:明文副本。
ApplicationErrors, CSPSessions, CacheEfficiency, DatabaseSpace, DiskReads, DiskWrites, ECPAppServer, ECPDataServer, GloRefs, JournalStatus, LicenseCurrent, LockTable, Processes, SystemUpTime, WriteDaemon, [...]
4. IRIS Running Process Agent 🤖
功能: 监控活动进程的详细信息:
Process ID | Namespace | Routine | State | PidExternal
5. IRIS Production Agent 🤖
角色: 提供生产详情以及启动和停止生产的功能。
6. WebSearch Agent 🤖
功能: 通过 API 集成执行上下文网络搜索
7.Order Agent 🤖
功能: 使用订单 ID 检索订单状态
交接
交接允许代理将任务委托给另一个代理。这在不同代理擅长不同领域的情况下尤其有用。例如,客户支持应用程序可能会有专门处理订单状态、退款、常见问题等任务的代理。
分流代理是我们的主代理,它会根据用户输入将任务分配给另一个代理
#TRIAGE AGENT, Main agent receives user input and delegates to other agent by using handoffs
triage_agent = Agent(
name="Triage agent",
instructions=(
"Handoff to appropriate agent based on user query."
"if they ask about Release Notes, handoff to the vector_search_agent."
"If they ask about production, handoff to the production agent."
"If they ask about dashboard, handoff to the dashboard agent."
"If they ask about process, handoff to the processes agent."
"use the WebSearchAgent tool to find information related to the user's query and do not use this agent is query is about Release Notes."
"If they ask about order, handoff to the order_agent."
),
handoffs=[vector_search_agent,production_agent,dashboard_agent,processes_agent,order_agent,web_search_agent]
)
跟踪
Agents SDK 包括内置跟踪功能,可收集代理运行期间事件的全面记录: LLM 生成、工具调用、切换、防护栏,甚至发生的自定义事件。使用跟踪仪表板,您可以在开发和生产过程中调试、可视化和监控工作流。https://platform.openai.com/logs
应用界面
应用工作流程矢量搜索代理
矢量搜索代理自动获取 New in InterSystems IRIS 2025.1 如果数据还不存在,只需将文本信息输入 IRIS 矢量存储区一次。
使用下面的查询来获取数据
SELECT
id, embedding, document, metadata
FROM SQLUser.AgenticAIRAG
分流代理接收用户输入,将问题转给矢量搜索代理。
IRIS 仪表盘代理
分流代理接收用户输入,将问题路由到 IRIS 仪表板代理。
IRIS 流程代理分流代理接收用户输入,将问题路由到 IRIS 流程代理。IRIS 生产代理
使用生产代理启动和停止生产。使用生产代理获取生产详情。
本地代理
分流代理接收用户输入,将问题转给本地订单代理。WebSearch 代理
在这里,分流代理接收到两个问题,并将两个问题都路由到 WebSearcg 代理。
MCP Server 应用
MCP Server在这里运行 https://localhost:8000/sse
下面是启动 MCP 服务器的代码:
import os
import shutil
import subprocess
import time
from typing import Any
from dotenv import load_dotenv
load_dotenv()
#Get OPENAI Key, if not fond in .env then get the GEIMINI API KEY
#IF Both defined then take OPENAI Key
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY is not set. Please ensure to defined in .env file.")
if __name__ == "__main__":
# Let's make sure the user has uv installed
if not shutil.which("uv"):
raise RuntimeError(
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
)
# We'll run the SSE server in a subprocess. Usually this would be a remote server, but for this
# demo, we'll run it locally at http://localhost:8000/sse
process: subprocess.Popen[Any] | None = None
try:
this_dir = os.path.dirname(os.path.abspath(__file__))
server_file = os.path.join(this_dir, "MCPserver.py")
print("Starting SSE server at http://localhost:8000/sse ...")
# Run `uv run server.py` to start the SSE server
process = subprocess.Popen(["uv", "run", server_file])
# Give it 3 seconds to start
time.sleep(3)
print("SSE server started. Running example...\n\n")
except Exception as e:
print(f"Error starting SSE server: {e}")
exit(1)
MCP 服务器配备了以下工具:
提供 IRIS 2025.1 发行说明详情(矢量搜索)
IRIS 信息工具
检查天气工具
查找暗语工具(本地功能)
加法工具(本地功能)
MCP 应用在这里运行 http://localhost:8001
MCP 服务器矢量搜索(RAG)功能
MCP 服务器配备 InterSystems IRIS 向量搜索摄取功能和检索增强生成 (RAG) 功能。
MCP Server other functionality
The MCP Server dynamically delegates tasks to the appropriate tool based on user input.
更多详情,请访问 iris-AgenticAI open exchange 界面。
谢谢!
文章
Claire Zheng · 九月 16
2024年10月19日,首都医科大学附属北京安贞医院(简称“北京安贞医院”)通州院区开诊,医院迈入两院区一体化发展新阶段。“随着手术量的增加,术中监测设备上的指标自动采集,对于术中工作效率的提高就更加重要了,但有些设备尚未做到国产化,例如心脏搭桥手术血流监测仪是挪威进口设备,只支持通过FHIR标准与HIS对接。于是,我们在院区开业一个月后启动了基于FHIR(Fast Healthcare Interoperability Resources,快速医疗保健互操作性资源)的医疗信息系统集成技术路线与开发项目,以心脏搭桥手术血流监测仪为突破口,实现此类设备的结构化数据采集、存储及应用。”北京安贞医院信息中心技术总监周奕介绍。
对接医、教、研需求
作为一家以心血管疾病诊治为特长的大型三甲医院,北京安贞医院的心脏内外科数量占了医院科室的一半,医院心脏外科手术量每年超过15000例,其中心脏搭桥手术占一半以上。
在开展搭桥手术过程中,血管流量监测设备中的指标数据,是术中及术后判断桥血管通畅性和血流动力学的重要指标。北京安贞医院的信息化建设基本上涵盖了医院所有业务,但此类进口医疗设备数据始终游离于信息系统之外,“因为它平常是采用DICOM格式传输报告,输出形式主要是影像图片,但PACS系统解析这些图片后,却是一堆乱码,无法进一步应用。”周奕介绍道。
于是,医生在进行搭桥手术时,需要由巡回护士或麻醉师手工抄写患者手术过程中的相关数据,录入Excel表格中,整个过程效率低下,也不利于科研和教学等数据利用需求。
从手工到电子化,可实时提取数据
“接口系统开发的第一步是需求调研。”周奕谈到,项目组分别同心外科医生、麻醉医生、体外循环医生和护士等相关人员进行了充分讨论,考虑了临床的各个场景,在此基础上设计了系统架构,然后是详细设计。由于FHIR具有便捷开发的特点,因此整个系统只用了半个月时间就开发完毕,实现了HIS系统与血管流量监测设备的对接。
“系统对接后的一大转变,是彻底改变了以前传统的手工流程,实现了全自动提取。”周奕强调,巡回护士在血管流量监测设备系统屏幕上点击一下,就会调出患者的基本信息及手术排班信息。当患者的心脏搭桥手术开始后,监测数据便开始自动采集,并传回HIS系统。HIS系统可以按照手术时间顺序、血流量和压力变化等,即刻出具图文并茂的报告,呈现在电子病历的手术记录中,具有很好的临床辅助决策支持意义。根据科研需求,周奕带领团队对报告的内容进行了全结构化处理,并将其保存在科研数据中,方便统一利用。
巡回护士在血管流量监测设备系统屏幕上点击一下,就会调出患者的基本信息及手术排班信息,手术开始后,监测数据便开始自动采集,并传回HIS系统。
手术开始后,数据即时回传至HIS系统,并生成图文并茂的报告,医生可以随时在电子病历中查看相关数据和报告。
FHIR四大优势推动集成落地
周奕谈到,基于WEB技术构建的FHIR标准具备四大优势,有效推动了集成的落地。
第一,开发非常便捷。FHIR能够按照标准的RESTful的常用请求方法调用接口,对开发人员而言,不用再摸索那些复杂的数据传输协议,使开发过程简单便捷。
第二,数据互操作性强。FHIR具备跨系统、跨平台的兼容性,只要遵循FHIR的资源结构和接口规范,即可实现标准化的数据交换,极大降低了系统集成复杂度。
第三,行业认可度高。FHIR在全球范围获得认可。
第四,符合法规和安全要求。医疗数据一般涉及患者的隐私,受到严格的法律法规监管。FHIR充分考虑到这一点,内置了安全机制,包括身份认证和数据加密等,支持多模式的验证,确保数据传输、存储的合规。
作为此次项目运行的基石平台,InterSystems IRIS医疗版数据平台提供了标准化FHIR API、事务处理能力与高性能存储能力,为整个系统的可靠运行提供了坚实支撑。“由于FHIR标准所具备的优势,我们的开发仅用了半个月就完成了。”周奕说,“在这个项目中,InterSystems提供的不仅仅是一个平台,还提供了方法和服务,帮助医院快速构建起稳定、高效、具备扩展性的FHIR数据服务能力。”
三步走整合数据中心
医院积累了大量数据后,如何实现数据的有效应用是一大挑战。
北京安贞医院在2018年就建立了数据中心,包括临床数据中心、科研数据中心、运营数据中心。周奕介绍,在医院制定的《北京安贞医院2025-2027信息化建设三年规划》中,一项非常重要的工作就是整合数据中心,主要分为三步:第一步,尽可能全地收集原始数据,形成数据湖;第二步,进行数据质控及初步清洗,形成纯净湖;第三步,在纯净湖基础上,根据各方应用,形成统一数据中心。
谈及医疗设备与HIS系统集成后的下一步规划时,周奕强调,将根据临床提出的新需求,探索通过FHIR标准将脱敏后的手术数据接入国际心脏内、外科排名系统,客观了解国内心脏病诊疗水平在国际上的位置,提升国际影响力。在此次项目中积累的FHIR能力,也有助于这项工作的完成。
本文首发于CHIMA微信公众号,点击此处查看原文。
文章
Claire Zheng · 一月 4, 2023
2022年,香港大学深圳医院迎来建院十周年。作为深港合作医疗平台和公立医院改革试点,医院是由深圳市政府全额投资,并引进香港大学现代化管理模式的大型综合性公立医院。十年来,医院全力打造集“医、教、研、管”为一体的四个粤港澳大湾区国际化中心,2021年7月成为国家公立医院高质量发展试点医院,2021年12月成为国家建立健全现代医院管理制度试点医院。目前,医院正在探索改革创新路径,全面配合公立医院高质量发展对信息化建设提出的需求。
香港大学深圳医院设立了绿色医院建设内涵,从绿色生态建筑的设计、到建筑智能化的应用、再到各类医疗信息系统的实施,医院践行可持续发展的建设理念。数字化转型是公立医院医院高质量发展的必由之路。十年间,医院在打造公立医院高质量发展路径过程中,借助信息技术推动数字化转型,探索智慧医院建设,不断提升医疗质量和效率,优化医院内部医疗资源配置,改善人民群众就医感受。
推行绿色医疗,开展数字化转型
作为深圳市首家去编制的公立医院,自成立伊始,香港大学深圳医院以“绿色医疗理念”为指导,引入香港公立医院管理模式,成为深圳市乃至全国公立医院改革的先锋。
香港大学深圳医院的绿色医疗涵盖八大主题:绿色办医,公平公正;绿色管医,廉洁高效;绿色行医,专业行政;绿色治安,持续改进;绿色文化,关爱慈善;绿色科技,智慧领跑;绿色建筑,节能降耗。
其中,“绿色科技,智能领跑”这一主题涉及到信息化建设,香港大学深圳医院通过构建一体化系统,探索出数字化转型四条特色路径。
1.通过新一代集成平台构建一体化业务系统
医院开业之初,采用了7+1的集成平台系统架构模式,为医院发展提供了重要的信息化支撑。但是该集成平台设计较早,在流程中没有考虑队列问题,不利于数据流通。同时,旧系统每天的消息吞吐量达到500多万条,要求每分钟每个业务系统处理要达到200条才能处理完,否则会对数据传输造成影响。基于此,医院期望在高效传输协议和标准化方面做一些探索和实践。经过深入调研后,医院于2021年1月份启动新一代集成平台建设。新一代集成平台于2021年8月份整体上线,构建了以InterSystems高性能数据平台为核心的一体化业务系统,新一代集成平台通过InterSystems数据平台先进的互操作能力、高度可扩展能力以及对国际数据标准(如HL7 V2和FHIR)的深度支持,实现了数据的标准化、规范化和一体化,加强了患者主索引(EMPI)和主数据词典(MDM)的建设,进一步完善了患者信息档案,实现了统一身份登录认证,推动数据互联互通和共享。
医院信息平台技术架构
香港大学深圳医院的集成平台分为内网和外网,两者数据是互通的,通过授权、安全访问机制、API管理,向患者提供标准化医疗服务。主要有两方面的业务协同。
第一是院内业务系统协同,是业务信息系统交换的核心组件。内网通过多种传输协议和技术,在不改变原有业务系统自身协议的情况下,进行数据对接和互联互通。其主要核心功能包括数据在消息流转的过程中,完成数据整个格式和内容的转换和调整。比如新一代集成平台可以把HL7 V2的协议转换为FHIR的文档,当医生在日常工作中需要浏览检查检验申请单或传输患者病历时,因病历内容很长,如果用HL7 V2或V3协议,传输的数据量非常大,传输也会比较慢。借助新一代集成平台,医院可以将需要传输的内容通过FHIR进行转换,从而将传输内容大幅缩短,大幅提高了传输效率,使数据流转更畅通。
InterSystems对FHIR的深度支持可以让香港大学深圳医院无缝地完成多数据模型的格式转换,从而实现整个异构数据类型的整合以及不同应用系统之间的交互服务,缩短了业务开发周期,聚焦到业务应用。
院内总线
第二,互联网业务和院内业务系统的协同,需要整合应用系统的数据或服务,实现单个或多个应用的场景。和单纯院内系统协同不同的是,当互联网医院的业务涉及和第三方系统以及互联网医疗一些商用平台对接的时候,医院一般不开放直接数据库,都是通过接口的方式,在内网的集成平台和外网的集成平台进行数据交互,再通过外网的集成平台提供对外的数据访问服务,InterSystems也提供了安全访问机制,用于平台对外提供各种数据服务。
院外总线
2.探索数字化转型四条路径
作为首家深港合作医院,医院的立足点是以深圳为中心,借助信息技术,向粤港澳大湾区的疑难重症患者提供高质量的诊疗救治服务。依据这一立足点,香港大学深圳医院探索了数字化转型的四条路径。
第一,基于目前已经开展的港澳药械通服务,在全国首次探索实现跨境药械SPG供应链追溯数据互联互通。目前医院建立了跨境药械SPG供应链,将国外药企的生产企业、经营企业、第三方物流公司,和医院的三方供应链联通,构建供应链平台,将供应的物资及时提供给医院,积极开展精细化的运营管理,包括跨界药械的申请、采购、进口、配送、使用以及不良反应监测的全过程监控、追溯,实现大湾区临床急需药品器械来源可溯,去向可追,使用可控,责任可纠。
第二,跨境医疗服务的健康数据共享。这个主要是探索跨境医疗服务的应用模式,目前医院和香港医管局在进行一个名为在院患者特别支援诊疗计划的合作。该合作于2020年11月份启动,由香港医管局授权,医院对在粤的香港患者进行后续诊疗复诊服务。香港医管局会把这些患者的病历提供给医院,实现了整个医疗健康档案的跨境应用,推进整个粤港澳湾区一体化的健康数据共享以及互联互通。
第三,加速推动新兴技术和医疗服务的深度融合。医院借助大数据、互联网、AI语音等信息技术,依托云计算、5G通信技术,并基于现有的院内信息系统,构建统一的数据存储平台。同时,医院还探索了全语音驱动操作机器人在智慧病房的应用。
第四,推动数据的创新应用。主要是结合医院重点医疗学科建设,把就诊的每个环节标准化、规范化,包括拆分和数字化。医院结合先进的信息技术,对这些数据提炼应用,帮助院内各流程提能、增效。医院也会和一些大学、企业、研究机构合作,形成产、学、研合作中心,把每个治疗环节和整个服务,应用信息技术,提升到较高的境界,为医院医疗服务提供更多支撑。
香港大学深圳医院在进行数字化转型过程中,逐步探索出符合医院管理模式、运营模式和业务模式的智慧医院建设方式,主要包括以下三点:第一,以智慧医院建设为抓手,加快和推进公立医院的高质量发展;第二,以信息化技术手段为路径,全面推动很多业务落地;第三,以远程医疗服务为支点,推进医疗服务的融会贯通,树立粤港澳大湾区医疗服务创新标杆。
规划智慧医院建设四个方向
在逐步推动医院数字化转型过程中,香港大学深圳医院根据公立医院高质量发展的要求,开展了三位一体智慧医院建设,打造具有中国特色的国内顶尖、国际一流的智慧医院样板,实现了流程无纸化、业务智能化、管理精细化、服务人性化。医院规划了智慧医院建设的四个方向。
第一,针对智慧医疗,围绕电子病历评级应用水平分级、以及互联互通的标准化成熟度等级评测等要求进行信息化建设,开展以评促建的升级改造服务。通过信息技术,实现院内流程闭环、互联互通和信息共享,完善医疗服务体系,构建线上线下、院内院外一体化医疗服务体系。
第二,针对智慧服务,将5G技术贯穿于整个院前、院中、院后的诊疗环节,加快5G在疫情预警、院前急救、实施会诊、远程手术以及医院和香港的跨境医疗等智慧医疗服务的应用,打造未来创新医疗服务中心。
第三,针对智慧管理,建设数字化医疗质量评价促进中心,上线国家三级公立医院绩效考核系统和智慧后勤的楼宇监测系统,加强运营管理监测,进行全方位驱动医院管理效能的提升。
第四,结合大数据、人工智能等新兴技术构建医院的数字大脑,建设医疗运营数据中心、科研数据中心和影像数据中心的主题数据库,设立多维度早期预警,实现敏感指标的监控,为智慧医院建设平台提供支撑。
通过这四项措施,香港大学深圳医院实现了智慧管理,构建了数字孪生的智慧医院。
未来,香港大学深圳医院将按照“高起点、高水平、高质量”的标准持续推进数字化转型,开展下一个十年的智慧医院建设:通过运用大数据、云计算、AI人工智能、物联网、移动互联网、5G等新技术,医院将持续创新各类医疗应用场景,实现“流程无纸化、业务智能化、管理精细化、服务人性化”的高质量医院管理体系;通过高质量医院建设,构建连接、共享、协同的粤港澳大湾区医疗服务体系,探索医疗改革,促进湾区融合。
如何应对医院信息科日常面临的两大挑战
系统选型以及团队建设是医院信息科日常面临的两大挑战,香港大学深圳医院也给出了自己的特色践行方案:
1.如何进行系统服务商选型
香港大学深圳医院信息系统选型标准有四个:
第一, 在医疗信息化领域位列前三甲,并在行业深耕的HIT厂商,通常排名靠前的厂商具有更好的专业背景、行业前瞻性以及更为完整的解决方案;
第二, 在本地有完善的实施和售后团队的,能够及时响应医院需求,并对医院需求进行定制化开发;更重要的是,成熟的实施和售后团队能够将业务和技术互联互通,协助信息科有效甄别临床的“真伪需求”,提供恰当的解决方案;
第三, 有专业和强大的技术研发团队,保障该厂商在医疗行业的可持续投入和发展;
第四, 双方要建立深入的战略合作,确保对系统/信息化目标达成共识,形成合力。
2.如何进行进行信息科的团队建设
香港大学深圳医院负责信息化建设的部门命名为信息资讯科技部(Information Technology Department),团队共26人,分为软件服务组、硬件服务组、数据安全组、项目管理组和行政服务组5个小组。在团队建设方面,信息资讯科技部负责人主要采取了以下措施:
第一, 把握好自己的角色,了解团队成员的性格、能力和习惯,以人为本进行任务安排;
第二, 有效的团队沟通,和团队成员建立良好的沟通机制,授人以渔,给予充分的发展空间,通过每个小组A、B角色安排,兼顾任务与个人能力发展;
第三, 掌握好批评和表扬,通过表扬激发团队的斗志,通过数据说话呈现工作欠缺,批评不针对个人,而是针对事件,指出调整方向;
第四, 对团队成员充分信任,同时让每位团队成员树立责任感;真正做到用人不疑,疑人不用;
第五, 合理分配和安排工作,保障工作完成的时间和质量。时间紧急的,需要找经验丰富的团队成员完成,时间不紧急的可以找希望往这方面发展的团队成员。
文章
Claire Zheng · 八月 17, 2021
实现互通的方式方法有很多种,我们通常会见到4种:消息交换、文档交换、服务和 API。
消息交换是最常见的一种互操作的方式,在医疗用得非常多。消息交换是基于消息引擎的,通常它应用在低业务集成度和跨数据管理域的业务环境里面。通常消息是基于临床事件,描述临床事件发生的上下文,并且能够在临床事件发生的时候,通过消息引擎把它路由给消息的接收方。
消息交换的本质是一个中心化的互操作方式,中心化的方式也能够保证消息的先进先出。我们常见的消息标准有HL7 V2、 HL7 V3的消息。
文档交换是另外一种常见的互操作方式。有别于基于临床事件的消息,文档是一种阶段性的、小结性的、完整的医疗信息的汇总。它的应用场景也是低业务集成度、跨数据管理域的环境,不过通常情况下,文档交换通常都是用在机构之间的,而不是在一家医疗机构内部的不同业务系统之间。可以通过消息引擎,当然也可以使用其他的方式来进行相应的交换。文档的标准,我们最常见的有HL7 CDA。
服务的交换是另外一种。服务是什么?服务是封装好的,并且暴露出来的一组内聚的应用系统的功能。基于服务交互的互操作,需要双方规范互操作的业务流程和角色。服务交互通常是基于面向服务的这种架构、通过服务总线来进行交互的,也是应用在低业务集成度和跨数据管理的业务环境。服务是基于规范的业务流程、角色的,但是在医疗行业并不是所有的医疗流程都已经或者是能够规范的,所以服务交互目前来看还是有一定的适用范围。最常见的国际上的服务标准是IHE。
API是现在最流行的。API在当今的网络经济里已经是无处不在了。它是概述了一组明确定义的规范,允许应用程序能够建立在另一个应用程序已有的数据和功能之上,而且不需要去了解其他的应用系统的系统设计。有别于消息和文档,API可以仅传输必要的信息,效率比较高,不需要传完整的上下文,所以它不需要中心化的消息,本质上是一个去中心化的架构,比较适合于业务集成度更紧密的互操作的场景。
目前面临的是传统的医疗边界被突破了——例如互联网医院这样的使用场景——我们需要扩大互操作性、互操作对象和数据,而这也驱动了医疗API的出现,这也是我这次介绍的一个重点,HL7 FHIR标准。
注:本文根据InterSystems中国技术总监乔鹏演讲整理而成。
文章
王喆 👀 · 十月 18, 2022
ObjectScript是一种面向对象的编程语言,它是InterSystems公司的Caché和Ensemble数据库的核心语言之一。ObjectScript语言的语法类似于MUMPS语言,它支持面向对象编程、过程式编程、函数式编程等多种编程范式。ObjectScript语言主要用于开发Caché和Ensemble数据库应用程序,它可以访问数据库中的数据、调用数据库中的存储过程、触发器和事件,还可以与其他编程语言进行交互。
Cache使用的语言是ObjectScript简称COS,下面展示的是其基本语法,也是我个人的COS字典:
1 系统指令
SET 缩写 s ,赋值命令,样例 - s hello ="Hello World";
WRITE 缩写 w ,向当前设备输出,样例 - w hello (特殊用法:w ! 换行、w # 清屏 )
DO 缩写 d ,执行函数,样例 – d ##class(%SYSTEM.License).ShowSummary();
Kill 缩写 k ,从堆栈中清楚变量 x,慎用(不加参数调用时候将清楚内存中的所有变量!)样例 - k x
Quit 缩写 q , 返回 样例 - q $$$OK
注意:系统指令不区分大小写,变量和对象大小写敏感
2 数据类型
%Integer 整数型 0,1,2,3,4,5,6......
%Boolean 布尔类型 0-F 1-T
%String 字符串类型 "你好,世界"
%Date 时间类型
更多的数据类型:
https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_proplit#GOBJ_C27523
3 运算符
数学运算符 加减乘除+,-,*,/ W 2+3+2*2 14 运算时严格从左到右执行,数学运算没有优先级 W 2+3+2*2 9
连字符_ W "shanghai"_"disney"
逻辑运算符 与或非&, ||, ‘ W 1&0
4 流程控制
For 循环 FOR variable=start:increment:end { . . . }
#; 第1种
For i = 1:1:9 {
Write i, !
}
#; 第2种
Set key = ""
For {
Set key = $ORDER(array(key))
Quit:key=""
// process array(key)
}
#; 第3种
For value = "Red","Green","Blue" {
Write value, !
}
If else判断 IF expression1 { . . . } ELSEIF expression2 { . . . } ELSE { . . . }
If a>0{
w a
}ElseIf a<0&&a=-1{
w a
}ElseIf a<-1{
w a
}else{
w a
}
While循环 WHILE expression,... { ;. . . }
#; 第1种
Do {
} While (1 /* condition */)
#; 第2种
While (1 /* condition */) {
}
5 系统变量
当前时间 $Horolog — W $H
时间戳 $ZTIMESTAMP — w ZTS
系统版本 $ZVERSION — W $ZV
注意:系统变量不区分大小写
更多系统变量
https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_VARIABLES
6 函数
字符串类型与日期类型相互转换: $ZDH/$ZD
— w $ZDH("2017-03-13",3)
— w $zd(0)
字符串截取函数: $piece
— W $P("This is training", " ",3)
字符串比较替换函数: $Case
— W $case(10,1:"100",2:"200",:"1000")
按值查找,并返回一个整数,该整数指定子字符串中的结束位置 $FIND
w $f("acvs","c",1) 输出为字符串的位置,输出为0标识不存在
更多函数:
https://docs.intersystems.com/healthconnect20201/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_FUNCTIONS
7 Global
Global算是IRIS的一个特殊概念,其实可以把它理解为可以持久化的多维数组,下面展示的是把Global当成多维数组的处理方式
下标:可以是数字也可以是字符串
操作:赋值 - set(s) 、删除 - kill(k)
持久化:多维数组命名时候以 ^ 开头,会按下标存储,如果用的是HealthConnect或者IRISHealth在【系统资源管理器】- 【Global】中可以看到。
样例:
s a = 1,a(1) = "a",a(1,1) = "b",a(1,1,"wow") = "foo",a(1,2) = "c",a(2) = 0 zw a
(这里,的意思代表省略了前面的 set 系统指令)如图:
这是我早些时候的笔记,可能至今还有一些错误,希望有看出问题的大佬指正。 非常棒的分享!
文章
Hao Ma · 五月 24, 2023
Manifest也许应该被翻译成“清单”, 字典上是这么解释的: 提供船舶及其货物和其他物品、乘客和船员的全面细节的文件,供海关官员使用,比如:飞机上的乘客或货物清单; 一辆货运列车的车厢清单。
在计算机语言中, Manifest可以是各种格式,用的最多的是xml和json,在IRIS中,manifest是xml格式的, 放在objectscript类的XDATA块里。
## 编写mainfest
IRIS用manifest来做配置。内部工具*%install*, 会读取manifest, 生成真正的objectscript代码来配置IRIS。我们来看个基本的例子。
### 基本用法
下面的User.Manifest.cls` ,它配置了IRIS的global buff, bbsize等等, 然后还创建了一个命名空间。
```java
Include %occInclude
Class User.Manifest
{
ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal ]
{
Quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "MyInstall")
}
XData MyInstall [ XMLNamespace = INSTALLER ]{
}
}
```
稍微解释一下代码:
- `Include %occInclude`是必须的
- `setup()`用来读取manifest的内容,完成配置工作。用户基本不用修改这个method。
- mainifest本身的逻辑层次很清楚,要配置什么内容查查文档都可以。上面的manifest只是个示意,真正用起来可以需要非常多的配置项,比如namespace, database的配置,有很多的标签可选。
### 传参数给manifest
调用manifest的method, 也就是例子里的setup(), 注意第一个参数是`ByRef pVars`。这是objectscript里常用的By referrence的传参方式。请看下面的例子:
```java
Include %occInclude
Class User.Manifest
{
ClassMethod main(){
Set pVars("Namespace")="MYNAMESPACE"
Set pVars("AnotherKey")= "AnotherValue"
$$$ThrowOnError(..CreateNamespace(.pVars))
}
ClassMethod CreateNamespace(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal ]{
Quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "CreateNamespace")
}
XData CreateNamespace [ XMLNamespace = INSTALLER ]{
}
}
```
上面code在main()里定义了一个pVars, 放了两个key-value, 用来调用CreateName()。 定义的namespace在manifest用到了, Anotherkey被用在了log里, 只是一个示意。
注意这个定义: `Dir="${MGRDIR}/${Namespace}"`。 其中的`MGRDIR`不需要自己定义。Manifest有一堆自己定义的Variable, 用的最多的是 CFGDIR, CSPDIR, INSTALLDIR, MGRDIR, PORT等等。具体列表见文档。
### 更多的用法
下面这个例子包括了import文件和copy文件, *SourceDir*和*Namespace*是传入的参数。导入文件要在一个namespace的定义里面, 拷贝文件和命名空间无关。
```xml
```
**CSPApplication**
在manifest里定义cspapplication不难,麻烦的是不同的版本使用的标签上会有修改。下面给了一个例子。
```xml
```
>
**调用代码的例子**
```xml
```
**创建User, Role**
```xml
```
Nampespace Mapping
```xml
```
其他还有很多的用法。想了解更多,可以看看[文档说明里的Tag列表](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GCI_manifest#GCI_manifest_tags), 和[template](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GCI_manifest#GCI_manifest_example_template)。
## 怎么执行cls文件
[Using the Manifest](https://docs.intersystems.com/iris20223/csp/docbook/DocBook.UI.Page.cls?KEY=GCI_manifest#GCI_manifest_invoke)
**在Terminal里执行**
```zsh
%SYS>do ##class(MyPackage.MyInstaller).setup()
```
或者, 带上参数
```
%SYS>set vars("SourceDir")="c:\myinstaller"
%SYS>set vars("Updated")="Yes"
%SYS>do ##class(MyPackage.MyInstaller).setup(.vars,3)
```
**During IRIS安装**?
Export the manifest class as DefaultInstallerClass.xml to the same directory where the InterSystems IRIS install (either .msi, setup_irisdb.exe, or irisinstall) is run. It is imported into %SYS and compiled, and the setup() method is executed.
那么是irisinstall里面的什么语句在执行manifest呢?
```bash
# script, 用ISC_INSTALLER_MANIFEST, installer-manifest-example.xml
[root@silent jhvinstall]# cat cache_install.sh
#!/bin/bash
echo -n "Installing Cache..."
ISC_INSTALLER_MANIFEST=$3 ISC_PACKAGE_INSTANCENAME=$1 ISC_PACKAGE_INSTALLDIR=$2 ISC_PACKAGE_UNICODE="Y" ISC_PACKAGE_INITIAL_SECURITY="Minimal" ISC_PACKAGE_MGRUSER="cacheusr" ISC_PACKAGE_MGRGROUP="cacheusr"
ISC_PACKAGE_USER_PASSWORD="sys" ./cinstall_silent
[root@silent jhvinstall]# ./cache_install.sh CACHE6 "/cache/tmpcache6" "/tmp/installer-manifest-example.xml"
Installing Cache...
```
**写一个脚本执行**
这里给一个脚本的例子,简短,但内容很丰富。
```bash
#!/bin/bash
# Usage install.sh [instanceName] [password]
instanceName=$1
password=$2
DIR=$(pwd)
ClassImportDir=$DIR/install
NameSpace="ENSDEMO"
CspPath="/csp/ensdemo"
SrcDir=$DIR/src/CLS
DirFront=$DIR/src/CSP/csp/demo
irissession $instanceName -U USER
文章
Hao Ma · 四月 16, 2024
索引分析器工具用来分析索引的使用情况,对DBA和开发者非常有用。 他们需要知道那些查询进行了全表扫描,那些查询缺失了索引, 而那些索引从来又从来没有被用过。多余的索引降低系统性能,浪费了磁盘空间。
**索引使用情况**
到“管理门户”的" 系统 > SQL 性能工具 > SQL 索引分析器", 点击**“索引使用情况”**, 您将看到这样的图
执行SQL语句查询会带来更多的灵活性。上面的查询可以写成下面这个SQL,
```sql
SELECT TableName, indexname, UsageCount
FROM %SYS_PTools.UtilSQLAnalysisDB order by usagecount desc
```
2016年以后的Caché版本就已经有了'索引使用情况'的查询。使用管理门户没有区别, 但SQL语句不同,使用的是比较老的类和表名,各位请参考文档。
注意上图中另外几个按钮,它们的介绍在文档的[这个链接](https://docs.intersystems.com/irisforhealth20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOD_indexes#GSOD_indexes_analyze_tool_reportoptions), 简单的做个翻译:
**全表扫描的查询**:
可识别当前命名空间中进行全表扫描的所有查询。应尽可能避免全表扫描。全表扫描并非总能避免,但如果某个表有大量全表扫描,则应检查为该表定义的索引。通常情况下,表扫描列表和临时索引列表会重叠;修复一个会移除另一个。结果集列出了从最大块计数到最小块计数的表。[显示计划](https://docs.intersystems.com/irisforhealth20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOE_interpret#GSOE_interpret_showplan)链接可显示语句文本和查询计划。
**使用临时索引的查询**:
该选项可识别当前命名空间中所有建立临时索引以解析 SQL 的查询。有时,使用临时索引有助于提高性能,例如,根据范围条件建立一个小索引,然后 InterSystems IRIS 可以使用该索引按顺序读取。有时,临时索引只是不同索引的子集,可能非常高效。其他时候,临时索引会降低性能,例如,扫描主MAP以在有条件的属性上建立临时索引。这种情况表明缺少一个所需的索引;你应该在类中添加一个与临时索引匹配的索引。结果集列出了从最大块计数到最小块计数的表。[显示计划](https://docs.intersystems.com/irisforhealth20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOE_interpret#GSOE_interpret_showplan)链接可显示语句文本和查询计划。
**缺少JOIN索引的查询**:
该选项会检查当前命名空间中所有使用JOIN的查询,并确定是否定义了支持该JOIN的索引。它将可用来支持JOIN的索引从 0(无索引)排到 4(索引完全支持JOIN)。外关联需要单向索引, INNER JOIN需要两个方向的索引。默认情况下,结果集中只包含 JoinIndexFlag < 4 的记录。 JoinIndexFlag=4 表示有一个完全支持JOIN的索引。
**具有离群值Outlier索引的查询**:
该选项可识别当前命名空间中所有具有[异常值的](https://docs.intersystems.com/irisforhealth20241/csp/docbook/DocBook.UI.Page.cls?KEY=GSOD_opttable#GSOD_opttable_tunetable_selectivity)查询,并确定是否定义了支持异常值的索引。它将可用来支持异常值的索引从 0(无索引)到 4(索引完全支持异常值)进行排序。默认情况下,结果集中只包含 OutlierIndexFlag < 4 的记录。 OutlierIndexFlag=4 表示有一个完全支持异常值的索引。
文章
Lilian Huang · 七月 9, 2023
您好!社区的各位老师,
在本文中,我们将学习以下主题:
什么是 Docker?
Docker 的一些好处
Docker 是如何工作的?
Docker 镜像
Docker容器
Docker 镜像存储库
InterSystems 的 Docker 镜像存储库
Docker安装
Docker 基本命令
使用 docker 运行 IRIS 社区版
Docker 桌面图形用户界面
那么让我们开始吧。
1.什么是Docker?
Docker 是一种虚拟化软件,可以让应用程序的开发和部署变得非常简单。 Docker 通过将应用程序打包到所谓的容器中来实现此目的,该容器保留应用程序运行所需的所有内容,包括应用程序的实际代码、其库和依赖项、运行时和环境配置。
Docker 是一个容器化平台,允许开发人员在容器化环境中创建、部署和运行应用程序。 Docker 提供了一种将应用程序及其依赖项打包到单个容器中的方法,该容器可以在任何支持 Docker 的计算机上运行。这使得创建可快速、轻松部署的可移植、轻量级应用程序变得容易。
2. Docker 的一些好处
您可以在下面找到使用 Docker 的一些好处:
可移植性Docker 容器可以在任何支持 Docker 的机器上运行,从而可以轻松地跨不同环境部署应用程序。
一致性通过将应用程序及其依赖项打包到容器中,Docker 可确保应用程序一致运行,无论底层基础设施如何。
可扩展性Docker 通过运行同一容器的多个实例,可以轻松地水平扩展应用程序。
资源效率Docker 容器是轻量级的,需要最少的资源,这使得它们非常适合在云基础设施上运行。
安全性Docker 为运行应用程序提供了安全且隔离的环境,降低了与其他应用程序或主机系统发生冲突的风险。
3.Docker是如何工作的?
Docker 为称为容器的应用程序创建虚拟化环境。容器是一个轻量级、独立的可执行包,包含运行应用程序所需的所有内容,包括代码、库和依赖项。容器与主机系统隔离。因此,它们可以在任何支持 Docker 的机器上运行,无论底层操作系统或硬件如何。
容器是从映像创建的,这些映像是定义应用程序及其依赖项的只读模板。这些镜像存储在称为注册表的中央存储库中,例如 Docker Hub 或私有注册表。开发人员可以自己创建自定义映像或使用注册表中的预构建映像。
当容器启动时,它是从映像构建的,并拥有自己的隔离文件系统、网络和进程空间。然后,容器可以运行应用程序,就像在专用服务器上运行一样。
4. Docker 镜像
Docker 映像是一个轻量级、独立的可执行包,它保留执行应用程序所需的所有内容,包括代码、库和依赖项。 Docker 镜像用于构建和运行容器,容器是可用于运行应用程序的隔离环境。
Docker 映像是根据 Dockerfile 构建的,Dockerfile 是一个文本文件,其中包含一组用于构建映像的指令。 Dockerfile 指定基础映像、应用程序代码和依赖项、环境变量以及创建映像所需的其他配置选项。
Docker 镜像存储在注册表中,例如 Docker Hub 或私有注册表。每次从映像创建容器时,它都会在主机上作为单独的进程运行,与其他进程和容器隔离。
Docker 镜像可用于在不同平台上以一致的方式部署应用程序。它们使打包、分发和部署应用程序变得容易,并确保它们在任何地方都以相同的方式运行。
5.Docker容器
镜像的运行实例是一个容器,如上所述,它是一个轻量级的、独立的、可执行的包,其中包含运行应用程序所需的所有内容,包括代码、库和依赖项。
Docker 容器为运行应用程序提供了一个隔离的环境,确保它拥有正确运行所需的所有资源。每个容器在主机上作为单独的进程运行,并拥有自己的文件系统、网络和其他资源。
Docker 容器被设计为可移植且易于部署。它们可以在任何安装了 Docker 的机器上运行,无论底层操作系统或硬件如何。容器为运行应用程序提供了一致的环境,使得在不同环境(例如开发、测试和生产)之间移动应用程序更加舒适。
Docker 容器可以借助 Docker CLI 或 Docker Compose 或 Kubernetes 等 Docker 工具进行管理。它们可以根据需要启动、停止、暂停和重新启动。还可以使用一系列工具和平台对其进行监控和管理。
总体而言,Docker 容器提供了一种灵活且可扩展的方式来打包和部署应用程序,从而使跨不同环境和平台管理和扩展复杂应用程序变得更加简单。
6.Docker 镜像存储库
Docker 托管着最大的 Docker 存储库之一,称为 Docker 中心。 它是一个Docker镜像的存储和分发系统。它为开发人员和组织提供了一个中央存储库来共享和分发其 Docker 映像,从而使使用 Docker 构建、共享和部署应用程序变得更加愉快。
Docker Hub 允许用户和组织存储和管理其 Docker 映像,并提供版本控制、标记和协作等功能。用户可以从 Docker Hub 搜索和下载镜像,也可以将自己的镜像发布到注册中心。
除了公共注册表之外,Docker Hub 还为想要管理自己的 Docker 映像并确保它们只能由授权用户访问的组织提供私有注册表。
7.InterSystems Docker 镜像存储库
通过使用 Docker Hub 搜索功能,我们可以在 Docker hub 上找到 InterSystems 镜像。
8.安装Docker
为了使用 Docker,我们需要在我们的系统上安装它。 Docker提供了各种操作系统的安装包,包括Windows、macOS和Linux。导航到Docker 网站。 我们可以从Docker网站下载安装包,运行安装程序,按照提示完成安装。
安装Docker Desktop后,我们可以使用Docker CLI(命令行界面)来管理Docker镜像、容器、网络和其他资源。
9.Docker基本命令
这里我们将回顾一些 docker CLI 的基本命令。 (在使用下面详述的命令之前,请确保运行 Docker Desktop)
9.1 列出图像(本地)我们可以使用-----100----- 命令列出系统上可用的所有 Docker 镜像。以下是如何使用此命令:
docker image ls
如您所见,目前我们本地没有任何图像9.2 从 Docker 存储库中拉取镜像
我们可以使用-----101----- 命令从注册表下载Docker镜像
docker pull <image>
让我们从 docker hub 中提取 intersystemsdc/iris-community 镜像此时我们应该使用list命令来查看本地的图片做得好! iris-community镜像拉取成功
9.3 本地删除镜像我们可以使用-----102-----命令从我们的系统中删除镜像
docker image rm <image name>
9.4 列出所有现有容器(正在运行和未运行)我们可以使用-----103-----命令来列出正在运行的容器
docker ps
如图所示,此时没有容器在运行。
9.5 创建并启动容器我们可以使用-----104-----命令来创建并启动容器
docker run <image id/namge>
让我们从 iris-community 镜像创建并启动容器这里 -----105----- 或 -----106----- 的含义如下:在后台运行命令并将控制权返回给终端。
是时候再次列出正在运行的容器了
docker ps
我们可以看到我们的 iris-community 镜像容器现在正在运行。
9.6 停止特定容器我们可以使用-----107-----命令来停止正在运行的容器
docker stop <container id/name>
9.7 启动特定容器我们可以使用-----108----- 命令来启动Docker中之前停止的容器。
docker start <container id/name>
9.8 重启特定容器
我们可以使用-----109-----命令来停止和启动Docker中正在运行的容器
docker restart <container id/name>
9.9 删除特定容器
我们可以使用-----110-----命令来删除停止的容器
docker rm <container id/name>
9.10 在正在运行的容器内运行命令我们可以使用-----111----- 命令在正在运行的容器内运行命令。在执行管理任务或出于调试目的时,它可以派上用场。
docker exec -it my-container sh
-----111----- 命令的一些常见选项是:
-----113----- 或 -----114-----: 即使未连接,此命令也会使 STDIN 保持打开状态,从而允许您与容器交互。
-----115----- 或 -----116-----: 此命令为命令分配一个伪 TTY,允许您在容器内使用终端命令。
-----105----- 或 -----118-----: 这在后台运行命令并将控制权返回到终端。
10.使用docker运行IRIS社区版
使用下面列出的命令通过使用 iris-community 映像来运行容器
docker run -d -p 52773:52773 intersystemsdc/iris-community
-----111----- 命令的一些常见选项如下:
-----105----- :该命令用于以分离模式启动一个新的 Docker 容器,这意味着该容器将在后台运行,我们可以继续使用终端执行其他任务。
-----121-----: 这个命令帮助我们将容器的端口发布到主机,以便可以从Docker网络外部访问容器。
在下图中,您可以看到 IRIS 在 Docker 中运行。
11.Docker 桌面 GUI
Docker 桌面还具有 GUI,我们可以在其中以图形方式使用所有上述命令。
概括
Docker 是一个功能强大的工具,允许开发人员和 IT 团队在容器化环境中创建、部署和运行应用程序。通过提供可移植性、一致性、可扩展性、资源效率和安全性,Docker 可以轻松地跨不同环境和基础设施部署应用程序。随着容器化的日益普及,Docker正在成为现代软件开发和部署的重要工具。在接下来的文章中,我们将学习如何使用 Docker 文件(用于构建 Docker 镜像)、Docker compose(一个 YAML 文件,指定应用程序中每个容器的配置选项)和 Docker 卷(一种持久数据存储机制)用于在 Docker 容器和主机之间共享数据。)
感谢您的阅读! 希望IRIS可以模仿Docker对页面做一下暗黑处理👀 啥意思? 浏览器可以设置暗黑呀
文章
姚 鑫 · 二月 20, 2021
# 第四十二章 Caché 变量大全 $ZTIMESTAMP 变量
包含协调世界时间格式的当前日期和时间。
# 大纲
```java
$ZTIMESTAMP
$ZTS
```
# 描述
`$ZTIMESTAMP`包含协调的通用时间值形式的当前日期和时间。这是世界范围内的时间和日期标准;此值很可能与当地的时间(和日期)值不同。
`$ZTIMESTAMP`将日期和时间表示为以下格式的字符串:
```java
ddddd,sssss.fff
```
其中`ddddd`是一个整数,指定自1840年12月31日起的天数;`sssss`是一个整数,指定自当天午夜以来的秒数,`fff`是一个可变的数字,指定小数秒。这种格式类似于`$HOROLOG`,只是`$HOROLOG`不包含分数秒。
假设当前日期和时间(世界协调时)如下:
```java
2018-02-22 15:17:27.984
```
当时,`$ZTIMESTAMP`的值为:
```java
64701,55047.984
```
`$ZTIMESTAMP`报告协调世界时(UTC),它独立于时区。因此,`$ZTIMESTAMP`提供了一个跨时区的统一时间戳。这可能不同于本地时间值和本地日期值。
`$ZTIMESTAMP`时间值是一个十进制数值,以秒及其分数为单位计算时间。分数秒的位数可能从零到九不等,具体取决于计算机时钟的精度。在视窗系统上,小数精度是三位小数;在UNIX系统上,它是六位十进制数字。`$ZTIMESTAMP`在此小数部分中抑制尾随零或尾随小数点。请注意,在午夜后的第一秒内,秒表示为`0.fff`(例如,`0.123`);这个数字不是ObjectScript规范形式(例如,`. 123`),这会影响这些值的字符串排序顺序。在执行排序操作之前,您可以添加一个加号(`+`)来强制将数字转换为规范形式。
比较了返回当前日期和时间的各种方法,如下所示:。
- `$ZTIMESTAMP`包含以系统间IRIS存储(`$HOROLOG`)格式表示的UTC日期和时间(小数秒)。小数秒以三位精度(在Windows系统上)或六位精度(在UNIX®系统上)表示。
- `$NOW`返回当前进程的本地日期和时间;不应用本地时间变体(如夏令时)。不带参数值的`$NOW`根据`$ZTIMEZONE`特殊变量的值确定当地时区。带有参数值的`$NOW`返回与指定时区参数对应的时间和日期。`$NOW(0)`返回UTC日期和时间。忽略`$ZTIMEZONE`的值。`$now`返回InterSystems IRIS存储(`$HOROLOG`)格式的日期和时间。它包括小数秒;小数位数是当前操作系统支持的最大精度。因此,`$NOW(0)`返回的UTC时间可能比`$ZTIMESTAMP`返回的秒精度高
- `$HOROLOG`包含采用InterSystems IRIS存储格式的本地变量调整日期和时间。它不记录小数秒。`$HOROLOG`如何解析小数秒取决于操作系统平台:在Windows上,它将任何小数秒四舍五入到下一整秒。在UNIX®上,它会截断小数部分。
注意:比较当地时间和UTC时间时要谨慎:
- 将UTC时间转换为本地时间的首选方法是使用`$ZDATETIMEH(UTC,-3)`函数。此函数根据当地时间变量进行调整。
- 不能通过简单地添加或减去`$ZTIMEZONE*60`的值来转换本地时间和UTC时间。这是因为,在许多情况下,当地时间会根据当地时间的变化进行调整(例如夏令时,它会将当地时间季节性地调整一小时)。这些本地时间变量不会反映在`$ZTIMEZONE`中。
- UTC时间是使用格林威治子午线上的时区计数来计算的。这和格林威治当地时间不一样。术语格林威治标准时间(GMT)可能会令人混淆;格林威治当地时间在冬季与UTC相同;在夏季,它与UTC相差一个小时。这是因为采用了当地时间变量,即英国夏令时(British Summer Time)。
- 时区与UTC和本地时间的偏差(例如季节转换为夏令时)都会影响日期和时间。从本地时间转换为UTC时间(反之亦然)可能会更改日期和时间。
不能使用`SET`命令修改此特殊变量。尝试这样做会导致``错误。
# 协调世界时转换
可以使用带有tFormat值7或8的`$ZDATETIME`和`$ZDATETIMEH`函数将本地时间信息表示为协调世界时(UTC),如下例所示:
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP()
ClassMethod ZTIMESTAMP()
{
WRITE !,$ZDATETIME($ZTIMESTAMP,1,1,2)
WRITE !,$ZDATETIME($HOROLOG,1,7,2)
WRITE !,$ZDATETIME($HOROLOG,1,8,2)
WRITE !,$ZDATETIME($NOW(),1,7,2)
WRITE !,$ZDATETIME($NOW(),1,8,2)
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP()
02/10/2021 09:46:32.53
02/10/2021T09:46:32.00Z
02/10/2021T09:46Z
02/10/2021T09:46:32.53Z
02/10/2021T09:46Z
```
上面的`$ZDATETIME`函数都以协调世界时(而不是本地时间)的形式返回当前时间。这些从本地时间转换的时间值可能不同,因为`$Now`不会针对本地时间变量进行调整;`$ZTIMESTAMP`和`$HOROLOG`会针对本地时间变量进行调整,并可能在必要时相应地调整日期。`$ZTIMESTAMP`显示值与tFormat 7或8转换后的显示值不同。Tformat值7和8在时间值之前插入字母`“T”`,在时间值之后插入字母`“Z”`。此外,因为`$HOROLOG` TIME不包含小数秒,所以上例中精度为2的小数位用零填充。
通过使用以下语法形式之一调用`Timestamp()`类方法,可以获得与`$ZTIMESTAMP`相同的时间戳信息:
```java
DHC-APP> WRITE !,$SYSTEM.SYS.TimeStamp()
65785,35395.629
DHC-APP> WRITE !,##class(%SYSTEM.SYS).TimeStamp()
65785,35408.245
```
# 示例
下面的示例将`$ZTIMESTAMP`的值转换为本地时间,并将其与本地时间的两种表示形式进行比较:`$NOW()`和`$HOROLOG`:
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP1()
ClassMethod ZTIMESTAMP1()
{
SET stamp=$ZTIMESTAMP,clock=$HOROLOG,miliclock=$NOW()
WRITE !,"当地日期和时间: ",$ZDATETIME(clock,1,1,2)
WRITE !,"当地日期和时间: ",$ZDATETIME(miliclock,1,1,2)
WRITE !,"UTC日期和时间: ",$ZDATETIME(stamp,1,1,2)
IF $PIECE(stamp,",",2) = $PIECE(clock,",",2) {
WRITE !,"当地时间为UTC时间" }
ELSEIF $PIECE(stamp,",") '= $PIECE(clock,",") {
WRITE !,"时差影响日期" }
ELSE {
SET localutc=$ZDATETIMEH(stamp,-3)
WRITE !,"UTC转换为本地: ",$ZDATETIME(localutc,1,1,2)
}
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP1()
当地日期和时间: 02/10/2021 17:54:46.00
当地日期和时间: 02/10/2021 17:54:46.93
UTC日期和时间: 02/10/2021 09:54:46.93
UTC转换为本地: 02/10/2021 17:54:46.93
```
下面的示例比较了`$ZTIMESTAMP`和`$HOROLOG`返回的值,并显示了如何转换`$ZTIMESTAMP`的时间部分。(请注意,在此简单示例中,只针对本地时间变化(如夏令时)进行了一次调整。其他类型的局部变化可能会导致时钟秒和戳秒包含不可调和的值。)
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP2()
ClassMethod ZTIMESTAMP2()
{
SET stamp=$ZTIMESTAMP,clock=$HOROLOG
WRITE !,"当地日期和时间: ",$ZDATETIME(clock,1,1,2)
WRITE !,"UTC日期和时间: ",$ZDATETIME(stamp,1,1,2)
IF $PIECE(stamp,",") '= $PIECE(clock,",") {
WRITE !,"时差影响日期" }
SET clocksecs=$EXTRACT(clock,7,11)
SET stampsecs=$EXTRACT(stamp,7,11)-($ZTIMEZONE*60)
IF clocksecs=stampsecs {
WRITE !,"没有本地时间变量"
WRITE !,"本地时间是时区时间" }
ELSE {
SET stampsecs=stampsecs+3600
IF clocksecs=stampsecs {
WRITE !,"夏令时变量:"
WRITE !,"当地时间与时区时间相差1小时" }
ELSE {
WRITE !,"由于当地时间不同,无法协调"
}
}
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP2()
当地日期和时间: 02/10/2021 17:58:16.00
UTC日期和时间: 02/10/2021 09:58:16.85
没有本地时间变量
本地时间是时区时间
```
文章
Jingwei Wang · 七月 11, 2022
InterSystems DeepSee的目的是使你能够将BI嵌入到你的应用程序中,这样你的用户就可以对他们的数据提出和回答复杂的问题。你的应用程序可以包括仪表盘,它包含图形部件。这些部件用来显示数据,由透视表和KPIs(关键绩效指标)驱动。对于一个透视表,用户可以显示一个列表,用其显示源值。
透视表、KPIs和列表是查询,在运行时执行。
数据透视表可以对运行时的输入作出反应,如用户的过滤器选择。在内部,它使用一个MDX(MultiDimensional eXpressions)查询,与DeepSee cube进行通信。一个cube由一个事实表和其索引组成。一个事实表由一组事实(行)组成,每个事实对应于一个基本记录。例如,这些事实可以代表病人或部门。DeepSee还生成了一组维度表(level tables)。所有的表都是动态维护的,根据你的配置和实现,DeepSee检测你的事务表的变化,并传播到事实表。当用户在分析器中创建透视表时,DeepSee会自动生成一个MDX查询。
KPI也可以对运行时的用户输入做出反应。在内部,它使用MDX查询(与DeepSee立方体)或SQL查询(与任何表)。在这两种情况下,你都可以手动创建查询,或从其他地方复制它。
列表显示来自用户选择的透视表行的源记录的选定值。在内部,一个列表是一个SQL查询。你可以指定要使用的字段,让DeepSee生成实际的查询。或者你可以指定整个查询。
仪表盘可以包括启动行动的按钮和其他控件。可以使用操作、设置过滤器、刷新仪表盘、打开其他仪表盘或其他URL,运行自定义代码,等等。DeepSee提供了一套标准行为,你也可以定义自定义。
DeepSee组件
要把DeepSee添加到一个应用程序中,你要添加以下一些或全部的组件。
数据连接器类(Data connector):数据连接器使你能够使用一个任意的SQL查询作为立方体或列表的来源。
cube定义类(Cube definition)一个cube定义了DeepSee透视表内使用的元素,并控制相应的事实表和索引的结构和内容。
一个cube定义指向作为它的基础使用的事务类(或数据连接器类)。
你可以有任何数量的cubes,而且你可以使用一个给定的类作为多个cubes的基础。
对于每个cube,DeepSee会生成并填充一个事实表类和其他类.
主题区类(Subject area ) : 一个主题区主要是一个过滤的cube。(它包括一个过滤器和cube体定义的不同部分的重写,如需要)。你可以在DeepSee中交替使用cube和主题区。
KPI定义类 : 当你需要自定义查询时,你会定义KPI,特别是在运行时根据用户输入确定的查询。当你需要自定义操作时,你也定义KPI,因为操作包含在KPI类中。
透视表 : 你通过拖放来创建。DeepSee会生成基础的MDX查询。
仪表盘 : 通过运行基础查询和显示结果来显示透视表和KPI。
用户门户 : 显示透视表和仪表盘。
基于高可用的推荐架构
对于任何大规模的应用,InterSystems建议你将DeepSee cube 建立在镜像服务器上的应用数据上,如下图所示。设置镜像,使应用程序的数据被镜像到镜像服务器上。在镜像服务器上,创建一个数据库,包含DeepSee立方体定义和(可选)数据。这样DeepSee就可以访问应用数据。但是,对于小规模的应用程序或演示,所有的代码和数据都可以在同一个数据库中。
主要实施步骤 - 此步骤会在之后的文章详细介绍
主要实施步骤
实施过程包括以下步骤。
建立web 应用。
从其他数据库中映射DeepSee的globals,以获得性能(可选择的,非必要步骤)。
创建cube和主题区域。这个过程包括以下步骤,你可以根据需要反复进行。
定义一个或多个cubes。在这个步骤中,你可以使用DeepSee Architect或者Studio。
建立cube。你可以使用Architect或终端。
使用DeepSee分析器来查看cubes并验证它们。
在定义好cube后,在这些cube的基础上定义任何主题区域。
创建KPIs(可选择的,非必要步骤)。
创建自定义行为(可选择的,非必要步骤)。
根据需要进行修改,以保持cubes的有效性。这样做的目的取决于数据必须是最新的,以及任何性能考虑。
创建透视表和仪表盘。
将透视表和仪表盘打包成类,以方便部署。
创建从你的应用程序到仪表盘的链接。
在这个过程中,你可能还需要做以下工作。
创建数据连接器。
配置设置。
执行本地化。
在仪表盘中使用自定义小程序。
执行其他开发任务。
安全设置。
实施工具
在实施过程中你会用到以下工具。
从管理门户的DeepSee部分提供的工具。
模型(Architech): 用来定义cube和主题区域。也可以编译cube和编译主题区域。
分析器(Analyzer) - 在验证你的模型时,使用它来检查立方体和主题区。后来你用它来创建透视表。
用户门户 :用它来定义仪表盘。
查询工具 : 使用它来创建MDX查询并查看其查询计划。
文件夹管理器 - 主要用于导出透视表和仪表盘,这样你就可以在类中打包它们的定义,你也可以用它来将资源与文件夹联系起来。
设置选项 : 使用它来指定用户门户的外观和行为,并定义可用于仪表盘的变量。
DeepSee日志 : 使用这个来查看这个命名空间的DeepSee编译日志。
Studio - 使用它来定义高级cube功能,cube元素使用的任何方法或例程,以及cube类中的任何回调方法。你还可以用它来定义KPI。
终端 - 可以用它来重建立方体和测试方法。
MDX shell(在终端运行)- 可以用它来检查cube和主题区域,创建自定义的MDX查询并查看其结果。
管理门户的其他部分 - 使用这些来做global映射,定义资源、角色和用户,以便与DeepSee一起使用,并在需要时检查DeepSee事实表。
Utility 方法 - %DeepSee.Utils包括一些方法,可以用来建立cube,同步cube,清除单元缓存,以及其他任务; %DeepSee.UserLibrary.Utils包括一些方法,可以用来以编程方式执行文件夹管理器中支持的任务。
数据连接器类(%DeepSee.DataConnector)- 使用它可以使任意的SQL查询在DeepSee立方体和列表中使用。
结果集API(%DeepSee.ResultSet)- 使用它来编程执行MDX查询并访问结果。
文章
聆严 周 · 九月 27, 2022
# 背景
Cache起源于没有SQL的1970时代,当时各种高级计算机语言才刚刚诞生,其中M语言较为独特,它的诞生就是为了在没有操作系统的机器上,进行数据存储。别忘了,Unix在1971年才发布。M语言别具一格地采用了Global多维数组,统一了复杂的内存操作和文件读写,使之成为了1970年代数据库的事实标准,特别是在医疗行业。而后Intersystems在1978年接过M语言的旗帜,在M语言上添加了SQL兼容层和ObjectScript层,前者顺应了时代的潮流,后者不仅为M语言提供了强大的OOP和各种便捷的语法糖,还让数据能以对象形式进行访问,让数据和代码更加紧密。
本文将简述多维数组、SQL、对象这3种数据操作方式,提供实例代码片段,并在运行效率、开发效率、管理效率、实用性方面讨论它们的优缺点。
为方便讨论,以学校与学生为例。对每种操作方法,都列举3种典型的用例,分别为,访问某特定ID的学生(即数据库ID索引)、访问某特定studentID的学生(即遍历唯一索引)、和访问某学校的所有人(即遍历非唯一索引)。
现假设学生表/对象定义如下:
```java
Class Student Extends %Persistent
{
Property schoolId AS %String;
Property studentId As %String;
Property name As %String;
Index IdxOnSchoolId ON schoolId ;
Index IdxOnStudentId ON studentId [Unique];
Storage Default
{
%%CLASSNAME
schoolId
studentId
name
^StudentD
StudentDefaultData
^StudentD
^StudentI
^StudentS
%Library.CacheStorage
}
}
```
# 方法1 多维数组
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
s id = $ORDER(^StudentI("IdxOnStudentId",studentId,""))
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
}
```
> `$ORDER` 方法返回多维数组最末端下标的下一个值。用来遍历多维数组。
# 方法2 SQL
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
&sql(SELECT name INTO :name from Student where id=:id)
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
&sql(SELECT name INTO :name from Student where studentId=:studentId)
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s query="SELECT name from Student where schoolId=?"
s statement=##class(%SQL.Statement).%New()
s sc=statement.%Prepare(query)
s rset=statement.%Execute(schoolId)
while (rset.%Next()) {
s name = rset.%Get("name")
w name,!
}
```
> - `&sql()`为嵌入式SQL语句,在`INTO`子句中赋值给变量,适合单行查询。
> - `&sql()`也可以返回游标Cursor,以实现多多行查询,但效率比`SQL.Statement`低,不推荐使用。
> - `SQL.Statement`类实现动态SQL语句,动态查询适合返回多行结果。
# 方法3 对象
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
s student = ##class(Student).IdxOnStudentIdOpen(studentId)
s name = student.name
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
}
```
> - `%OpenId`方法通过ID查找并返回对象。
> - `IndexOpen`方法通过唯一索引值查找并返回对象。注意,非唯一索引没有类似方法。
# 讨论
* 多维数组
* 运行效率: 高。
* 可控程度高,只要有老练的程序员,有足够的加班时间,有足够的资金和时间,总能打磨出最好的效率。据说多维数组的效率是SQL的10倍。
* 面向过程编程,能够实现SQL难以实现的逻辑控制。
* 注意,事实上,未经优化的多维数组操作未必比SQL效率高。
* 开发效率: 低。
* 虽然对于简单的数据操作,利用多维数组也能快速实现。但是一旦数组结构、索引、下标达到一定数量级,直接对为数组操作是个噩梦。代码中将充斥数组名,索引名,下标等magic values。
* 直接操作数组太过底层,数据校验、初始值、空置、锁管理、事务等都需要人工编码。
* 管理效率:低。
* 值和索引必须同时维护,稍有不慎,容易造成索引损坏。
* 不同熟练度的程序员实现可能千差万别,对锁的使用、回调函数的调用等容易产生分歧,统一化难度大。
* 一旦数据定义发生变化,或者数据分布发生变化,需要调整或者调优,都需要较大人力投入。
* 数据提取、数据迁移、数据备份等日常操作,都须要程序员参与。
* 实用性:高
* 对临时数据、不需要考虑数据提取的数据,多维数组是很好的Key-Value数据库。
* SQL
* 运行效率: 中。
* SQL解析和优化需要耗费额外时间。
* 适合批量处理。
* 不适合面向过程的逻辑。
* 可控程度低,如果不使用Frozen Plan,实际执行策略变化大,造成系统不稳定的假象。
* 但是经过调优后的SQL,可以实现较好的执行效率。
* 开发效率: 高。
* SQL提供隔离等级、事务、锁表等指令,简化了并发。
* SQL是声明式语言,简洁明了,可读性高,使程序员更关注结果,而不是遍历各种索引的过程。
* 管理效率:高。
* SQL提供了数据定义、数据查询、数据更新等的统一化。
* 数据提取、数据迁移、数据备份可以通过标准SQL客户端。
* 自适应性高,对存储的变化,例如变更索引,变更数据分布等,都能自动适应。
* Intersystems为SQL提供了额外的权限配置。
* 实用性:高
* 对象
* 运行效率: 低。
* 不支持对索引的遍历,只能通过主索引和唯一索引访问单一对象。
* 开发效率: 中。
* 使对列的访问转化成了对对象属性的访问,使对外键的访问转化成了对外键对象的访问,代码的语义性强,可读性高。
* 管理效率:中。
* 在锁管理、值校验等方面统一化程度比多维数组高。
* 和多维数组一样,也无法提供标准客户端来访问数据。
* 实用性: 中
* 实际应用中,持久类除了单个对象内字段的校验逻辑,几乎不包含业务逻辑。一是因为持久类必须稳定,一旦编译,要尽量避免再次编译。二是因为实际项目中,业务逻辑在业务层中,数据是相互依存的,例如退费数据需要退费审核,而这样的逻辑,不可能在某个数据对象中存在,只能在数据层之上的业务层才合理。
# Do's & Don'ts
- 批量的读写操作多用SQL。
- 写操作应尽量用SQL或者对象。
- 多维数组应尽量只用于读操作。
- 多维数组的读、写操作应封装在方法中。 Nice work 讲述的很清楚,值得学习 深度好文
文章
姚 鑫 · 三月 28, 2021
# 第十三章 使用动态SQL(六)
### 用`%ObjectSelectMode = 1` Swizzling字段名称属性
下面的示例使用`%ObjectSelectMode = 1`进行准备,当使用字段名称属性返回值时,其类型类别为可Swizzle类型的字段(持久性类,序列类或流类)将自动发生Swizzle。转换字段值的结果是相应的对象参考(oref)。使用`%Get()`或`%GetData()`方法访问字段时,InterSystems IRIS不会执行此筛选操作。在此示例中,`rset.Home`处于Swizzle状态,而引用同一字段的`rset.%GetData(2)`处于not swizzled状态:
```java
/// d ##class(PHA.TEST.SQL).PropSQL2()
ClassMethod PropSQL2()
{
SET myquery = "SELECT TOP 5 Name,Home FROM Sample.Person"
SET tStatement = ##class(%SQL.Statement).%New(0)
SET tStatement.%ObjectSelectMode=1
WRITE !,"set ObjectSelectMode=",tStatement.%ObjectSelectMode,!
SET qStatus = tStatement.%Prepare(myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE "Row count ",rset.%ROWCOUNT,!
WRITE rset.Name
WRITE " ",rset.Home,!
WRITE rset.%GetData(1)
WRITE " ",$LISTTOSTRING(rset.%GetData(2)),!!
}
WRITE !,"End of data"
WRITE !,"Total row count=",rset.%ROWCOUNT
}
```
```java
DHC-APP> d ##class(PHA.TEST.SQL).PropSQL2()
set ObjectSelectMode=1
Row count 1
yaoxin 5@Sample.Address
yaoxin 889 Clinton Drive,St Louis,WI,78672
Row count 2
xiaoli 5@Sample.Address
xiaoli
Row count 3
姚鑫 5@Sample.Address
姚鑫
Row count 4
姚鑫 5@Sample.Address
姚鑫
```
下面的示例使用`%ObjectSelectMode = 1`从唯一记录ID(`%ID`)导出所选记录的`Home_State`值。请注意,在原始查询中未选择`Home_State`字段:
```java
/// d ##class(PHA.TEST.SQL).PropSQL2()
ClassMethod PropSQL2()
{
SET myquery = "SELECT TOP 5 %ID AS MyID,Name,Age FROM Sample.Person"
SET tStatement = ##class(%SQL.Statement).%New()
SET tStatement.%ObjectSelectMode=1
SET qStatus = tStatement.%Prepare(myquery)
IF qStatus'=1 {WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE rset.Name
WRITE " Home State:",rset.MyID.Home.State,!
}
WRITE !,"End of data"
WRITE !,"Total row count=",rset.%ROWCOUNT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).PropSQL2()
yaoxin Home State:WI
xiaoli Home State:
姚鑫 Home State:
姚鑫 Home State:
姚鑫 Home State:
End of data
Total row count=5
```
如果已配置,则如果配置了swizzled属性,但系统无法生成引用,则系统会生成``错误。如果引用的属性已从磁盘中意外删除或被另一个进程锁定,则会发生这种情况。要确定SWIZZLE失败的原因,请在``错误之后立即在`%objlasterror`中查找并解码此`%Status`值。
默认情况下,未配置``。可以通过设置`SET ^%SYS("ThrowSwizzleError")=1`或使用InterSystems IRIS管理门户来全局设置此行为。在“系统管理”中,选择“配置”,然后选择“ SQL和对象设置”,然后选择“对象”。在此屏幕上,可以设置``选项。
## %Get("fieldname")方法
可以使用`%Get(“ fieldname”)`实例方法按字段名称或字段名称别名返回数据值。 Dynamic SQL根据需要解析字母大小写。如果指定的字段名称或字段名称别名不存在,系统将生成``错误。
下面的示例从查询结果集中返回`Home_State`字段和`Last_Name`别名的值。
```java
/// d ##class(PHA.TEST.SQL).PropSQL4()
ClassMethod PropSQL4()
{
SET myquery = "SELECT TOP 5 Home_State,Name AS Last_Name FROM Sample.Person"
SET tStatement = ##class(%SQL.Statement).%New(2)
SET qStatus = tStatement.%Prepare(myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE rset.%Get("Home_State")," : ",rset.%Get("Last_Name"),!
}
WRITE !,"End of data"
WRITE !,"Total row count=",rset.%ROWCOUNT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).PropSQL4()
WI : yaoxin
: xiaoli
: 姚鑫
: 姚鑫
: 姚鑫
End of data
Total row count=5
```
必须使用`%Get("fieldname")`实例方法从使用`%PrepareClassQuery()`准备的现有查询中按字段属性名称检索单个数据项。如果字段属性名称不存在,则系统会生成``错误。
下面的示例从内置查询中按字段属性名称返回Nsp(命名空间)字段值。因为此查询是现有的存储查询,所以此字段检索需要使用`%Get("fieldname") `方法。请注意,由于`“Nsp”`是属性名称,因此区分大小写:
```java
/// d ##class(PHA.TEST.SQL).PropSQL5()
ClassMethod PropSQL5()
{
SET tStatement = ##class(%SQL.Statement).%New(2)
SET qStatus = tStatement.%PrepareClassQuery("%SYS.Namespace","List")
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE "Namespace: ",rset.%Get("Nsp"),!
}
WRITE !,"End of data"
WRITE !,"Total row count=",rset.%ROWCOUNT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).PropSQL5()
Namespace: %SYS
Namespace: DHC-APP
Namespace: DHC-CHSSWEB
Namespace: DHC-CSM
Namespace: DHC-DATA
Namespace: DHC-DWR
Namespace: DHC-EKG
Namespace: DHC-HEIS
Namespace: DHC-HR
Namespace: DHC-LISDATA
Namespace: DHC-LISSRC
Namespace: DHC-MEDSRC
Namespace: DHC-MRQ
Namespace: DOCBOOK
Namespace: FDBMS
Namespace: PACS
Namespace: PIS
Namespace: RIS
Namespace: SAMPLES
Namespace: USER
End of data
Total row count=20
```
重复名称:如果名称解析为相同的属性名称,则它们是重复的。重复名称可以是对同一字段的多个引用,对表中不同字段的引用或对不同表中字段的引用。如果`SELECT`语句包含相同字段名称或字段名称别名的多个实例,则`%Get(“fieldname”)`始终返回查询中指定的重复名称的最后一个实例。这与`rset.PropName`相反,后者返回查询中指定的重复名称的第一个实例。在下面的示例中显示:
```java
/// d ##class(PHA.TEST.SQL).PropSQL6()
ClassMethod PropSQL6()
{
SET myquery = "SELECT c.Name,p.Name FROM Sample.Person AS p,Sample.Company AS c"
SET tStatement = ##class(%SQL.Statement).%New()
SET qStatus = tStatement.%Prepare(myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE "Prop=",rset.Name," Get=",rset.%Get("Name"),!
}
WRITE !,rset.%ROWCOUNT," End of data"
}
```
## %GetData(n)方法
`%GetData(n)`实例方法返回由结果集的整数计数列号索引的当前行的数据您可以将`%GetData(n)`与使用`%Prepare()`准备的指定查询或使用`%PrepareClassQuery()`准备的存储查询一起使用。
使用`%PrepareClassQuery()`准备。
整数n对应于查询中指定的选择项列表的序列。除非在选择项列表中明确指定,否则不会为`RowID`字段提供整数n值。如果n大于查询中的选择项数,或者为0,或者为负数,则Dynamic SQL不返回任何值,也不发出错误。
`%GetData(n)`是返回特定重复字段名称或重复别名的唯一方法; `rset.Name`返回第一个重复项,`%Get(“Name”)`返回最后一个重复项。
```java
/// d ##class(PHA.TEST.SQL).PropSQL7()
ClassMethod PropSQL7()
{
SET myquery="SELECT TOP 5 Name,SSN,Age FROM Sample.Person"
SET tStatement = ##class(%SQL.Statement).%New()
SET qStatus = tStatement.%Prepare(myquery)
IF qStatus'=1 {
WRITE "%Prepare failed:" DO $System.Status.DisplayError(qStatus) QUIT
}
SET rset = tStatement.%Execute()
WHILE rset.%Next() {
WRITE "Years:",rset.%GetData(3)," Name:",rset.%GetData(1),!
}
WRITE "End of data"
WRITE !,"Total row count=",rset.%ROWCOUNT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SQL).PropSQL7()
Years:30 Name:yaoxin
Years: Name:xiaoli
Years:7 Name:姚鑫
Years:7 Name:姚鑫
Years:43 Name:姚鑫
End of data
Total row count=5
```
# 返回多个结果集
`CALL`语句可以将多个动态结果集作为一个集合返回,称为结果集序列(RSS)。
下面的示例使用`%NextResult()`方法分别返回多个结果集:
```java
/// d ##class(PHA.TEST.SQL).PropSQL8()
ClassMethod PropSQL8()
{
SET mycall = "CALL Sample.CustomSets()"
SET rset = ##class(%SQL.Statement).%ExecDirect(,mycall)
IF rset.%SQLCODE'=0 {
WRITE !,"ExecDirect SQLCODE=",rset.%SQLCODE,!,rset.%Message QUIT
}
SET rset1=rset.%NextResult()
DO rset1.%Display()
WRITE !,"End of 1st Result Set data",!!
SET rset2=rset.%NextResult()
DO rset2.%Display()
WRITE !,"End of 2nd Result Set data"
}
```
```java
DHC-APP> d ##class(PHA.TEST.SQL).PropSQL8()
ID Name Street City State Spouse
3 Davis,Robert I. 4177 Franklin Court Fargo WY 86
2 Hanson,Roberta O. 9840 Ash Drive Boston KS 155
4 Huff,Olga M. 1902 Franklin Avenue Vail DE 150
1 Woo,Jocelyn A. 9932 Clinton Avenue Queensbury NM 14
5 Zubik,George T. 8102 First Drive Denver VA 110
5 Rows(s) Affected
End of 1st Result Set data
ID Name Street City State Spouse
5 Campos,Alvin N. 1847 Franklin Drive Ukiah WY 206
1 Fripp,Kristen A. 1487 Ash Place Islip NC 133
3 Jafari,Christen K. 7384 Washington Court Newton CO 168
4 Kratzmann,Mark V. 9573 Second Blvd Chicago OR 43
2 O'Donnell,George H. 3413 Main Drive Newton RI 143
7 Ravazzolo,Danielle Y. 2898 Clinton Blvd Tampa HI 133
10 Rodriguez,Sophia U. 4766 Clinton Avenue Ukiah AR 202
6 Sverdlov,Phyllis J. 5010 Oak Place Fargo VT 214
8 Uhles,Andrew O. 4931 Madison Street Bensonhurst IA 129
9 Xerxes,Mo C. 49 Main Drive Vail CA 151
10 Rows(s) Affected
End of 2nd Result Set data
```