MySQL Learning

MySQL Linux安装

  1. hadoop102节点上执行MySQL安装命令:
sudo apt update
sudo apt install mysql-server
  1. 启动MySQL并检查启动状态
systemctl start mysql
systemctl status mysql
  1. 为root用户创建密码:
ALTER USER 'root'@localhost IDENTIFIED BY '123456';

删除MySQL

systemctl stop mysql
apt-get remove --purge mysql-server mysql-client mysql-common
rm -rf /etc/mysql /var/lib/mysql
reboot

MySQL Basic

Log in to MySQL

Check if MySQL server is running.

sudo service mysql status
mysql -u root -p

Create/Drop a database

Create a database, a user and his privileges.

CREATE DATABASE my_database_name;
DROP DATABASE my_database_name;
USE my_database_name;

Create/Drop a user

Create a user and his privileges.

CREATE USER 'springuser'@'%' IDENTIFIED BY '123456'; -- Create the user
GRANT ALL ON db_example.* TO 'springuser'@'%'; -- Give all privileges to the new user 
DROP USER 'springuser'@'%';  -- Delete a user; 

springuser is the user name; '%' is a wildcard which means that the user name can be logged in at whatever hostname or ip. And '%' can be either 'localhost' or '192.168.1.100' which will limit the log in privilege.

123456 is the password of user “springuser”.

SELECT user FROM mysql.user;
DROP user 'springuser'@'%';

If I create a user using CREATE USER 'zt'@'192.168.0.45' IDENTIFIED BY '123456';, then I must set bind-address=0.0.0.0 in /etc/mysql/mysql.conf.d/mysqld.cnf before I login with code mysql -u zt -p -h 192.168.0.45.

192.168.0.45 in mysql -u zt -p -h 192.168.0.45 means the target login server IP.

Create/Drop/ALTER a table

CREATE TABLE mytable (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
name varchar(255) NOT NULL,
email varchar(255));  -- create a table

CREATE TABLE mynewtable AS 
SELECT column1, column2 
FROM mytable
WHERE column1 = 'W';  -- create a table from an old table
DROP TABLE mytable; -- delete the table
TRUNCATE TABLE mytable;  -- clear the table
ALTER TABLE mytable
ADD COLUMN gender varchar(255) NOT NULL DEFAULT 'male'
AFTER name; -- add a new column

ALTER TABLE mytable
DROP COLUMN gender;  -- drop a column

ALTER TABLE mytable
MODIFY COLUMN gender char(1);  -- change data type of a column

CONSTRAINT

  • NOT NULL: a column cannot have a NULL value

  • UNIQUE: all values in a column are different.

  • PRIMARY KEY: NOT NULL + ‘UNIQUE’

  • CHECK: ensure values in a column satisfies a specific condition

  • DEFAULT: (It won’t change the already existed “NULL” into a default value.)

  • CREATE INDEX: use to create and retrieve data from database very quickly.

Show constraint information of a table:

SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, COLUMN_TYPE, COLUMN_KEY, EXTRA  FROM INFORMATION_SCHEMA.COLUMNS  WHERE TABLE_NAME = 'table1';

UNIQUE

The constraint name is “UC_mytable”. The column id and name are constrained as UNIQUE.

CREATE TABLE mytable (
id int NOT NULL,
name varchar(255),
email varchar(255),
CONSTRAINT UC_mytable UNIQUE (id, name));  -- use constraint when creating a table

ALTER TABLE mytable
ADD CONSTRAINT UC_mytable UNIQUE (id, name);  -- use constraint when altering a table

ALTER TABLE mytable
DROP INDEX UC_mytable;

PRIMARY KEY

A table can only have one PRIMARY KEY. But PRIMARY KEY can be made of more than one different columns.

CREATE TABLE mytable (
id int NOT NULL,
name varchar(255) NOT NULL,
email varchar(255),
CONSTRAINT UC_mytable PRIMARY KEY (id, name));  -- use constraint when creating a table

ALTER TABLE mytable
ADD CONSTRAINT PK_mytable PRIMARY KEY (id, name);  -- use constraint when altering a table

ALTER TABLE mytable
DROP PRIMARY KEY;

PRIMARY KEY (id, name) means neither id nor name can be NULL, and id can be duplicate alone, name can be duplicate, but id + name cannot be duplicate.

FOREIGN KEY

CREATE TABLE table1 (
id INT NOT NULL,
name VARCHAR(225) NOT NULL,
PRIMARY KEY (id));

