TimeStamp

5.1.13 MySQL Server Time Zone Support

11.2 Date and Time Data Types

Section 11.2.6, “Automatic Initialization and Updating for TIMESTAMP and DATETIME”.

格林威治时间

UTC

廖雪峰-如何正确的处理时间

jdbc url timezone

Server SQL Modes

ALLOW_INVALID_DATES

STRICT_MODE

NO_ZERO_DATE

  • TIMESTAMP 类型的的范围 1970-01-01 00:00:01.000000 UTC 到 2038-01-19 03:14:07.999999 UTC,注意是 UTC 时间

  • TIMESTAMP 的值存储为自 epoch 1970-01-01 00:00:00 UTC 的秒数

  • TIMESTAMP[(fsp)] fsp (0-6) 表示可以指定小数秒数精度, 默认是 0 ,表示没有用小数部分, 如 ‘2038-01-19 03:14:07.999999’

  • STRICT_MODE, NO_ZERO_DATE 决定是否可以设为 ‘0’ 值 (0000-00-00 00:00:00)

  • Automatic initialization and updating to the current date and time can be specified using DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP column definition clauses. By default, the first TIMESTAMP column has these properties, as previously noted. However, any TIMESTAMP column in a table can be defined to have these properties.

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME) By default, the current time zone for each connection is the server’s time. The time zone can be set on a per-connection basis. As long as the time zone settings remains constant, you get back the same value you store. If you store a TIMESTAM value, and then change the time zone and retrieve the value, the value is different from the value you stored. The current time zone is avaliable as the value of the time_zone system variable. This occurs because the same time zone was not used for conversion in both directions. The current time zone is available as the value of the time_zone system variable. For more information, see Section 5.1.13, “MySQL Server Time Zone Support”.

MySQL Server Time Zone Support

  • system time zone 系统时区,server 启动时,尝试自动获取所在机器的时区,并设置为 system_time_zone 系统变量(system variable)

    要显示指定 Mysql Server 启动时的系统时区,启动 mysqld 服务前,设置 TZ 环境变量。 如果使用 mysqld_safe, 使用 --timezone 选项也可以设置

  • server time zone 服务器当前时区, 全局 time_zone系统变量表明了 server 当前操作使用的时区. time_zone 的初始化值是 SYSTEM, 表示和系统时区 (system time zone) 相同; 初始的全局 server time zone 也可以通过命令行选项 --default-time-zone 来设置,或者在 option 文件中指定 default-time-zone='timezone'; 如果拥有权限,也可以在运行时设置:

1
SET GLOBAL time_zone = timezone;
  • per-session time zone 针对每个会话的时区,每一个连接的客户端都有自己的会话时区设置,通过会话范围内的 time_zone 变量来设置;默认会话变量会从全局 time_zone 变量继承,但是客户端也可以修改:
1
SET time_zone = timezone;

会话时区设置会影响与时区相关(zone-sensitive)的数据类型的展示和存储. 包含 NOW() 或者 CURTIME 函数值的展示,以及 TIMESTAMP 列的值的存储和查询. 存储时,TIMESTAMP 列的值会从当前会话时区转为 UTC 进行存储,查询时,再从 UTC 时间转为当前会话的时区时间

UTC_TIMESTAMP 函数和 DATE, TIME 或者 DATETIME 列的值不受会话时区影响,它们也不是转为 UTC 存储

timezone 时区的设置格式

  • 指定为 SYSTEM, 表明使用系统时区 system time zone
  • 指定一个 UTC 偏移量的字符串, +/-[H]H:MM, 如 ‘+10:00’, ‘-6:00’ (前面会自动补 0). 有效范围 ‘-12:59’ - ‘+13:00’ inclusive
  • ‘Europe/Helsinki’, ‘US/Eastern’ 类似于这种命名时区,需要 mysql 库中创建的了时区信息表,并且填充了数据

JDBC 查询 TIMESTAMP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// step 1 
com.mysql.jdbc.ResultSetImpl#getTimestamp(int)

// step 2
com.mysql.jdbc.ResultSetImpl#getTimestampInternal

// step3
com.mysql.jdbc.ByteArrayRow#getTimestampFast

// step4
com.mysql.jdbc.ResultSetRow#getTimestampFast(int, byte[], int, int, java.util.Calendar, java.util.TimeZone, boolean, com.mysql.jdbc.MySQLConnection, com.mysql.jdbc.ResultSetImpl)

// step5
(useLegacyDatetimeCode=false) mysql.jdbc.TimeUtil#fastTimestampCreate(java.util.TimeZone, int, int, int, int, int, int, int)

(useLegacyDatetimeCode=false) com.mysql.jdbc.TimeUtil#changeTimezone(com.mysql.jdbc.MySQLConnection, java.util.Calendar, java.util.Calendar, java.sql.Timestamp, java.util.TimeZone, java.util.TimeZone, boolean)

INIT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"net_buffer_length" -> "16384"
"interactive_timeout" -> "28800"
"query_cache_size" -> "16777216"
"character_set_connection" -> "utf8"
"max_allowed_packet" -> "16777216"
"net_write_timeout" -> "60"
"lower_case_table_names" -> "0"
"system_time_zone" -> "CST"
"tx_isolation" -> "REPEATABLE-READ"
"time_zone" -> "SYSTEM"
"wait_timeout" -> "28800"
"character_set_server" -> "utf8"
"auto_increment_increment" -> "1"
"license" -> "GPL"
"character_set_client" -> "utf8"
"sql_mode" -> "ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
"character_set_results" -> "utf8"
"query_cache_type" -> "OFF"
"init_connect" -> ""


