在快学(一),我们讲了比较常用的一些SQL查询方式,本章的目标:
1.巩固基础知识
2.延展特性
我建议每一位在看的朋友,一定要把快学(一)看一下,当然可以先看完快学(二)再回头看快学(一)也是不耽误的。
public class Person {
private static final AtomicLong ID_GEN = new AtomicLong();
@QuerySqlField(index = true)
public Long id;
@QuerySqlField(index = true)
public Long companyId;
@QuerySqlField
public String firstName;
@QuerySqlField
public String lastName;
@QuerySqlField
public String resume;
@QuerySqlField(index = true)
public double salary;
private transient AffinityKey key;
public Person() {
}
}
CacheConfiguration personConfig = new CacheConfiguration("CACHE_ONLY_PERSON");
personConfig.setIndexedTypes(Long.class,Person.class);
try (IgniteCache cacheOnlyPerson = ignite.getOrCreateCache(personConfig)) {
...
}
在上述的Person类中,我们通过Ignite的注解,实现了类到表(缓存中的)的一种关联映射,并且通过@QuerySqlField(index = true)设置了索引,这里需要注意的是@QuerySqlField这样的注解仅仅可以使得该属性是Ignite可见的,不带盖注解的属性,Ignite是不可见的。如果想设置为索引项,那么就要像示例中,加上index=true属性。
我们这里只是做了索引与可见属性的标识,下面还需要进行索引注册。
Ignite的索引注册比较特别,我们来看第二段代码。我们调用了CacheConfiguration#setIndexedTypes(...).该方法的入参是一个可变类型,我们可以传递多个class对象进去。但是官文明确要求,这个参数的数量必须是2的倍数,因为奇数作为键的class对象类型,而偶数则是相应的值的class对象类型。比如说我们的测试用例中,我们在该缓存中缓存的只能是Long类型的键和Person类型的值,那么我们就按照实例这样填写。通过这种注册,Ignite即知道了要为Person设置索引。
在这里引申一个组索引的概念:当查询条件复杂时可以使用多字段索引来加快查询的速度,这时可以用@QuerySqlField.Group注解。如果希望一个字段参与多个分组索引时也可以将多个@QuerySqlField.Group注解加入orderedGroups/groups中。
我下面分别进行演示
public class Person {
@QuerySqlField(index = true)
public Long id;
@QuerySqlField(index = true)
public Long companyId;
@QuerySqlField(groups= {"A"})
public String firstName;
@QuerySqlField(groups= {"A"})
public String lastName;
@QuerySqlField(orderedGroups={@QuerySqlField.Group(name = "B", order = 0)})
public String resume;
@QuerySqlField(orderedGroups={@QuerySqlField.Group(name = "B", order = 1, descending = true)})
public double salary;
}
上述用例展示的就是分组索引,我们一共做了两种分组方式,一种是以group的形式,另外就是以orderFroups形式。区别还是很大的,我从头开始讲:
@QuerySqlField是以个注解,维护着String[]形式的groups属性个其内部类Group[]的orderGroups,这两个都是进行分组索引的,我先分别讲解功能,然后再剖析其区别:
1.groups:这种形式的分组,可以将多个字段,联合成一个组索引,我们在上述代码中是以A形式命名的组索引,这样,lastname和firstname都会被联合成一个组索引,类似于(firstname asc,lastname,asc).我们设想一个这样的情景:select * from Person where Person.firstname = ? and Person.lastname = ?。如果按照我们之前的方式,即单个属性维持自己的索引,也就是@QuerySqlField(index=true)的话,我们为属性firstname和lastname都加上这个注解,效率还是没有我们现在的这种分组形式快!!!为什么呢??因为在Ignite中,SQL引擎只能在查询中每表出现一个索引。所以,即使你在每个属性上都加了索引,SQL引擎只会选取一个索引来加速我们的SQL操作。因此组索引就派上用场了。在复杂查询时候,组索引可以显著改善我们的查询速度。
groups属性后跟的是一个数组,因为该属性是String[],所以只能写一个name值,同一组的,名字要相同,你如果要为一个属性设置多个多个组,那就写多个值
2.orderGroups:这种形式的分组,与上述基本类似,唯一区别则是,orderGroup做为注解@QuerySqlField的一个属性,其类型签名是其内部类Group形式的,它是由自己的属性的。我们来看我们的样例中的注释,即可弄明白。我着重讲一下其属性,
①name:同上,就是分组的名字,相同组内的要用一致的名字。
②descending:是否降序排序,默认是false,即asc,设置为true的话就是desc了,那么我们可以预见到我们的索引B是长的这个样子(salary desc,resume asc).这样的好处是但我们在SQL:select * from Person order by salary desc,resume asc中需要做排序操作时候,而且还是逆序排序,那么这种方式可以加快速度,因为我们的索引已经排好序了,但是如果使用上述的groups形式,它只能产生类似于(salary asc,resume asc)的索引,效率还是低于我们以orderGroups形式产生的索引的。
③order:这个值必须设置,定义组中该字段的排序顺序。参考在descending讲的,为什么salary会在前面,以及我写的SQL中排序的先后顺序。
PS:注意啊各位:
在@QuerySqlField(orderedGroups={...})之外使用@QuerySqlField.Group注释字段,是无效的。所以,按照我写的这样继续拓展即可。
当为您的ignite应用程序选择索引时,您应该考虑多种因素。
1.索引不是免费的。它们消耗内存,而且每个索引都需要分别更新,因此当设置更多索引时,缓存更新性能可能会更差。
最重要的是,优化器可能会选择错误的索引来运行查询,从而犯更多错误。
2.索引只是排序的数据结构。如果你为字段(a、b、c)定义一个索引,那么记录将首先被a排序,然后是b,然后是c。
PS:排序索引的例子
| A | B | C |
| 1 | 2 | 3 |
| 1 | 4 | 2 |
| 1 | 4 | 4 |
| 2 | 3 | 5 |
| 2 | 4 | 4 |
| 2 | 4 | 5 |
任意条件,比如a = 1 and b > 3,都会被视为有界范围,在log(N)时间内两个边界在索引中可以被快速检索到,
然后结果就是两者之间的任何数据。
下面的条件会使用索引:
a = ?
a = ? and b = ?
a = ? and b = ? and c = ?
从索引的角度,条件a = ?和c = ?不会好于a = ?
明显地,半界范围a > ?可以工作得很好。
3.单个字段上的索引在以相同字段开始的多个字段上的组索引不比组索引好(a)的索引与(a、b、c)相同。因此,最好使用组索引。
what's DML??
MERGE UPDATE DELETE INSERT
public class Person {
private static final AtomicLong ID_GEN = new AtomicLong();
@QuerySqlField(index = true)
public Long id;
@QuerySqlField(index = true)
public Long companyId;
@QuerySqlField(groups= {"A","single"})
public String firstName;
@QuerySqlField(groups= {"A"})
public String lastName;
@QuerySqlField(orderedGroups={@QuerySqlField.Group(name = "B", order = 0, descending = true)})
public String resume;
@QuerySqlField(orderedGroups={@QuerySqlField.Group(name = "B", order = 0, descending = true)})
public double salary;
private transient AffinityKey key;
}
这是我们将会使用的POJO,一定注意它的合格注解,我们一共注解了6个属性,而最后的并置键没有注解,因为我们测试场景不使用它。
public static void main(String[] args) {
try (Ignite ignite = Ignition.start("examples/config/example-ignite.xml")) {
CacheConfiguration companyConfig = new CacheConfiguration(
"CACHE_ONLY_COMPANY");
companyConfig.setIndexedTypes(Long.class, Company.class);
CacheConfiguration mergeConfig = new CacheConfiguration(
"CACHE_MERGE_PERSON");
mergeConfig.setIndexedTypes(Long.class, Person.class);
CacheConfiguration personConfig = new CacheConfiguration("CACHE_ONLY_PERSON");
personConfig.setIndexedTypes(Long.class,Person.class);
try (IgniteCache cacheOnlyCompany = ignite.getOrCreateCache(companyConfig);
IgniteCache cacheOnlyPerson = ignite.getOrCreateCache(personConfig);
IgniteCache cacheMergePerson = ignite.getOrCreateCache(mergeConfig)) {
cacheOnlyCompany.put(1L, new Company("ultrapower"));
// DML INSERT(1)
cacheOnlyPerson.query(new SqlFieldsQuery("INSERT INTO Person(_key,_val) VALUES(?,?)").setArgs(1L,new Person(1L,1L,"piemon","jax",20000D,"Good")));
// DML INSERT(2)
cacheOnlyPerson.query(new SqlFieldsQuery("INSERT INTO Person(_key,id,companyId,firstName,lastname,salary,resume) VALUES(?,?,?,?,?,?,?)").setArgs(11L,11L,1L,"piemon","jax",20000D,"Bad"));
//DML SELECT
QueryCursor> person = cacheOnlyPerson.query(new SqlQuery(Person.class,"SELECT * FROM Person where Person.resume = ?").setArgs("Bad"));
System.out.println(person.getAll().get(0));
System.out.println("update ::::");
//DML UPDATE
FieldsQueryCursor> update = cacheOnlyPerson.query(new SqlFieldsQuery("update Person set firstname = ? where _key= ?").setArgs("anokata",1L));
System.out.println("update影响行数:" + update.getAll().get(0));
System.out.println("delete :::");
//DML DELETE
FieldsQueryCursor> delete = cacheOnlyPerson.query(new SqlFieldsQuery("DELETE FROM Person where _key= ?").setArgs(1L));
System.out.println("update影响行数:" + delete.getAll().get(0));
//DML MERGE
System.out.println("merge :::");
cacheMergePerson.query(new SqlFieldsQuery("MERGE INTO CACHE_MERGE_PERSON.Person(_key,id,companyId,firstName,lastname,salary,resume)(SELECT _key+1000,id+1000,companyId,firstName,lastname,salary,resume FROM CACHE_ONLY_PERSON.Person WHERE CACHE_ONLY_PERSON.Person._key = ?)").setArgs(11L));
QueryCursor> merge = cacheMergePerson.query(new SqlQuery(Person.class, "from Person"));
merge.getAll().stream().forEach(System.out::println);
}
}
}
日志输出
[15:11:48] Topology snapshot [ver=1, servers=1, clients=0, CPUs=4, heap=0.87GB]
insert :::
Entry [key=11, val=Person [id=11, companyId=1, lastName=jax, firstName=piemon, salary=20000.0, resume=Bad]]
update ::::
update影响行数:[1]
delete :::
update影响行数:[1]
merge :::
Entry [key=1011, val=Person [id=1011, companyId=1, lastName=jax, firstName=piemon, salary=20000.0, resume=Bad]]
[15:11:48] Ignite node stopped OK [uptime=00:00:00:909]
我们的日志打印的是我们查询出的Person。但是一定要注意,我们查询的是Person存储时候的方式
对于DML的Query操作,返回的结果是本次操作所影响的行数!!!上述的日志有体现~~
MERGE和INSERT命令的不同在于,后者添加的条目必须是缓存中不存在的。
如果要把一个键值对插入缓存,那么最后,INSERT语句会被转换为cache.putIfAbsent(...)操作,否则,如果插入的是多个键值对,那么DML引擎会为每个对创建一个EntryProcessor,然后使用cache.invokeAll(...)将数据注入缓存。
我们在实例代码中,以三种方式存储了数据,分别是API:IgniteCache#put和两种SQL方式。我们接下来分别讲解
1.API:这是每种内存数据库都有的操作,不过多将了
2.(_key,_val):我们之前介绍过。Ignite有两个关键字,即:_key,_val,分别标识着键和值,我们这里也是如这般用法,我们将_key赋值1L,将_val赋值一个Person对象
3.(_key,id,compantId.....):这应该算是我们比较常用的SQL写法了,但是这里必须注意的一点就是,你在_key后面所加的属性名,必须在POJO中以@QuerySqlField注释,否则Ignite无法将查询的数据封装为你想要的数据类型。
Ignite的INSERT内是可以写参数列表以及子查询的,我举个例子:
INSERT INTO Person(_key,id,firstname,lastname) values(1L,1L,"a","a")(2L,2L,"b","b")
子查询的例子,我再merge的测试用例中实现的,可以参考哦~~
ignite将所有数据以键-值对的形式存储在内存中,因此所有DML相关操作都被转换成相应的基于像cache.put(...)或者cache.invokeAll(...)这样的命令指令。
开始时,SQL引擎会根据UPDATE语句的WHERE条件生成并且执行一个SELECT查询,然后会修改满足条件的已有值。 修改的执行是利用cache.invokeAll(...)实现的。基本上来说,这意味着一旦SELECT查询的结果准备好,SQL引擎就会准备一定数量的EntryProcessors然后执行cache.invokeAll(...)操作,下一步,EntryProcessors修改完数据之后,会进行额外的检查来确保在SELECT和数据实际更新之间没有其他干扰。
理解该功能并不难,这里需要注意的是:
UPDATE不可以修改_key
原因是缓存键的状态决定了内部数据的布局及其一致性(键的哈希及其关系,索引完整性),所以目前除非先将其删除,否则无法更新缓存键。比如下面的查询:
UPDATE _key = 11 where _key = 10;
会导致下面的缓存操作:
val = get(10);
put(11, val);
remove(10);
DELETE语句的执行分为两个阶段,类似于UPDATE语句的执行。
首先,使用SELECT查询,SQL引擎收集那些满足DELETE语句中WHERE子句的键。接下来,把所有的缓存键都放好后,它创建了一些EntryProcessors并以cache.invokeAll(…)来执行它们。当数据被删除时,会执行额外的检查以确保没有人干扰到数据的SELECT和实际删除。
MERGE是一个非常简单的操作,因为它会被翻译成cache.put(...)或者cache.putAll(...),具体是哪一个,取决于MERGE语句涉及的要插入或者要更新的记录的数量。
我们实例中演示了MERGE是支持子查询的,当然他也支持参数列表,与INSERT是基本类似的。
INSERT和MERGE语句中的子查询和UPDATE和DELETE操作自动生成的SELECT查询一样,如有必要都会被分布化然后执行,要么是并置,要么是非并置的模式。 我们在之前已经解释了Query的并置与非并置,忘记的返回去复习。
我们这里需要重点介绍的是,如果WHERE语句里面有一个子查询,那么他是不会以非并置的分布式模式执行的,子查询始终都会以并置的模式在本地节点上执行。
下面我们选用之前的一个例子,具体代码就不贴了,就是我在讲分布式非并置关联查询时候使用的数据,下面我们直接进到用例的重要部分
private static void distributedNonCollocatedJoin(IgniteCache cacheOnlyPerson) {
final String ORG_CACHE = "CACHE_ONLY_COMPANY";
QueryCursor, Person>> query = cacheOnlyPerson.query(new SqlQuery, Person>(Person.class, "from Person").setLocal(true));
print("Local all persons:", query);
IgniteCache cacheOnlyCompany = Ignition.ignite().cache("CACHE_ONLY_COMPANY");
QueryCursor> query2 = cacheOnlyCompany
.query(new SqlQuery(Company.class, "from Company").setLocal(true));
print("Local all company:", query2);
String sql = "delete from Person as p where p.companyId in (select _key from CACHE_ONLY_COMPANY.Company)";
SqlFieldsQuery sqlFieldsQuery = new SqlFieldsQuery(sql);
sqlFieldsQuery.setDistributedJoins(true);
cacheOnlyPerson.query(sqlFieldsQuery);
System.out.println("------------------------------------------");
QueryCursor> result = cacheOnlyPerson.query(new SqlQuery(Person.class, "from Person"));
result.getAll().stream().forEach(System.out::println);
}
输出日志
[16:23:56] Topology snapshot [ver=2, servers=2, clients=0, CPUs=4, heap=1.7GB]
>>> Local all persons:
>>> Entry [key=3, val=Person [id=3, companyId=11, lastName=Smith, firstName=John, salary=3000.0, resume=John Smith has Bachelor Degree.]]
>>> Local all company:
>>> Entry [key=11, val=Company [id=11, name=ultrapower93]
------------------------------------------
Entry [key=4, val=Person [id=4, companyId=11, lastName=Smith, firstName=Jane, salary=4000.0, resume=Jane Smith has Master Degree.]]
[16:23:58] Ignite node stopped OK [uptime=00:00:02:472]
我们上面只贴出了SQL操作的部分,其他因为以前用的太多了,就不贴出来占地方了。
首先,我一共启动的了两个服务器节点。
通过上述的日志,我们也可以看到本地节点上数据并不多,只有一个Company和Person,其他的数据自然是在另外一个节点上。我下面草拟一下数据的排布
A节点:
Person[id=1,companyId=1]
Person[id=2,companyId=1]
Person[id=4,companyId=11]
Company[id=1]
B节点(即我们上述方法执行所在的节点,我们有用Cleint)
Person[id=3,CompanyId=11]
Company[id=11]
然后我们来看最终结果。剩下的是Person[id=4,companyId=11]
其实到这里已经明了了,where字句中的子查询确实只是在本地做关联查询,并没有分布式!!
所以,如果必须在where中使用子查询的话,一定要确保子查询数据采取了必要的并置,杜绝我们示例这种情况发生。
what's DDL
数据定义语言(DDL)
Apache ignite支持使用数据定义语言(DDL)语句在运行时创建和删除SQL索引。原生的Ignite SQL API以及JDBC和ODBC驱动都可以用于SQL模式的修改。
Apache ignite支持在运行时创建和删除SQL表和索引的数据定义语言(DDL)语句。我们之前讲过的Query都可以激活语句,也可以使用JDBC和ODBC驱动程序来修改SQL模式。
语法:
CREATE TABLE [IF NOT EXISTS] tableName (tableColumn [, tableColumn]...[, PRIMARY KEY (columnName [, columnName]...)] )[WITH "paramName=paramValue [,paramName=paramValue]..."]
tableColumn := columnName columnType [PRIMARY KEY]
创建一般顺序的索引语法:
CREATE [SPATIAL] INDEX [IF NOT EXISTS] indexName ON tableName (indexColumn, ...)
创建符合索引的语法:
CREATE INDEX idx_person_name_birth_date ON Person (name ASC, birth_date DESC)
创建地理空间索引的语法:
CREATE SPATIAL INDEX idx_person_address ON Person (address)
删除索引
DROP INDEX [IF EXISTS] indexName
public class Company {
private static final AtomicLong ID_GEN = new AtomicLong();
@QuerySqlField(index = true)
private Long id;
@QuerySqlField()
private String name;
}
public static void main(String[] args) {
try(Ignite ignite = Ignition.start("examples/config/example-ignite.xml")){
CacheConfiguration companyConfig = new CacheConfiguration(
"CACHE_ONLY_Company");
companyConfig.setIndexedTypes(Long.class, Company.class);
try (IgniteCache cacheOnlyCompany = ignite.getOrCreateCache(companyConfig);) {
String sql = "CREATE INDEX idx_company_name on Company(name)";
FieldsQueryCursor> query = cacheOnlyCompany.query(new SqlFieldsQuery(sql));
query.getAll().stream().forEach(System.out::println);
String sql2 = "CREATE INDEX idx_company_id_name on Company(id DESC,name ASC)";
FieldsQueryCursor> query2 = cacheOnlyCompany.query(new SqlFieldsQuery(sql2));
query2.getAll().stream().forEach(System.out::println);
String sql3 = "CREATE SPATIAL INDEX idx_company_name ON Company (name)";
FieldsQueryCursor> query3 = cacheOnlyCompany.query(new SqlFieldsQuery(sql3));
query3.getAll().stream().forEach(System.out::println);
}
}
}
输出日志
通过DDL语法,使得我们可以在运行时创建索引。删除索引太简单,就不写了。
实例应该是最好的语言,我相信绝大多数的看官都会。
这里需要着重说的是,做地理空间索引时候,必须加入如下依赖
GridGain External Repository
http://www.gridgainsystems.com/nexus/content/repositories/external
...
org.apache.ignite
ignite-geospatial
2.1.4
作为拓展项目,后面还会说这个点,各位看官可以在后面学习。
Ignite的DDL支持jdbc拓展,我们看下面的例子:
Class.forName("org.apache.ignite.IgniteJdbcDriver");
Connection conn = DriverManager.getConnection(
"jdbc:ignite:cfg://file:///etc/config/ignite-jdbc.xml");
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE INDEX idx_company_name ON Company (name)");
}
作为一个拓展点,当无法获取Ignite实例时候,您可以通过这种方式来操作DDL
还是老一样的数据,就不贴了
public static void main(String[] args) {
try (Ignite ignite = Ignition.start("examples/config/example-ignite.xml")) {
CacheConfiguration companyConfig = new CacheConfiguration(
"CACHE_ONLY_COMPANY");
companyConfig.setIndexedTypes(Long.class, Company.class);
CacheConfiguration personConfig = new CacheConfiguration("CACHE_ONLY_PERSON");
personConfig.setIndexedTypes(Long.class,Person.class);
try (IgniteCache cacheOnlyCompany = ignite.getOrCreateCache(companyConfig);
IgniteCache cacheOnlyPerson = ignite
.getOrCreateCache(personConfig)) {
// 初始化数据
initData(cacheOnlyCompany, cacheOnlyPerson);
timeout(cacheOnlyPerson);
shutdown(cacheOnlyPerson);
}
}
}
private static void timeout(IgniteCache cacheOnlyPerson) {
// TODO Auto-generated method stub
QueryCursor> person = cacheOnlyPerson.query(new SqlQuery(Person.class,"from Person").setTimeout(1, TimeUnit.SECONDS));
}
private static void shutdown(IgniteCache cacheOnlyPerson) {
QueryCursor> person = cacheOnlyPerson.query(new SqlQuery(Person.class,"from Person"));
person.close();
}
我们先来看设置timeout时间的方式,这种方式javaCompletableFuture的形式差不多,可以借鉴来理解。
SqlQuery和SqlFieldQuery均提供了这API,帮你断掉失败的查询
我们来看shutdown方法,代码中我们并没有对查询Query设置超时时间,也就是说如果数据可以的话,那么就一直持续查询。
我们可以自己维护一个定时器,当你感觉不像等待时候,可以调用查询结果集的close方法,强制停掉查询。
QueryCursor#close()方法的作用是:
关闭与此cursor(就是结果集)相关的所有资源。如果查询已经在进行中(如果从另一个线程调用,这是可能的),将会尝试去取消掉。对这种方法的连续调用没有副作用。
生产中可以多采用timeout的的形式来提高系统的响应性~~
Ignite的SQL引擎支持通过额外用Java编写的自定义SQL函数,来扩展ANSI-99规范定义的SQL函数集。
一个自定义SQL函数仅仅是一个加注了@QuerySqlFunction注解的公共静态方法。
public class SqlFunctionEnjoy {
@QuerySqlFunction
public static double sqlFunction(double salary) {
return salary + 1000;
}
}
你需要做的就是这么简单,但是方法必须静态,注解必须加!!!
public static void main(String[] args) {
try (Ignite ignite = Ignition.start("examples/config/example-ignite.xml")) {
CacheConfiguration companyConfig = new CacheConfiguration(
"CACHE_ONLY_COMPANY");
companyConfig.setIndexedTypes(Long.class, Company.class);
CacheConfiguration personConfig = new CacheConfiguration("CACHE_ONLY_PERSON");
personConfig.setIndexedTypes(Long.class,Person.class);
personConfig.setSqlFunctionClasses(SqlFunctionEnjoy.class);
try (IgniteCache cacheOnlyCompany = ignite.getOrCreateCache(companyConfig);
IgniteCache cacheOnlyPerson = ignite
.getOrCreateCache(personConfig)) {
// 初始化数据
initData(cacheOnlyCompany, cacheOnlyPerson);
FieldsQueryCursor> result = cacheOnlyPerson.query(new SqlFieldsQuery("select salary from Person as p where p.firstname=? and lastname = ?").setArgs("John","Doe"));
Double salary = (Double) result.getAll().get(0).get(0);
System.out.println("不调用函数的salary:" + salary);
result = cacheOnlyPerson.query(new SqlFieldsQuery("select sqlFunction(salary) from Person as p where p.firstname=? and lastname = ?").setArgs("John","Doe"));
salary = (Double) result.getAll().get(0).get(0);
System.out.println("调用函数的salary:" +salary);
}
}
}
输出日志:
[18:47:43] Topology snapshot [ver=1, servers=1, clients=0, CPUs=4, heap=0.87GB]
不调用函数的salary:2000.0
调用函数的salary:3000.0
[18:47:43] Ignite node stopped OK [uptime=00:00:00:752]
自定义函数的步骤:
1.在某一个类A中创建你的SQL函数,该函数必须静态,必须标识以@QuerySqlFunction注解
2.在业务用例中,通过CacheConfiguration#setSqlFunctionClasses(class...),将你函数所在的类的class实例注册进去。Ignite会去自己找
3.在sql中可以像样例中这样使用自定义的sql函数啦。
该功能比较简单,但是用处很大,切记。