CREATE TABLE table2 (
id INT NOT NULL,
email VARCHAR(255) NOR NULL,
FOREIGN KEY (id) REFERENCES table1(id));  -- foreign key in table2 pointing to primary key in table1.

ALTER TABLE table
ADD CONSTRAINT FK_table2 
FOREIGN KEY (id) REFERENCES table1(id);  -- add a foreign key

ALTER TABLE table2
DROP FOREIGN KEY FK_mytable2;  -- drop a foreign key
table1:
\(id\) \(name\)
\(1\) \(ZT\)
\(2\) \(WQ\)
\(3\) \(ZKS\)
table2:
\(id\) \(name\)
\(1\) \(zouxiaotao@163.com\)
\(1\) \(894905246@qq.com\)
\(2\) \(quanwang@163.com\)
\(3\) \(kuisongzhu@163.com\)

If I want to insert a “4, ” into table2, there will throw an error. Because I’m to change the foreign key aligned to primary key (1, 2, 3) only in table1.

CHECK

CREATE TABLE mytable (
id int NOT NULL,
name VARCHAR(255),
age int,
CHECK (age>=18));  -- define a CHECK CONSTRAINT without name when creating a table

CREATE TABLE mytable (
id int NOT NULL,
name VARCHAR(255),
age int,
CONSTRAINT CHK_mytable CHECK (age>=18 AND id<10));  -- define a CHECK CONSTRAINT with a name when creating a table
ALTER TABLE mytable
ADD CHECK (age>=18);
--or
ALTER TABLE mytable
ADD CONSTRAINT CHK_mytable CHECK (age>=18 AND id<10);

ALTER TABLE mytable
DROP CHECK CHK_mytable;

CREATE INDEX

/*INDEX is a kind of data structure which is stored on disk.
It will facilitate: point query, range query, ordering and join. There is no need to scan the whole table for indexed column.
Shortcomings: modifying an indexed column will be slower*/
CREATE INDEX idx_email ON mytable (email);

AUTO_INCREMENT

CREATE TABLE mytable (
id int NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
PRIMARY KEY (id));

INSERT INTO mytable (name)
VALUES ('ZT');  -- I don't need to INSERT INTO the AUTO_INCREMENT column.

MySQL Data Type

\(Data Type\) \(Description\)
\(\text{CHAR(size)}\) \(fixed length; size\in\{0,\cdots,255(2^8-1)\}\)
\(\text{VARCHAR(size)}\) \(size\in\{0,\cdots,65535(2^16-1)\}\)
\(\text{INT}\) \(\{-2147483648(-2^{32-1}),\cdots, 2147483647(2^{32-1}-1)\}\)
\(\text{BIGINT}\) \(\{-2^{64-1},\cdots, 2^{64-1}-1\}\)
\(\text{DATE}\) YYYY-mm-dd
\(\text{DECIMAL(M, N)}\) \(M\in\{1,\cdots,65(2^6+1)\}, D\in\{1,\cdots,30\}\text{且} D\leq M\)

CHAR创建时分配固定长度的空间;VARCHAR创建时根据实际存储值的大小动态分配空间。

INSERT

INSERT INTO my_database_name.my_table_name (id, email, name)
VALUES 
(1, 'zouxiaotao886@163.com', 'Tom ZOU'),
(2, '1234546789@qq.com', 'Jack Smith');
INSERT INTO mytable1 (id, email, name)
SELECT id, email, name FROM mytable2;

SELECT

DISTINCT

SELECT DISTINCT * FROM mytable; -- select different rows.
mytable:
\(Column1\) \(Column2\)
\(A\) \(X\)
\(A\) \(Y\)
\(B\) \(Z\)
\(C\) \(Z\)
\(A\) \(Z\)
\(B\) \(Z\)
result:
\(Column1\) \(Column2\)
\(A\) \(X\)
\(A\) \(Y\)
\(B\) \(Z\)
\(C\) \(Z\)
\(A\) \(Z\)

ORDER BY

-- ORDER BY clause
-- DESC clause means to sort reversely
SELECT column1, column2, column3
FROM myBD
ORDER BY column2 DESC, column3 ASC;

WHERE

-- WHERE clause
-- Some WHERE clause operators: >, >=, !=, =
-- ORDER BY clause should be placed behind WHERE
SELECT column1, column2, column3
FROM mytable
WHERE column1 BETWEEN 1 AND 10;

SELECT mytable, column2, column3
FROM myBD
WHERE column1 IS NULL AND column2 IS NOT NULL;