com.mysql.jdbc.ConnectionImpl#configureTimezone

//1. configuredTimeZoneOnServer=CST

com.mysql.jdbc.ConnectionPropertiesImpl#getServerTimezone
// 2. 返回 url 中配置的 serverTimezone: canonicalTimezone = America/New_York
public String getServerTimezone() {
return this.serverTimezone.getValueAsString();
}

// 3. (getUseTimezone() || !getUseLegacyDatetimeCode()) && configuredTimeZoneOnServer != null)

// 4. this.serverTimezoneTZ = sun.util.calendar.ZoneInfo[id="America/New_York",offset=-18000000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/New_York,offset=-18000000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]


/**
* Configures the client's timezone if required.
*
* @throws SQLException
* if the timezone the server is configured to use can't be
* mapped to a Java timezone.
*/
private void configureTimezone() throws SQLException {
String configuredTimeZoneOnServer = this.serverVariables.get("timezone");

if (configuredTimeZoneOnServer == null) {
configuredTimeZoneOnServer = this.serverVariables.get("time_zone");

if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
configuredTimeZoneOnServer = this.serverVariables.get("system_time_zone");
}
}

String canonicalTimezone = getServerTimezone();

if ((getUseTimezone() || !getUseLegacyDatetimeCode()) && configuredTimeZoneOnServer != null) {
// user can override this with driver properties, so don't detect if that's the case
if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
try {
canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
} catch (IllegalArgumentException iae) {
throw SQLError.createSQLException(iae.getMessage(), SQLError.SQL_STATE_GENERAL_ERROR, getExceptionInterceptor());
}
}
}

if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
this.serverTimezoneTZ = TimeZone.getTimeZone(canonicalTimezone);

//
// The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this...
//
if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverTimezoneTZ.getID().equals("GMT")) {
throw SQLError.createSQLException("No timezone mapping entry for '" + canonicalTimezone + "'", SQLError.SQL_STATE_ILLEGAL_ARGUMENT,
getExceptionInterceptor());
}

this.isServerTzUTC = !this.serverTimezoneTZ.useDaylightTime() && this.serverTimezoneTZ.getRawOffset() == 0;
}
}
1
2
Calendar.DST_OFFSET
Calendar.DST_OFFSET
1
2
3
4
5
6
7
8
9
10
11
12
13
# 相关时区设置和查看命令

SET GLOBAL time_zone = "+5:00";

SELECT @@GLOBAL.time_zone, @@SESSION.time_zone;
# @@GLOBAL.time_zone @@SESSION.time_zone
# SYSTEM SYSTEM


show variables like '%time_zone%';
# Variable_name Value
# system_time_zone CST
# time_zone SYSTEM

CST 是个坑,安装的 Mysql 查看系统时区,显示 CST, 其实是东八区 UTC+8, 可以通过改变会话时区查看 TIMESTAMP 列的值进行验证,但是在 Java 中,CST 指的就是 Central Standard Time, 是 UTC-6, 所以如果客户端,如 Java 程序默认的时区就是 Asia/Shanghai (UTC+8), 不指定任何时区转换设置,操作和显示没有什么问题;Mysql 系统时区变量为 CST, Mysql Server 默认全局时区(System Time Zone - 全局 time_zone 变量) 为 SYSTEM , 继承自系统时区,会话时区默认(Session Time Zone - 局部 time_zone 变量)继承全局时区,最终其实就是 UTC+8

如果特意在 jdbc url 中指定 serverTimezone=CST:

1
jdbc:mysql://localhost:3306/learning_db?useUnicode=true&characterEncoding=utf8&useLegacyDatetimeCode=true&useTimezone=true&serverTimezone=CST

Mysql 查询 TIMESTAMP 列时,将存储的 UTC+0 时间转换为默认的会话时区时间,默认的会话时区是 UTC+8, 在 Mysql 里显示的是 SYSTEM,继承的 CST(这里虽然是 CST, 其实是 UTC+8, 系统时区默认取机器的时区,通过 date -R, 显示 +8:00), 这时候又指定了 serverTimezone=CST, JDBC 获取的系统时区服务器变量 system_time_zoneCST, 但不是同一个东西,Java 将这个指定的CST转换为时区对象时,转换的就是 UTC-6, 而在 Mysql 服务器一侧是 UTC+8

1
2
// Java 中,如果是 CST  TimeZone.getTimeZone("CST"), offset=-21600000, 即 'UTC-6'
sun.util.calendar.ZoneInfo[id="CST",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=CST,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]

这时,反而会有问题,相当于是 UTC-6(指定的Mysql Server会话时区) 转换为 UTC+8 (客户端默认时区)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# JDBC 查询 TIMESTAMP 列的步骤

1.
默认的会话时区UTC+8
2038-01-19 11:14:07
Mysql服务器 ---------------------------------------------------------------------> JDBC

### JDBC 拿到 TIMESTAMP 列的字节数组转为字符串是 ‘2038-01-19 11:14:07’, 所以第一步应该是这样的;而且改变全局 time_zone 变量,获取的字节数组也发送了变化
## SET GLOBAL time_zone = "+5:00";

2.
serverTimezone=CST client默认时区=Asia/Shanghai
2038-01-19 11:14:07(CST UTC-6)----> 2038-01-20 13:14:07 (Asia/Shanghai UTC+8)
JDBC------------------------------------------------------------------------------> Java TimeStamp

所以如果真的存在不同客户端时区问题,必要时修改 Mysql 的系统时区为含义明确的时区表示,而不是 CST, 客户端在使用的时候也不用指定 useLegacyDatetimeCode=true&useTimezone=true&serverTimezone=时区, 使用新的 useLegacyDatetimeCode=false 获取时区的方式就可以了,这种会查询 Mysql Server 的时区变量,作为 Server 的时区,因为会话默认也是取的系统时区,所以是一致的,没有什么问题。这时候会将正确的时间转换为客户端时区的时间。

TIMESTAMP 操作真的是很麻烦了,必要时可以采用 廖雪峰-如何正确的处理时间 中策略,存储成 long 型的时间偏移量就好多了,需要比较的话,直接比较的是整数,显示时,再指定特定时区,就可以了。

Ubuntu 安装配置

Ubuntu 国内镜像下载

http://mirrors.163.com/ubuntu-releases/

创建 USB 启动盘

https://ubuntu.com/tutorials/create-a-usb-stick-on-windows#1-overview

USB 启动盘工具

https://rufus.ie/

安装教程:

https://ubuntu.com/tutorials/install-ubuntu-desktop#4-boot-from-usb-flash-drive

设置教程:

写给工程师的 Ubuntu 20.04 最佳配置指南

开启 SSH

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装 ssh-serve
sudo apt-get install openssh-server

# 设置允许 root 登录
sudo vi /etc/ssh/sshd_config
PermitRootLogin yes


# 生成 ssh 密钥对
ssh-keygen -t rsa

# 复制到目标机器
ssh-copy-id user@domain/ip

安装 Vim

1
2
3
4
5
6
# 1. 
sudo apt install vim

# 2. 修改 /etc/vim/vimrc.tinyset compatible 设置成set nocompatible . 这是因为有时候系统会默认vim兼容vi,所以使用vi的命令。
vi /etc/vim/vimrc.tinyset compatible
set nocompatible

Spring Resource

Application Context 构造器的 Resource 路径中的通配符

注意事项:只能使用在 ApplicationContext 的构造器的字符串路径中,与 Resource 类型没有任何关系,一个 Resource 一次只能指定一个 resource 数据源,不能在构造一个实际的 Resource 时,路径中包含这些通配符。

Note that this wildcarding is specific to the use of resource paths in application context constructors (or when you use the PathMatcher utility class hierarchy directly) and is resolved at construction time. It has nothing to do with the Resource type itself. You cannot use the classpath*: prefix to construct an actual Resource, as a resource points to just one resource at a time.

classpath* 前缀

在 ApplicationContext 的构造器中,指定 bean 定义的 xml 文件时,可以使用通配符 classpath* 前缀,如果在多个 classpath 目录下都存在相同名称的文件,且匹配路径中的文件名,首先会通过调用 ClassLoader.getResources(…)) 来获取每个 classpath 下文件对应的 URL, 然后合并作为最终的上下文 bean 定义

This special prefix specifies that all classpath resources that match the given name must be obtained (internally, this essentially happens through a call to ClassLoader.getResources(…)) and then merged to form the final application context definition.

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-resourceloaderaware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  /**
* resources/test.xml
* E:/DevelopmentKit/ClasspathOutofIdea/test.xml 指定为 classpath 中 (File -> Project Structure -> SDKS -> classpath)
* 上面的两个路径都为 classpath, 并且包含了同名的 bean 定义 xml 文件中,分别定义了 Date bean 和 Mp3Song bean
*/
public static void resources() {
try {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath*:test.xml");
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}

Enumeration<URL> resources1 = applicationContext.getClassLoader().getResources("test.xml");
while (resources1.hasMoreElements()) {
URL url = resources1.nextElement();
System.out.println("url = " + url);
}
} catch (IOException e) {
e.printStackTrace();
}
}

结果:

1
2
3
4
beanDefinitionName = java.util.Date#0
beanDefinitionName = com.something.learn.bean.Mp3Song#0
url = file:/E:/DevelopmentKit/ClasspathOutofIdea/test.xml
url = file:/E:/DevelopmentKit/idea-workspace/java-workspace/spring_loader_bean/target/classes/test.xml

Ant-style 匹配表达式

(matched by using Spring’s PathMatcher utility)

ant 风格的表达式也可以使用在 Application Context 的构造器路径中,如 classpath:com/mycompany/**/service-context.xml, 此时,会通过调用 ClassLoader.getResource(…)), 传入最后的非通配符片段,返回一个 URL 对象,如果是非 jar: 形式的 URL, 会从中获取 java.io.File 对象,通过遍历文件系统来解析通配符。

Ant-style的匹配原则

Apache Ant样式的路径有三种通配符匹配方法(在下面的表格中列出)

路径 描述
? 匹配任何单字符
* 匹配0或者任意数量的字符
** 匹配0或者更多的目录

Table Example Ant-Style Path Patterns