-- When don't use "()", AND is prior to OR
SELECT column1, column2, column3
FROM mytable
WHERE column1 IN (1, 0) OR column2 = 0 AND NOT column3 < 100;

SELECT column1, column2, column3
FROM mytable1
WHERE column1 IN (SELECT column1 FROM mytable2);

LIMIT

LIMIT can specify the number of records to return. It is useful on large tables.

SELECT * FROM my_table_name
WHERE column1='A'
LIMIT 3;  -- Show the first three records.

SELECT * FROM my_table_name
WHERE column1='A'
LIMIT 3 OFFSET 3;  -- Show the records 4 5 6.

LIKE(wildcard)

-- % represents a set of character including " " and "".
-- _ represents one character including " ".
-- A returned example: 'Fish bean bay toy'
-- Keep in mind that pattern doesn't split strings. It just match and return the whole content.
SELECT column1, column2, column3
FROM myBD
WHERE column1 LIKE 'F_sh%';
SELECT column1, column2, column3
FROM myBD
WHERE column1 LIKE '%_%_%';  -- Match value that contains at least two characters.

REGEXP(regular expression)

-- I can also use regular expression through clause REGEXP
SELECT column1, column2, column3
FROM myBD
WHERE column1 REGEXP '^[FK]';

select two tables

The results of two columns will be boardcasted together.

SELECT t1.column1, t2.column2
FROM table1 AS t1, table2 AS t2;

UPDATE/DELETE

UPDATE my_table_name
SET column1 = 'Z', column = 'T'
WHERE column3 = 'W';  -- The WHERE clause should not be omitted usually.
DELETE FROM my_table_name
WHERE column3 = 'W';  -- The WHERE clause should not be omitted usually.

Function

MIN, MAX

SELECT MIN(column1)  -- Return the minimum value of column1.
FROM my_table_name
WHERE column3 = 'W';
SELECT MAX(column1) AS maximal_column1
FROM my_table_name
WHERE column3 = 'W';

AVG, SUM

AVG and SUM are used to deal with numeric column.

SELECT AVG(column1), SUM(column1)
FROM my_table_name
WHERE column3 = 'W';

COUNT

COUNT returns the number of rows that matches a specified criterion.

SELECT COUNT(column1)
FROM my_table_name
WHERE column3 = 'W';

User-designed Function

DROP FUNCTION IF EXISTS getNthHighestSalary;

DELIMITER $$
CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT
DETERMINISTIC  -- 明确指定函数为确定性函数
BEGIN
    DECLARE offset_val INT;
    SET offset_val = N - 1;
    RETURN (
        SELECT (
            SELECT DISTINCT Chinese 
            FROM student
            ORDER BY Chinese DESC
            LIMIT 1 OFFSET offset_val
        )
    );
END $$
DELIMITER ;

JOIN/UNION

JOIN

SELECT * 
FROM table1 AS t1 INNER JOIN table2 AS t2 ON t1.column1=t2.column1;

SELECT * 
FROM table1 AS t1 LEFT JOIN table2 AS t2 ON t1.column1=t2.column1;

SELECT * 
FROM table1 AS t1 RIGHT JOIN table2 AS t2 ON t1.column1=t2.column1;

SELECT * 
FROM table1 CROSS JOIN table2;

CROSS JOIN is different from the other three JOIN. It means Cartesian product and it should not be used with on.

UNION

SELECT column1 FROM table1
UNION
SELECT column2 FROM table2;  -- union distinct values

SELECT column1 FROM table1
UNION ALL
SELECT column2 FROM table2;  -- union all values

GROUP BY/HAVING

SELECT COUNT(id), country FROM table1
GROUP BY country
HAVING COUNT(id)>2
ORDER BY COUNT(id) DESC;  -- Count number of people from different countries.

EXISTS/ALL/ANY

EXISTS

EXISTS returns TRUE if the subquery returns one or more records.

SELECT ProductName 
FROM Products
WHERE EXISTS (SELECT ProductID FROM OrderDetails WHERE Quantity = 10);

ALL/ANY

SELECT ProductName 
FROM Products
WHERE ProductID = ANY (SELECT ProductID FROM OrderDetails WHERE Quantity = 10);
-- For each ProductID, if it equals to one of the ProductID from OrderDetails.
SELECT ProductName 
FROM Products
WHERE ProductID > ALL (SELECT ProductID FROM OrderDetails WHERE Quantity = 10);
-- For each ProductID, if it is larger than all of the ProductID from OrderDetails.

CASE

SELECT orderid, quantity
CASE
WHEN quantity > 30 THEN '>30'
WHEN quantity = 30 THEN '=30'
ELSE '<30'
END AS quantity_text
FROM orderdetails;

Sort by city’s name, and sort by country when city’s name is null.

SELECT name, city, country
FROM customers
ORDER BY 
(CASE 
WHEN city IS NULL THEN country
ELSE city
END);

IFNULL

If IFNULL meets a NULL, then the NULL will be replaced by a user-defined value.

SELECT name, IFNULL(weight, 100) / POW(height, 2)
FROM mytable;

VIEW

VIEW is a virtual table generated from other table(s). When other table changes, the VIEW table will be changed correspondingly.

-- Create a view
CREATE VIEW my_view AS
SELECT id, name FROM mytable;

-- Update a view
CREATE OR REPLACE VIEW my_view AS
SELECT id, name, gender FROM mytable;

-- Drop a view
DROP VIEW my_view

常用函数

LATERAL VIEW

LATERAL VIEW用于关联横向展开的数组或Map类型字段,对样本行扩展。

SELECT  *
FROM    mytable
LATERAL VIEW EXPLODE(SPLIT(hobbies, ','));

EXPLODE()用于将数组进行行展开,LATERAL VIEW再去跟原表的每一行关联。

注:如果EXPLODE()返回的结果是NULL或空列表,LATERAL VIEW去跟原表关联时会删除原表的对应行,为了避免删除原表行,使用LATERAL VIEW OUTER

DBeaver连接MySQL

参照https://blog.csdn.net/LogosTR_/article/details/125602116完成配置。

  1. 对于MySQL8.0,先修改配置文件,注释掉地址绑定:# bind-address = 127.0.0.1
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
  1. 重启mysql,修改root用户的登录地址,并授权。
# 重启mysql
sudo service mysql restart
# 修改root用户登录地址、授权、刷新
USE mysql;
UPDATE user SET host='%' WHERE user='root';
FLUSH PRIVILEGES;

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;

FLUSH PRIVILEGES;

至此,可以通过DBeaver连接虚拟机上的MySQL数据库了。

MySQL体系结构

  • 连接层:接收客户端的连接、认证授权、校验每一个客户端具有的权限。

  • 服务层:SQL接口、解析器、查询优化器、缓存。

  • 引擎层:MySQL提供很多可插拔式的存储引擎,如InnoDB等。索引是在存储引擎层实现的。

  • 存储层:数据最终是存储在磁盘当中的。像Redo、Index等日志都会存储在磁盘当中。

存储引擎

存储引擎是基于表的,不同表可以有不同的存储引擎(表的类型)。执行SHOW CREATE TABLE students;,展示建表SQL。其中ENGINE指定了默认的存储引擎InnoDB;自增列id的下一条插入数据的值应该为4;DEFAULT CHARSET指定当前表的字符集是utf8mb4

CREATE TABLE `students` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `score` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

执行SHOW ENGINES;展示当前数据库所支持的存储引擎。

InnoDB存储引擎

  1. DML操作遵循ACID模型(原子性、一致性、隔离性、持久性),支持事务。

  2. 行级锁,提高并发访问性能。

  3. 支持外键FOREIGN KEY约束。

适用场景:应用对事务的完整性有比较高的要求。

InnoDB的物理存储结构

每个基于InnoDB表的表文件存储在磁盘的.ibd表空间文件中。执行SHOW VARIABLES LIKE 'innodb_file_per_table';可以查看此时是否设置为每个表存储在每个.ibd文件中。

/var/lib/mysql/my_database_name目录下就可以找到我创建表的table_name.ibd文件。

InnoDB的逻辑存储结构

InnoDB是以页(默认大小16KB)为基本单位存储数据表的。即使一张表的所有数据远小于一个页的大小,InnoDB仍然会分配16KB的空间来存储该表。

MyISAM存储引擎 (已被MongoDB替代)

  1. 不支持事务,不支持外键。

  2. 支持表锁,不支持行锁。

  3. 访问速度快。

基于MyISAM引擎的表的磁盘存储文件为table_name.MYD(数据),table_name.MYI(索引)以及table_name.sdi(表结构)。

适用场景:应用是以读取和插入操作为主,如日志、电商中的足迹、评论相关数据。

Memory存储引擎 (已被Redis替代)

表数据是存储在内存中的,因此它对应的磁盘存储文件只有table_name.sdi(表结构)。

  1. 内存存放。

  2. Hash索引。