Path Description
/app/*.x 匹配(Matches)所有在app路径下的.x文件
/app/p?ttern 匹配(Matches) /app/pattern 和 /app/pXttern 但是不包括/app/pttern
/**/example 匹配(Matches) /app/example/app/foo/example, 和 /example
/app/**/dir/file. 匹配(Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf, 和 /app/dir/file.java
/*/.jsp 匹配(Matches)任何的.jsp 文件

可以结合 classpath* 和 ant-style 的表达式,如:

1
classpath*:com/mycompany/**/service-context.xml

注意事项:

Note that classpath*:, when combined with Ant-style patterns, only works reliably with at least one root directory before the pattern starts, unless the actual target files reside in the file system. This means that a pattern such as classpath*:*.xml might not retrieve files from the root of jar files but rather only from the root of expanded directories.

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-app-ctx-ant-patterns-in-paths

1
2
3
4
EnvironmentPostProcessor
PathMatchingResourcePatternResolver
ResourcePropertySource
EncodedResource

连接

问题还原

表结构及索引情况

1
2
3
4
5
6
7
8
9
10
11
12
> desc user_info

Field Type Null Key Default Extra
id int(11) NO PRI \N auto_increment
uid varchar(64) NO MUL \N
name varchar(255) YES \N

> desc user_scode
Field Type Null Key Default Extra
id int(11) NO PRI \N auto_increment
uid varchar(64) NO MUL \N
score float YES \N

表的大小和 user_score 表的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
 select count(*) from  user_info
-- 22000000

select count(*) from user_score
-- 6

id uid score
9 899-99-9905 1.1
10 899-99-9905 1.1
11 899-99-9905 1.1
12 899-99-9905 1.1
13 899-99-9904 1.23
14 899-99-9904 1.254
1
2
3
4
5
6
7
select * from user_score us join user_info ui  on us.uid = ui.uid where us.id = 13;


id uid score id uid name
13 899-99-9904 1.23 20419033 899-99-9904 Julissa Rolfson

1 row retrieved starting from 1 in 9 s 63 ms (execution: 9 s 31 ms, fetching: 32 ms)

执行计划

1
2
3
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1 SIMPLE us \N const PRIMARY,index_uid PRIMARY 4 const 1 100 \N
1 SIMPLE ui \N ALL \N \N \N \N 21614208 100 Using where
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
show create table user_info;

Table Create Table
user_info "CREATE TABLE `user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` varchar(64) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_uid` (`uid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=22000001 DEFAULT CHARSET=utf8"



show create table user_score;
Table Create Table
user_score "CREATE TABLE `user_score` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` varchar(64) NOT NULL,
`score` float DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_uid` (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4"

image-20200510120118147

1
2

/* select#1 */ select '13' AS `id`,'899-99-9904' AS `uid`,'1.23' AS `score`,`learning_db`.`ui`.`id` AS `id`,`learning_db`.`ui`.`uid` AS `uid`,`learning_db`.`ui`.`name` AS `name` from `learning_db`.`user_score` `us` join `learning_db`.`user_info` `ui` where (('899-99-9904' = convert(`learning_db`.`ui`.`uid` using utf8mb4)))

字符集不一致的问题导致索引失效.

修改字符集:

1
ALTER TABLE user_score CONVERT TO CHARACTER SET utf8;
1
2
3
4
5
6
select * from user_score us join user_info ui  on us.uid = ui.uid where us.id = 13

id uid score id uid name
13 899-99-9904 1.23 20419033 899-99-9904 Julissa Rolfson

1 row retrieved starting from 1 in 47 ms (execution: 0 ms, fetching: 47 ms)

执行计划:

1
2
3
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1 SIMPLE us \N const PRIMARY,index_uid PRIMARY 4 const 1 100 \N
1 SIMPLE ui \N ref index_uid index_uid 194 const 1 100 \N
  1. 如果存在 JOIN 连接等操作的表,在建表的时候就应该留心之前系统的规范是什么,建表的默认字符集或者规定的字符集是什么, 而不应该依靠于自己的喜好和认知去建立
  2. 索引列参加计算会导致索引失效,所以非索引常量等值条件的查询,或者较复杂涉及连接的查询,都应该提前分析是否会使用索引,语句的效率,测试执行时间,查看执行计划,看看是否符合预期

原文地址: https://mp.weixin.qq.com/s/Cimeyo4cVQsF-MfHBsNgGg?1=a

1
explain select * from  user_info ui straight_join user_score us  on ui.uid = us.uid  where us.id = 13
1
[HY000][1003] /* select#1 */ select `learning_db`.`ui`.`id` AS `id`,`learning_db`.`ui`.`uid` AS `uid`,`learning_db`.`ui`.`name` AS `name`,`learning_db`.`ui`.`upd_time` AS `upd_time`,`learning_db`.`us`.`id` AS `id`,`learning_db`.`us`.`uid` AS `uid`,`learning_db`.`us`.`score` AS `score` from `learning_db`.`user_info` `ui` straight_join `learning_db`.`user_score` `us` where ((`learning_db`.`us`.`id` = 13) and (`learning_db`.`us`.`uid` = convert(`learning_db`.`ui`.`uid` using utf8mb4)))

在字符集编码相同的情况下,强制使得 user_info 表作为驱动表的情况:

1
2
3
4
5
explain select * from  user_info ui straight_join user_score us  on ui.uid = us.uid  where us.id = 13;

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE ui \N ALL index_uid \N \N \N 21919807 100 \N
1 SIMPLE us \N const PRIMARY,index_uid PRIMARY 4 const 1 50 Using where

外连接

备注:使用的 Mysql版本为 5.7 ,参考的Mysql官方使用手册也是 5.7 的

1
2
3
version(): 5.7.29-0ubuntu0.18.04.1

Reference Manual: MySQL 5.7 Reference Manual Including MySQL NDB Cluster 7.5 and NDB Cluster 7.6

外连接 Outer Joins 包含 LEFT JOINRIGHT JOIN

在外连接 A LEFT JOIN B 中将 B 表的筛选条件写在连接条件 (JOIN CONDITION, on A.col = B.col)和写在 WHERE 字句中的结果

此例中,user_info_temp 表中有一条 uid'899-99-9905' 的行,user_score_temp 中有两条 uid 为 '899-99-9905' 的数据

B 表的筛选条件写在连接条件中

sql 语句:

1
select * from  user_info_temp ui left join user_score_temp us on us.uid = ui.uid and us.uid = '899-99-9905';

执行计划:

1
2
3
4
5
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1 SIMPLE ui \N ALL \N \N \N \N 5 100 \N
1 SIMPLE us \N ref index_uid index_uid 194 const 2 100 Using where

-- 这里应该使用了 Indexed Nested Loop Join 连接算法, 会使用 B 表中连接字段的索引

执行结果:

1
2
3
4
5
6
7
id	uid	name	id	uid	score
5042849 899-99-9926 Leif Streich V \N \N \N
8018366 899-99-9948 Hulda Rodriguez \N \N \N
20107227 899-99-9921 Sarita Kassulke IV \N \N \N
20419033 899-99-9905 Julissa Rolfson 12 899-99-9905 1.1
20419033 899-99-9905 Julissa Rolfson 13 899-99-9905 1.23
21439863 899-99-9926 Stuart Daugherty \N \N \N

B 表的筛选条件写在 WHERE 字句中

sql 语句:

1
select * from  user_info_temp ui left join user_score_temp us on us.uid = ui.uid where us.uid = '899-99-9905';

执行计划:

1
2
3
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1 SIMPLE ui \N ref index_uid index_uid 194 const 1 100 \N
1 SIMPLE us \N ref index_uid index_uid 194 const 2 100 Using index condition

执行结果:

1
2
3
id	uid	name	id	uid	score
20419033 899-99-9905 Julissa Rolfson 12 899-99-9905 1.1
20419033 899-99-9905 Julissa Rolfson 13 899-99-9905 1.23

解释

出现不同结果的原因是,写在连接条件中,会作为连接前筛选 B 表的条件,B 表中满足条件的有两条, 再做外连接; 而写在 WHERE 字句中,是作为连接后的结果的筛选条件的, 只筛选出结果中 B 表的列满足条件的结果.

Mysql 手册 - 8.2.1.8 Outer Join Optimization 节中对外连接的步骤有如下描述:

The LEFT JOIN condition is used to decide how to retrieve rows from table B. (In other words, any
condition in the WHERE clause is not used.)

也就是说, 连接条件决定了如何从 B 表中进行检索数据, WHERE 子句中 B 的条件此时是不会使用的.

还需要注意一点的是, B 表的筛选条件写在 WHERE 字句中, 此时, Mysql 应该是将其转换为一个 INNER JOIN 了, 执行计划中的两个步骤都是用的索引, 而且在控制台中对应打印出了如下的结果:

1
[HY000][1003] /* select#1 */ select `learning_db`.`ui`.`id` AS `id`,`learning_db`.`ui`.`uid` AS `uid`,`learning_db`.`ui`.`name` AS `name`,`learning_db`.`us`.`id` AS `id`,`learning_db`.`us`.`uid` AS `uid`,`learning_db`.`us`.`score` AS `score` from `learning_db`.`user_info_temp` `ui` join `learning_db`.`user_score_temp` `us` where ((`learning_db`.`us`.`uid` = `learning_db`.`ui`.`uid`) and (`learning_db`.`ui`.`uid` = '899-99-9905'))

其中并没有出现 left join , 而是被转换成了 join, 大概想下, 这时的外连接其实是等价于内连接的, 如果 B 的常量等值条件要满足, B 表对应的那一行必然和 A 表中的某一行关联的上, 不能关联上, 则都为 NULL (前提是 B 的条件列声明为 NOT NULL), 所以是等价于内连接的.

因此, 在这种情况下, 写成内连接更能表达清楚本来的意图.

Mysql 手册 - 8.2.1.8 Outer Join Optimization 中的相关描述:

For a LEFT JOIN, if the WHERE condition is always false for the generated NULL row, the LEFT JOIN
is changed to an inner join.

如果 t2.column 为 null, t2.column2=5 这个条件总是 false

1
SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;

因此,可以安全的等价为一个内连接:

1
SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;

在进行转换后,查询优化器(optimizer)就可以根据情况,做出一个较优的查询计划,比如,使用 t2 作为驱动表

不能进行转换的情况

所以如果 B 的条件列可以是 NULL (没有声明为 NOT NUll), 而且条件为 B.col is NULL , 这时, 就不能等价于内连接了, 因为满足 B.col is NULL 的情况有两种, 一种是本来就没有关联上, 另一种是关联上了, 这一列的值本来就是 NULL, 就不满足 WHERE condition is always false for the generated NULL row

外连接的简化

  • 解析阶段,右连接会被转换为只包含左连接
1
(T1, ...) RIGHT JOIN (T2, ...) ON P(T1, ..., T2, ...)
1
(T2, ...) LEFT JOIN (T1, ...) ON P(T1, ..., T2, ...)
1
select * from user_info_temp a right join user_score_temp b on a.uid = b.uid;
1
2
3
mysql> show warnings;

| Note | 1003 | /* select#1 */ select `learning_db`.`a`.`id` AS `id`,`learning_db`.`a`.`uid` AS `uid`,`learning_db`.`a`.`name` AS `name`,`learning_db`.`b`.`id` AS `id`,`learning_db`.`b`.`uid` AS `uid`,`learning_db`.`b`.`score` AS `score` from `learning_db`.`user_score_temp` `b` left join `learning_db`.`user_info_temp` `a` on((`learning_db`.`a`.`uid` = `learning_db`.`b`.`uid`)) where 1 |
  • 所有 T1 INNER JOIN T2 ON P(T1, T2) 形式的内连接表达式,会被替换成,T1,T2, P(T1, T2) 会作为 WHERE 条件进行结合
1
select * from user_info_temp a join user_score_temp b on a.uid = b.uid;
1
2
3
mysql> show warnings; 

| Note | 1003 | /* select#1 */ select `learning_db`.`a`.`id` AS `id`,`learning_db`.`a`.`uid` AS `uid`,`learning_db`.`a`.`name` AS `name`,`learning_db`.`b`.`id` AS `id`,`learning_db`.`b`.`uid` AS `uid`,`learning_db`.`b`.`score` AS `score` from `learning_db`.`user_info_temp` `a` join `learning_db`.`user_score_temp` `b` where (`learning_db`.`b`.`uid` = `learning_db`.`a`.`uid`) |
  • 利用外连接的转换,获得更优的执行计划
1
2
SELECT * T1 LEFT JOIN T2 ON P1(T1,T2)
WHERE P(T1,T2) AND R(T2)

如果此时 R(2) 这个限制条件能够极大的减少 T2 表中匹配的行数,但是还时按照当前的执行顺序,先访问 T1, 再访问 T2 表,就可能会产生一个不够高效的执行计划

如果此时,能够将这个外连接转换为内连接,可能会有一个更高效的执行计划。

什么样的情况下,一个外连接可以被转换为一个内连接?

MySQL converts the query to a query with no outer join operation if the WHERE condition is null-rejected.

A condition is said to be null-rejected for an outer join operation if it evaluates to FALSE or UNKNOWN for any NULL-complemented row
generated for the operation.

如果对于外连接操作的补足NULL行(A LEFT JOIN B –> B.COL IS ALL NULL), 这些条件总是 FALSE 或者 UNKNOW 的,这些条件就是 null-rejected

考虑这样一个简单的外连接:

1
2
3
4
5
6
7
8
9
10
11
12
T1 LEFT JOIN T2 ON T1.A=T2.A

-- null-rejected conditions: false for `null` rows
T2.B IS NOT NULL
T2.B > 3
T2.C <= T1.C
T2.B < 2 OR T2.C > 1

-- not null-rejected conditions: mignt be true for `null` rows
T2.B IS NULL
T1.B < 3 OR T2.B IS NOT NULL
T1.B < 3 OR T2.B > 3

通用的检查规则

  • A IS NOT NULL A 是左连接内表的一个属性

  • 一个谓词(predicate)条件,其中包含了对左连接内表的引用,并且当这个谓词的某一个参数为 NULL 时,谓词返回的值为 UNKNOWN

    什么是谓词?谓词就是返回值为真值(TRUE, )的函数,如 = < > <>比较谓词,LIKE谓词,BETWEEN 谓词,IS NULL, IS NOT NULL, IN谓词,EXISTS

  • AND 进行连接条件,包含了一个 null-rejected 的条件

  • OR 连接的条件,并且所有条件都是 null-rejected

  • 示例*

  1. 简单转换
1
2
3
4
5
6
7
8
9
10
11

-- before converted
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
LEFT JOIN T3 ON T3.B=T1.B
WHERE T3.C > 0


-- after converted
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
INNER JOIN T3 ON T3.B=T1.B
WHERE T3.C > 0
  1. 连续触发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- before converted
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
LEFT JOIN T3 ON T3.B=T2.B
WHERE T3.C > 0

-- first convertion
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
INNER JOIN T3 ON T3.B=T2.B
WHERE T3.C > 0

-- equivalent to the query
SELECT * FROM (T1 LEFT JOIN T2 ON T2.A=T1.A), T3
WHERE T3.C > 0 AND T3.B=T2.B

--second convertion: T3.B=T2.B is null-rejected for embeeded outer join
SELECT * FROM (T1 INNER JOIN T2 ON T2.A=T1.A), T3
WHERE T3.C > 0 AND T3.B=T2.B

MySQL Optimizer 查询优化器

会根据当前表的信息给出执行计划,所以类似的查询语句可能在不同的情况下执行计划不同:

user_info 表和 user_score 两张表, user_info 表有 2200 百万数据,表 user_score 有 6 条数据,通过 uid 字段可以关联,且两张表的 uid 字段都设置了索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
 select count(*) from  user_info
-- 22000000

select count(*) from user_score
-- 6

id uid score
9 899-99-9905 1.1
10 899-99-9905 1.1
11 899-99-9905 1.1
12 899-99-9905 1.1
13 899-99-9904 1.23
14 899-99-9904 1.254
  1. 当连接的筛选条件为 uid = '899-99-9904' 时,user_score 表中有两条可以关联的上,执行计划:
1
2
3
4
5
explain select * from  user_info ui join user_score us on us.uid = ui.uid where ui.uid = '899-99-9904';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE ui \N ref index_uid index_uid 194 const 1 100 \N
1 SIMPLE us \N ref index_uid index_uid 194 const 2 100 Using index condition

可以看出,此时给出的执行计划, 驱动表 ui, 被驱动表 us 都是索引查询,使用的应该是 Indexed Nested Loop Join 连接算法。

  1. 当连接的筛选条件为 uid = '899-99-9905' 时,user_score 表中有四条可以关联的上,执行计划:
1
2
3
4
5
explain select * from  user_info ui join user_score us on us.uid = ui.uid where ui.uid = '899-99-9905';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE ui \N ref index_uid index_uid 194 const 1 100 \N
1 SIMPLE us \N ALL index_uid \N \N \N 6 66.67 Using where; Using join buffer (Block Nested Loop)

此时的执行计划, 连接算法使用的时 Block Nested Loop Join 连接算法 (参考 Mysql 使用手册 - 8.2.1.6 Nested-Loop Join Algorithms),且 user_info 使用了索引;这时,会扫描 user_score 表一次,将 user_score 表中的每一条记录同 Join Buffer 连接缓冲区中的 user_info 表筛选出来的记录进行匹配,如果满足关联条件,返回。

猜想:此时,查询优化器可能是从 uid 的索引的情况来看,user_score 表中 uid = '899-99-9905' 的记录占据了整个表的大部分,而且表本身比较小,此时,做一次全表扫描的性能可能是会更好。

Mysql 查询优化器的工作是一个很复杂的工作流程、会考虑各种因素,参考表的统计信息,给出当前较优的执行计划。

Using filesort (JSON property: using_filesort)
MySQL must do an extra pass to find out how to retrieve the rows in sorted order. The sort is done
by going through all rows according to the join type and storing the sort key and pointer to the row for
all rows that match the WHERE clause. The keys then are sorted and the rows are retrieved in sorted
order. See Section 8.2.1.14, “ORDER BY Optimization”.

常用命令

Linux 常用命令

[TOC]

快捷键

  • CTRL + K 光标处剪切文本至本行结束
  • CTRL + Y 粘贴文本
  • CTRL + E 将光标移动到本行的末尾
  • CTRL + A 将光标移动到本行的开头
  • ALT + F 跳转到下一个空格处
  • ALT + B 回到前一个空格处
  • ALT + Backspace 删除前一个词
  • CTRL + w 剪切光标前一个词

find

查找命令

  • Usage
1
find [path...] [expression]
  • path 制定查找的目录,默认是当前目录

  • expression 包含 operators, options, tests, and actions.

    • tests:

      • -name Pattern 指定文件名的模式
      • -mmin N 过去N分钟更新过的普通文件
    • actions:

      • -delete
      • -print 默认
      • -ls
  • example
1
find . -name "*.txt" -ls

locate

同样用来查找文件,是 find -name 的另一种说法,但是它搜索的是包含所有本地文件信息的数据库。Linux系统自动创建这个数据库,并且每天自动更新一次,所以使用locate命令查不到最新变动过的文件。为了避免这种情况,可以在使用locate之前,先使用updatedb命令,手动更新数据库

  • example
1
2
3
4
5
# 搜索用户目录下所有以 m 开头的文件
locate ~/m

# 忽略大小写,搜索用户目录下所有以 m 开头的文件
locate -i ~/m

whereis

只能用来搜索程序名,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。

  • example
1
2
whereis grep
# 打印: grep: /bin/grep /usr/share/man/man1/grep.1.gz /usr/share/info/grep.info.gz

which

PATH 变量指定的路径中,搜索某个命令的位置,并返回第一个搜索结果。

  • example
1
2
which grep
# 打印: /bin/grep

type

用来区分某个命令是 shell 自带的,还是由外部的二进制文件提供的。如果是一个外部命令, -p 显示该命令的路径。

  • example
1
2
3
4
5
6
7
8
type cd
# cd is a shell builtin

type wget
# wget is /usr/bin/wget

type -p wget
/usr/bin/wget

wget

wget 是一个通过指定 URL 下载文件的工具,比如下载一些软件或者从远程服务器恢复备份到本地服务器。支持HTTP, HTTPS 和 FTP 协议,可以使用 HTTP 代理。可以在后台执行,支持断点下载

参数或使用范例

  • wget https://www.baidu.com

    下载整个页面

    -x 建立服务器上一样的目录

    -nd下载的所有内容会出现在本地当前目录

  • wget -r https://www.baidu.com

    递归下载。下载服务器上的所有目录和文件。

    -l number 指定下载的层次。

  • wget -c http://prefix/dir/file

    断点续传,服务器必须支持。

    -t 表示重试次数 -t 0 表示无穷次重试,直到连接成功。

    -T 表示超时等待时间,超过指定的时间就算超时。

  • wget -i download.txt

    批量下载。多个需要下载的文件的 URL 可以写在一个文件中,每行一个。(URL 指向文件就下载文件,指向网站就下载首页)

  • wget -m -reject=git http://prefix/dir

    选择性下载

    -reject=list 拒绝接受的文件类型

    -accept=list 接受的文件类型

  • wget -http-user=USER -http-passwd=PASS URL

    密码和认证。 只能处理利用用户名/密码方式限制访问的网站

  • wget -proxy-user=USER -proxy-passwd=PASS URL

    代理进行下载。需要创建 .wgetrc 文件设置代理服务器:

    http-proxy=11.11.11.11:8080

    ftp-proxy=11.11.11.11:8080

    -proxy=on/off 使用或关闭代理

  • wget -O rename.zip URL

    重命名下载的文件。wget默认会以最后一个符合”/”的后面的字符来命令。

  • wget –limit-rate=300k URL

    限速下载

  • wget -b URL

    后台下载。

1
2
# 查看正在下载的下载进度。
tail -f wget-log
  • wget -tries=40 URL

    增加重试次数,默认重试20次?

  • wget -mirror URL

    镜像下载。下载整个网站到本地。

    -p 下载所有为了html页面显示正常的文件
    –convert-links 下载后,转换成本地的链接
    -P ./LOCAL 保存所有文件和目录到本地指定目录

  • wget -o download.log URL

    保存下载日志到文件中。

  • wget -Q5m -i files.txt

    限制下载的文件大小,超出后推出下载。

  • wget -r -A.pdf URL

    只下载指定格式的文件。

  • wget FTP-URL

  • wget –ftp-user=USER –ftp-password=PASS URL

    wget FTP 下载。

下载jdk

1
2
3
# wget后需要加参数–no-cookies –header “Cookie: oraclelicense=accept-securebackup-cookie;”

wget --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie;" http://download.oracle.com/otn-pub/java/jdk/8u181-b13/96a7b8442fe848ef90c96a2fad6ed6d1/jdk-8u181-linux-x64.rpm

tar

用于打包、拆包、压缩、解压

参数或使用范例

  • -c

    建立一个打包文件

  • -x

    解开一个打包文件

  • -t

    查看 tar 包里的文件

  • -z

    gzip 工具,用于压缩成或解压 .tar.gz 格式的文件 (格式的名字只是为了标记,以免解压时用了错误的压缩工具)

  • -j

    zip2 工具,用于压缩成或解压 .tar.bz2 格式的文件

  • -v

    显示打包、拆包、压缩或解压过程。

  • -f

    使用文件名,后面立即接文件名

  • -p

    保持原文件的属性

  • -P

    使用绝对路径来压缩。

  • -N 2001/03/11

1
tar -N 2017/01/23 -zcvf index.html hello.txt world.txt

设定日期,后面接的日期还要新的文件才会被打包。

  • –exclude file
1
tar --exclude hello.txt -zcvf b.tar.gz index.html hello.txt

剔除不需要进行打包的文件

可以作为命令行参数

1
2
3
tar -cvf - *.txt | tar -xf - -C dir/

# tar cvf - files/ | ssh user@example.com "tar xv -C Documents/"

虽然 tar 命令提供了排除 VCS 的选项, 但是使用起来却有点问题, 简单点, 可以直接使用排序选项, * 默认应该是排除了 . 开头的文件, 所以 .gitignore .git/ 都没有包含在最终的包中

1
tar  --exclude "target" --exclude "*.iml" -cvf spring-boot-demo-source.tar *

cat

cat 命令用于显示文件的内容

参数或使用范例

  • 显示整个文件
1
cat file
  • 从键盘创建一个文件
1
2
cat > file
Hello World!

只能创建新文件,不能编辑已有文件。 Ctrl + d 结束输入

  • 合并文件
1
2
3
4
5
# 合并文件输出到另一个文件中
cat file1 file2 > file

# 合并文件并输出到屏幕
cat file1 file2
  • -n 行编号
1
cat -n|number file
  • -b 行编号,空白行不编号
1
cat -b file

grep

Global Search Regular Expression(RE) and Print out the line

全局正则表达式搜索,用于文本搜索(支持正则表达式),并把匹配的行打印出来。

  • 常规用法
1
2
3
grep match_pattern file

grep "match_pattern" file
  • 多文件搜索
1
grep "match_pattern" file_1 file_2 file_3 ...
  • -v 反转查找

    即打印不匹配该模式的所有行

1
grep -v "match_pattern" file
  • -E 使用正则表达式
1
2
3
grep -E "[1-9]+" file

egrep "[1-9]+" file
  • -o 只输出文件中匹配到的部分
1
grep -o -E "[1-9]+" file
  • -c 文件中包含匹配模式的行数
1
grep -c "text" file
  • -n 输出包含匹配字符串所在的行号
1
grep -n "text" file
  • -b 匹配模式位于的字符或字节偏移
1
2
# 一般与 -o 一起使用,从第一个字符开始计算,起始值为 0 
grep -b -o "text" file
  • -l 搜索匹配模式出现在哪些文件中
1
2
grep -l "text" file1 file2 file3 ...
# 只输出文件名,不指定 -l, 输出文件名及相应的匹配行
  • -r 多级目录中对文本进行递归搜索
1
2
grep "text" . -r -n
# . 表示当前目录
  • -i 忽略匹配模式中字符的大小写
1
grep -i "text" file
  • -e 制定多个匹配模式
1
2
grep -e "text1" "text2" file
# 这个当然也可以直接用正则表达式写
  • 搜索结果中包括或者排除指定文件
1
2
3
4
5
6
7
8
# 此参数的格式为 Ubuntun 下的格式,搜索文件只包括 txt,html,php 文件 
grep --include=*.{txt,html,php} "text" file

# 搜索文件不包括 txt,html,php 文件
grep --exclude=*.{txt,html,php} "text" file

# 排除 fileList 文件列表里的文件
grep "text" . -r --exclude-from=fileList
  • -q 静默输出

  • 打印匹配某个结果之后或之前的 n 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#显示匹配某个结果之后的3行,使用 -A 选项:
seq 10 | grep "5" -A 3
5
6
7
8


#显示匹配某个结果之前的3行,使用 -B 选项:
seq 10 | grep "5" -B 3
2
3
4
5

#显示匹配某个结果的前三行和后三行,使用 -C 选项:
seq 10 | grep "5" -C 3
2
3
4
5
6
7
8

#如果匹配结果有多个,会用“--”作为各匹配结果之间的分隔符:
echo -e "a\nb\nc\na\nb\nc" | grep a -A 1
a
b
--
a
b

stat

stat 命令用于显示文件的大小、所有者、用户组、索引节点号、权限、修改和访问时间等重要的统计信息。这是一个非常有用的命令,可以显示比 ls -l 更多的细节。

  • example
1
2
# -c 用于格式化输出, %n 表示文件的名称 %a 表示数字格式的权限
stat -c '%n %a' *
1
ls * ""

https://linux.cn/article-9601-1.html