适用场景:常用于临时表、缓存,但表的大小有限制,无法保证安全性。

索引

  • B+Tree索引:(最常见)。

  • Hash索引:只支持精准查询,不支持范围查询。属于Memory引擎。

  • R-Tree索引:空间索引属于MyISAM引擎,用于地理位置数据类型。

  • Full-text索引

B+Tree

普通二叉树或平衡二叉树(红黑树)的缺点都是:在大数据量的情况下层级较深,导致检索速度慢。(我就奇怪了,\(log_2^n\)即使是在\(n\)很大的情况下也增长得很慢呀?)

B-Tree,下图展示的是一个5阶B-Tree,一个节点可以最多存储4个数,分出5个节点。

B+Tree。1. 所有的数据都会出现在叶子节点。2. 叶子节点形成一个单向链表。

MySQL中的B+Tree,相比于传统B+Tree增加了双向指针。对于下述图片,是存储在磁盘上的。非叶子只存放索引而不存放数据可以提高索引存储容量,进而进一步减小树的深度。

索引分类

  • 主键索引:PRIMARY
  • 唯一索引:UNIQUE
  • 常规索引:快速定位特定数据
  • 全文索引:查找文本中的关键词,FULLTEXT

在InnoDB引擎中,聚集索引Clustered Index只有一个,通常是索引的PRIMARY那一列;若没有PRIMARY,则索引第一个UNIQUE列;若都没有,则自动生成rowid作为索引列。聚集索引的每个叶子节点对应表中的每个行数据。

二级索引Second Index用于辅助聚集索引,其每个叶子对应每个聚集索引列数据。

当使用SELECT sex FROM user WHERE name='Jack';时,先走二级索引,提取出名字为Jack的行对应的聚集索引,然后走聚集索引找到对应的行数据,最后从行中找到sex值。即回表查询。

建立索引

-- 创建索引
CREATE [UNIQUE|FULLTEXT] INDEX index_name ON table_name (column1, column2, ...);
-- 查看索引(可在下面的SQL代码后加上"\G"以纵向展示结果)
-- 默认使用FULLTEXT为索引。
SHOW INDEX FROM table_name;
-- 删除索引
DROP INDEX index_name ON table_name;

为对查询功能需求不大的表做索引是没有必要的。下述命令可以查看整个MySQL服务的增删查改频次:

SHOW GLOBAL STATUS LIKE 'Com_______';  -- 七个下划线

使用索引

  • 索引的使用要符合最左前缀法则,否则联合索引会部分失效。

  • 联合索引中,范围查询的右边的列的索引会失效。例如:

-- 业务场景下更推荐使用>=(我目前还不理解这是为什么)
SELECT * FROM student WHERE age>30 AND name='Jack';
  • 对索引列使用运算函数会导致索引失效。

  • 尾部模糊匹配,索引列不是失效;头部模糊匹配,索引列失效。

  • 如果在某些情况下使用索引的查询速度更慢,MySQL会选择使用全局扫描不使用索引。

  • 当使用OR连接符时,若左右两个条件一个有索引一个没有,则MySQL可能不会使用索引查询;相同情况下,当使用AND连接符时,MySQL会使用索引查询。

  • 尽量避免使用SELECT *,因为它会涉及到回表查询。

  • 某列不存储NULL值,则最好声明其NOT NULL属性,这样可以优化器可以更好地确定使用最有地索引。

强制使用索引

SELECT * FORM mytable USE INDEX(index_name) WHERE ...;
SELECT * FORM mytable IGNORE INDEX(index_name) WHERE ...;
SELECT * FORM mytable FORCE INDEX(index_name) WHERE ...;

使用前缀索引(针对长文本)

抽取长文本的前n个字符来创建索引:

CREATE INDEX index_name ON table_name(column1(n));

前缀索引可能导致同一索引对应多条数据的情况,当选择性为1时性能时,每个索引都对应不同的数据。

SELECT COUNT(DISTINCT SUBSTRING(column1, 1, 10)) / COUNT (*) FROM table_name;

慢查询日志

-- 查看MySQL慢查询日志是否打开
SHOW VARIABLES LIKE 'slow_query_log';

/etc/mysql/mysql.conf.d/mysqld.cnf中添加下述信息

# 开启慢查询日志开关
slow_query_log=1
# 设置慢查询时间阈值,如果SQL执行超过2秒就会被视为慢查询,
# 此时记录到慢查询日志当中。
long_query_time=2
# 设置慢查询日志文件的路径
slow_query_log_file = /var/log/mysql/mysql-slow.log

使用tail -f mysql-slow.log可以实时展示该文件中被追加的文本内容。

profiles查询每条SQL执行时间

profiles用于记录每一条SQL语句的执行时间。

-- 查看数据库是否支持profiles
SELECT @@have_profiling;
-- 开启profiles
SELECT @@profiling;  -- 为0则表示未开启
SET @@profiling=1;
SHOW profiles;  -- 查询当前会话所有SQL语句的执行耗时情况
SHOW profile FOR QUERY query_id;  -- 单独查看某条SQL语句的具体耗时情况
SHOW profile CPU FOR QUERY query_id;  -- 单独查看某条SQL语句的具体耗时情况(CPU)

EXPLAIN解释SQL语句执行详情

EXPLAIN SELECT * FROM student WHERE id=(SELECT id FROM course ORDER BY id LIMIT 1);

上述语句会展示查询涉及到的每一个表的具体执行情况。 - id: id值越大的表越先执行,同id值得所有表按照从上到下得顺序执行。

  • select_type: SIMPLE(最普通的查询)、PRIMARY(主查询)、UNION、SUBQUERY。

  • type: 表示查询性能,性能由好到差依次为:NULL、system、const、eq_ref、ref、range、index、all。根据PARIMARY或UNIQUE索引进行访问时,查询的性能时const。

  • pssible_key: 在要查询的表当中可能用到的索引。

  • key: 实际用到的索引。

  • key_len: 索引字段最大可能的长度。

  • rows: 要执行查询的行数(估计值)。

  • filtered: 返回结果的行数占读取行数的百分比,显然filtered值越大SQL性能越好。

  • Extra: 如果出现using index condition,意味着查询使用了索引但是需要回表查询;如果出现using where; using index,意味着数据能在索引中找到,不需要回表查询。

SQL优化

插入数据优化

建议使用批量插入而不要使用单条数据插入

-- 批量插入
INSERT INTO mytable VALUES (1, 'Tom'), (2, 'Jack'), (3, 'Lucas');

执行一条SQL语句,该语句会被自动提交事务并执行。可采用手动提交事务的方式实现以单条数据插入的方式插入多条数据。

-- 手动提交事务
START TRANSACTION;
INSERT INTO mytable VALUES (1, 'Tom');
INSERT INTO mytable VALUES (2, 'Jack');
INSERT INTO mytable VALUES (3, 'Lucas');
COMMIT;

按照主键的顺序插入

由InnoDB的聚集索引特点所决定。

大量数据的.sql文件插入用LOAD插入

假设下述文件是sql1.sql

1, Tom
2, Jack
3, Lucas
## 在客户端连接MySQL服务器时,加上参数--local-infile
mysql --local-infile -u root -p
-- 开启在本从本地加载文件导入数据的开关。通过SELECT @@local_infile可以查看开关状态。
SET GLOBAL local_infile=1;
-- 将.sql文件加载到目标表中
LOAD DATA LOCAL INFILE '/root/sql1.sql' INTO TABLE `mytable` FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';

主键优化

  1. 主键的长度尽可能小,UID和身份证号等过长不宜作为主键。
  2. 插入数据时选择顺序插入,选择使用AUTO_INCREMENT自增主键。
  3. 尽量避免对主键的修改。

ORDER BY 优化

filesort单独设立缓存区对查询的所有数据进行排序,这种排序的效率较低。

index直接通过字段索引返回了有序的数据,这种排序的效率较高。

-- 假设对age,phone创建了一个联合索引(默认以升序构建索引)
SELECT id, age, phone ... ORDER BY age;  -- index
SELECT id, age, phone ... ORDER BY age, phone;  -- index
SELECT id, age, phone ... ORDER BY age DESC, phone, DESC;  -- index (backward scan)
SELECT id, age, phone ... ORDER BY age ASC, phone DESC; -- index & filesort:对于age使用index,对于phone使用filesort。
SELECT id, age, phone ... ORDER BY phone, age; -- filesort & index:对于phone使用filesort,对于age使用index

对于上述第3条和第4条SQL的差异我暂时不理解。不过对两个联合索引使用顺序一致的查询可以避免filesort。 为了解决第4条SQL的问题,可以创建联合索引并指定构建顺序:CREATE INDEX idx_mytable_age_phone_ad ON mytable(age ASC, phone DESC);

当遇到大数据量排序时,且不可避免要使用filesort时,可以手动通过SET sort_buffer_size=1048576 -- 1MB来增加缓冲区大小(默认为256K)。

GROUP BY 优化

MVCC机制

MVCC(Multi-Version Concurrency Control)是InnoDB引擎的并发控制。

事务隔离级别

  • 读未提交:会出现脏读。

  • 读已提交(MVCC)

  • 重复读(MVCC):在同一个事务中的读操作的读取内容会使完全一样的,因为它只生成一次ReadView。

  • 串行化:修改表数据会触发表锁,无并发性。

隐式字段、undo日志

一个真实的table还包含三个隐藏字段:DB_ROW_ID(隐含的自增ID)、DB_TRX_ID(最近修改的事务ID)、DB_ROLL_PTR(回滚指针)。

name age DB_ROW_ID DB_TRX_ID DB_ROLL_PTR
Jerry 24 1 1 0x12446545

下面的两个图展示两次UPDATE语句修改表格中的内容所造成的undo日志的变化以及表格隐式字段的变化。

MVCC机制流程

事务1(ID 10) 事务2(ID 20)
START TRANSACTION START TRANSACTION
UPDATE mytable
SET name=‘Tom’
LIMIT 1;
SELECT *
FROM mytable;
COMMIT; COMMIT;

在事务2的查询语句开始执行时,会创建一个ReadView,在ReadView中用creator_trx_id来存储当前事务的ID 20,用m_ids来存储当前正在并行的、且不大于当前事务ID的所有事务的ID,包括当前的事务ID,用max_trx_id来存储当前事务ID+1用于表示未开始的事务。

ReadView:

creator_trx_id m_ids max_trx_id
20 10、20 21

之后依次进行下述判断:

  1. 将要查询数据的DB_TRX_ID与creator_trx_id对比,如果DB_TRX_ID=creator_trx_id,则说明当前要查询的数据已经在当前事务中被保存,可以直接读取。

  2. 将要查询数据的DB_TRX_ID与m_ids对比,如果DB_TRX_ID小于m_ids中的最小值,则说明当前要查询的数据的事务提交已经结束,可以直接读取数据。

  3. 将要查询数据的DB_TRX_ID与max_trx_id对比,如果DB_TRX_ID大于等于max_trx_id,则说明当前要查询的数据的事务位于事务2之后,不能直接读取,要开始在undo log进行回溯。

  4. 如果DB_TRX_ID在m_ids之间,此时说明要查询的数据正在并发过程当中但还未提交,需要在undo log中进行回溯。

个别问题说明

  1. 在MySQL中,除了TRUEFALSE外还有一个UNKNOWNTRUE and UNKNOWN = UNKNOWNFALSE and UNKNOWN = FALSE。与NULL的比较、判断会得到UNKNOWN

窗口函数

LAG()

Leetcode SQL 180题

求连续出现至少三次的数字。

+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| id          | int     |
| num         | varchar |
+-------------+---------+
在 SQL 中,id 是该表的主键。
id 是一个自增列。

解答:

WITH myWindowFunc AS (
    SELECT id, num, LAG(num, 1) OVER (ORDER BY id) as prev_num1, LAG(num, 2) OVER (ORDER BY id) as prev_num2 FROM Logs
)
SELECT DISTINCT num as ConsecutiveNums
FROM myWindowFunc
WHERE num=prev_num1 AND num=prev_num2;

DENSE_RANK()

生成一个列:表中的score字段的密度秩

SELECT score, DENSE_RANK() OVER(ORDER BY score DESC) AS `rank`
FROM Scores
ORDER BY score DESC;

如果score字段中只有一个NULL值,那么DENSE_RANK()会返回一个秩1,而不是返回一个NULL

MAX()

生成一个列:每个部门的所有员工的工资最大值。

SELECT *, SUM(salary) OVER(PARTITION BY department) AS maxSalaryPerDepartment
FROM mytable;

ROW_NUMBER()

ROW_NUMBER() OVER(PARTITION BY gender ORDER BY grade) AS rm

在男生里根据成绩单独排个序;在女生里根据成绩单独排个序。序号从1开始,连续不间断、不重复(即使grade有重复值或是空值,rm也能满足该特点)。

ODPS

ODPS特性

  • 没有事务、没有主键、没有索引、没有游标、没有存储过程。

  • 不支持UPDATEDELETE

哈希分桶

CREATE TABLE dwd_trd_alipay_order_dd (
    order_id STRING COMMENT '订单id',
    buyer_id STRING COMMENT '买家id',
    seller_id STRING COMMENT '卖家id',
    gmt_pay STRING COMMENT '付款时间'
) COMMENT '订单表'
PARTITIONED BY (dt STRING COMMENT '日期分区yyyymmdd')
CLUSTERED BY (order_id) SORTED BY (order_id) INTO 64 BUCKETS,
LIFECYCLE 365;

上述CLUSTERED BY子句对order_id列进行hash,并按顺序将这些记录存储再不同的桶中。

  • 建议CLUSTERED BYSORTED BY的对象列保持一致,以便于ODPS在查询时自动生成索引,加快执行速度。

  • 建议每个Bucket的数据大小在500MB到1GB之间。

回收站

表/分区被删除或生命周期回收后,会进入回收站保存1-2天。

-- 获取logid
SHOW CHANGELOGS FOR TABLE mytable PARTITION (dt=20250801);

-- 恢复指定的logid
UNDO TABLE mytable PARTITION (dt=20250801) TO <logid>;

UDF

阿里云UDF开发内部文档支持Python、Java开发。

UDF开发资源Resources:(后面补充)。

数据仓库

层次 缩写 说明
原始数据层 ODS 日级增量表、小时增量表、全量快照表、实时表;不做任何加工,数据永久保留
明细整合层 DWD 多表整合,通用,保存全量明细。
汇总层 DWS 日汇总表、月汇总表、按业务要求汇总表
应用层 ADM 个性化、数据集市、BI报表、运营、算法

数据仓库命名规范

物理表名 物理表含义
ods_*_delta 每个分区存储天增量
ods_* 每个分区存储天全量
ods_*_delta_hi 每个分区存储当前小时增量
ods_*_delta_thi 每个分区存储0点截至当前小时增量
ods_*_delta_hd 每个分区存储历史截止当前小时全量
dwd_*_di 每个分区存储天增量
dwd_*_dd 每个分区存储天全量
dwd_*_da 累计快照事实表
dws_*_1d 最近一天汇总表
dws_*_nd 最近n天汇总表
dws_*_cw 自然周汇总表
dws_*_cm 自然月汇总表

ODPS性能优化

数据资产平台查看资产算力消耗。 sql作业队列查看平台作业队列查看。

GROUP BY数据倾斜优化

SET odps.sql.groupby.skewindata=true;

防止在GROUP BY操作在遇到数据倾斜时任务进度被一直卡住在99%(少量REDUCE任务未完成)

Map一读多写

FROM mytable
INSERT OVERWRITE TABLE mytable1 PARTITION (dt=20250801)
SELECT id, name
WHERE region='China'
INSERT OVERWRITE TABLE mytable2 PARTITION (dt=20250801)
SELECT id, name
WHERE region='Japan'

一次读表多次写入,节省I/O资源。

慎用DISTINCT

慎用全局排序

测试表

student

CREATE TABLE IF NOT EXISTS `student` (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, sex ENUM('m', 'f'), phone BIGINT, Chinese FLOAT(5, 2), English FLOAT(5, 2), Math FLOAT(5, 2), CONSTRAINT score_check CHECK (Chinese>=0 AND Chinese <=100 AND English>=0 AND English<=100 AND Math>=0 AND Math<=100));

INSERT INTO `student` (name, sex, phone, Chinese, English, Math)
VALUES
('zt', 'm', 15386648422, 90.12, 92.00, 92),
('Lucas', 'm', 19833718892, 100, 60.0, 00.00),
('Bella', 'f', 19766539910, 100, 100, 100),
('Nancy', 'f', 13877461178, 60.00, 96, 46),
('Tom', 'm', 13877461123, 100, 100, 100),
('Jack', 'm', NULL, 41, 29, 99),
('Lucy', 'f', 18622115566,0, 0, 0);

exam

CREATE TABLE IF NOT EXISTS `exam` (id BIGINT DEFAULT 7, exam_time DATE, Chinese FLOAT(5, 2), Math FLOAT(5, 2), CONSTRAINT exam_score_check CHECK (Chinese>=0 AND Chinese <=100 AND Math>=0 AND Math<=100));

INSERT INTO `exam` (exam_time, Chinese, Math)
VALUES
('2024-01-01', 50, 60),
('2024-02-01', 69, 85),
('2024-03-11', 12, 91),
('2024-04-01', 54, 54),
('2024-05-01', 91, 31),
('2024-06-21', 92, 21),
('2024-07-31', 89, 100),
('2024-08-17', 90, 100),
('2024-09-18', 95, 80);