跳转至

SQL 语法

在不同的数据库管理系统中,实例和模式都有不同的概念。在 MySQL 中,分别有如下意义:

  • 实例(instance):一个操作系统中启动中的一个 mysqld 守护进程被称为一个数据库实例。
  • 数据库(database):存放多个相关业务数据表的逻辑对象。
  • 模式(schema):MySQL 中 schema 等同于 database。

可以在MySQL 术语表中查到上面这些定义。

在关系型数据库术语中,模式(schema)是一个逻辑概念,通常被看作是一个数据库对象的集合,用于组织数据库中的对象。

模式中的对象通常包括表、索引(Tables)、数据类型、序列、视图(Views)、存储过程(Stored Procedures)、主键、外键等等。

一个数据库下面可以包含多个Schema,而每个 Schema 又可以包含多个数据库对象。这种层级结构使得数据库的管理和维护变得更加有序和高效。

然而,并非所有数据库都实现了 Schema 这一层。例如,MySQL 直接将 SchemaDatabase 等效,而在 PostgreSQLOracleSQL Server 等数据库中,Schema 的含义并不完全相同。因此,在实际应用中,我们需要根据所使用的数据库系统来理解 Schema 的具体含义和用法。

模式可以为数据库对象提供逻辑隔离功能,不用应用程序可以使用各自的模式,实现安全的访问权限控制。

  • 对于 MySQL 而言,模式和数据库是同义词。语法中的 SCHEMA 都可以使用 DATABASE 替代,例如 CREATE DATABASE 和 CREATE SCHEMA 的作用完全相同。

  • PostgreSQL 中的一个数据库包含一个或多个模式,一个模式包含多个对象(表、索引、函数等)。模式的管理使用 CREATE SCHEMA、DROP SCHEMA 等语句。

  • Oracle 数据库中的模式概念和其他数据库产品不同,它和用户的概念关系紧密。Oracle 数据库中的一个用户对应一个模式,用户名就是模式名。模式中的对象都属于这个用户。对于模式的管理,实际上就是用户的管理,例如 CREATE USERDROP USER

数据库一词,在不同的上下文有不同的含义,因为它可以是数据库产品、数据库服务(系统)、数据库实例、数据库进程其中之一,而这些名词所表示的是不同的事物。

数据库产品

是指由各数据库厂商提供的产品或服务,如 oracle、mysql、postges,以及各云厂商提供的各种云数据库服务,需要付费或者下载才能得到的物件,此为软件,类似于商品。当拿到数据库产品时,拿到的是一份数据库产品的软件拷贝;而购买在线云数据库服务时,得到的是云数据库的使用权限。

数据库实例

是指将数据库产品的软件安装到硬件环境上后,由操作系统调度所运行起来的一系列程序,它由一个或多个操作系统进程、内存、数据存储单元所构成,即软件程序的运行态。学习过面向对象的同学,就非常清楚“实例”的含义。当数据库系统退出时,数据库实例也就消亡。sqlite 的数据库进程只有一个,而 oracle 、mysql、postgres 这些数据库产品,运行后的实例都由多个数据库进程协调工作。

数据库服务(系统),是指部署了数据库产品后所运行起来后,对外提供了数据操作相关的一系列功能的服务系统,通过程序或者接口来获取这些功能。数据库服务(系统)可由多个数据库实例组成(比如集群)。某些情况,数据库服务与数据库实例很难区分。

SQL 语言概述

SQL 语言概览

SQL 最早是 IBM 公司开发,最初称作 Sequel,后来变为 SQL(结构化查询语言),SQL 的标准是由美国国家标准化组织和国际标准化组织制定。

IBM 对关系数据库以及SQL语言的形成和规范化产生了重大的影响,第一个版本的SQL标准SQL86就是基于System R的手册而来的。Oracle 在 1979 年率先推出了支持SQL的商用产品。随着数据库技术和应用的发展,为不同RDBMS提供一致的语言成了一种现实需要。

SQL标准影响最大的机构自然是那些著名的数据库产商,而具体的制订者则是一些非营利机构,例如**国际标准化组织 ISO**和**美国国家标准委员会 ANSI**。各国通常会按照ISO标准和ANSI标准制定自己的国家标准。

**美国国家标准化组织 ANSI**一个核准多种行业标准的组织。SQL 作为关系型数据库所使用的标准语言,最初是基于 IBM 的实现在 1986 年被批准的。1987 年,**国际标准化组织 ISO**把ANSI SQL作为国际标准。

SQL 有两个主要的标准,分别是SQL92SQL99。92 和 99 代表了标准提出的时间,SQL92就是 92 年提出的标准规范。除了SQL92SQL99以外,还有其他的标准。

下面是 SQL 标准简要的发展与演化历史:

  • 1986 年,ANSI X3.135-1986,ISO/IEC 9075:1986,SQL-86。这是 ANSI 首次将 SQL 语言标准化的版本。
  • 1989 年,ANSI X3.135-1989,ISO/IEC 9075:1989,SQL-89。增加了完整性约束。
  • 1992 年,ANSI X3.135-1992,ISO/IEC 9075:1992,SQL-92(SQL2)。最重要的一个版本。 引入了标准的分级概念。sql92
  • 1999 年,ISO/IEC 9075:1999,SQL:1999(SQL3)。变动最大的一个版本。改变了标准符合程度的定义;增加了面向对象特性、正则表达式、存储过程、Java 等支持。
  • 2003 年,ISO/IEC 9075:2003,SQL:2003。引入了 XML、Window 函数等。
  • 2008 年,ISO/IEC 9075:2008,SQL:2008。引入了 TRUNCATE 等。
  • 2011 年,ISO/IEC 9075:2011,SQL:2011。引入了时序数据等。
  • 2016 年,ISO/IEC 9075:2016,SQL:2016。引入了 JSON 等。
  • 2023 年,ISO/IEC 9075:2023,SQL:2023

细心的读者能发现,从 SQL:1999 开始,标准简称中的短横线(-)被换成了冒号(:),而且标准制定的年份也改用四位数字了。

前一个修改的原因是 ISO 标准习惯上采用冒号,ANSI 标准则一直采用短横线。后一个修改的原因是标准的命名也遇到了 2000 年问题。

SQL86大概只有几十页,SQL92正文大约有 500 页,而 SQL99 则超过了 1000 页。

可以看出,从 SQL99 开始,SQL 标准的个头就非常庞大了,内容包罗万象,可以说已经没有人能够掌握标准的所有内容了。

SQL:2003 为例,它包括以下 9 个部分(中间编号空缺是曾经被占用,之后被废弃的标准造成的)

最新的标准是 SQL:2023 ,网上有一份的SQL-99规范

SQL 语言有以下几个部分:

  • 数据定义语言 DDL:主要是表定义,表删除,表结构修改等语句。常用的语句关键字有 CREATE、DROP、ALTER 等。
  • 数据操纵语言 DML:主要是数据库表的增删改语句。常用的语句关键字有 INSERT、UPDATE、DELETE 等。
  • 数据查询语言 DQL:主要是对数据进行查询操作。常用关键字有 SELECT、FROM、WHERE 等。
  • 数据控制语言 DCL:主要是用来设置/更改数据库用户权限。常用关键字有 GRANT、REVOKE 等。

  • 完整性:SQL DDL 中包括数据定义的完整性的约束。是指数据的一致性和有效性.

  • 视图定义:SQL DDL 包含视图定义的命令
  • 事务控制:SQL
  • 嵌入式和动态 SQL
  • 授权

SQL:2023最新标准包含 11 个部分的内容,具体如下:

  • ISO/IEC 9075-1 信息技术 – 数据库语言 – SQL – 第 1 部分:框架(SQL/框架)
  • ISO/IEC 9075-2 信息技术 – 数据库语言 – SQL – 第 2 部分:基本原则(SQL/基本原则)
  • ISO/IEC 9075-3 信息技术 – 数据库语言 – SQL – 第 3 部分:调用级接口(SQL/CLI)
  • ISO/IEC 9075-4 信息技术 – 数据库语言 – SQL – 第 4 部分:持久存储模块(SQL/PSM)
  • ISO/IEC 9075-9 信息技术 – 数据库语言 – SQL – 第 9 部分:外部数据管理(SQL/MED)
  • ISO/IEC 9075-10 信息技术 – 数据库语言 – SQL – 第 10 部分:对象语言绑定(SQL/OLB)
  • ISO/IEC 9075-11 信息技术 – 数据库语言 – SQL – 第 11 部分:信息与定义概要(SQL/Schemata)
  • ISO/IEC 9075-13 信息技术 – 数据库语言 – SQL – 第 13 部分:使用 Java 编程语言的 SQL 程序与类型(SQL/JRT)
  • ISO/IEC 9075-14 信息技术 – 数据库语言 – SQL – 第 14 部分:XML 相关规范(SQL/XML)
  • ISO/IEC 9075-15 信息技术 – 数据库语言 – SQL – 第 15 部分:多维数组(SQL/MDA)
  • ISO/IEC 9075-16 信息技术 – 数据库语言 – SQL – 第 16 部分:属性图查询(SQL/PGQ)

相比于其他语言,SQL 的"半衰期"可以说是非常长了。SQL有两个重要的标准,分别是SQL92SQL99,它们分别代表了 92 年和 99 年颁布的 SQL 标准,我们今天使用的 SQL 语言依然遵循这些标准。 要知道 92 年是 Windows3.1 发布的时间,如今还有多少人记得它,但如果你从事数据分析,或者和数据相关的工作,依然会用到 SQL 语言。 作为技术和互联网行业的从业人员(一个学习 IT 的人来说),我们总是希望能找到一个通用性强,变化相对少,上手相对容易的语言,SQL 正是为数不多的,可以满足这三个条件的语言。 入门 SQL 并不难。SQL 更像是一门英语,有一些简单的英语单词,当你使用它的时候,就好像在用英语与数据库进行对话。

SQL 标准的符合性

提及标准,大家容易想到的一个问题就是产品对标准的遵循程度,也就是标准的符合性到底如何,因为它会影响代码在不同系统之间迁移的难度。很遗憾的是,SQL 标准因为定义过于宽泛等技术和非技术原因,不同产品对标准的符合程度存在很大的差异。大到功能特性,小到语法语义的细节,在不同产品之间都存在很多差异,造成实际的应用迁移远比 C/C++ 程序的迁移要复杂很多。因此,尽管很多产品都号称自己符合 SQL 标准,并不意味着应用可以容易的在它们之间切换。除了 Oracle、DB2 等经典的商业产品,以及 PostgreSQL、MySQL 等开源产品总体上对 SQL 标准的符合程度较高以外,很多产品提到的 SQL 标准,涉及的内容其实是 SQL92 里头最基本或最核心的一部分(属于入门级的范畴。SQL92 本身是分级的,包括入门级、过渡级、中间级和完全级)。

各国通常会按照 ISO 标准和 ANSI 标准(这两个机构的很多标准是差不多等同的)制定自己的国家标准。中国是 ISO 标准委员会的成员国,也经常翻译一些国际标准对应的中文版。标准为了避免采用具体产品的术语,往往会抽象出很多名词,从而增加了阅读和理解的难度,翻译成中文之后更容易词不达意。对于数据库系统实现者和用户而言,很多时候还不如直接读英文版本为好。

《信息技术 数据库语言 SQL 第 1 部分:框架》

《信息技术 数据库语言 SQL 第 2 部分:基础》

《信息安全技术 数据库管理系统安全技术要求》

为了验证具体的产品对标准的遵从程度,NIST 还曾经专门发起了一个项目,来做标准符合程度的测试集合。不过,SQL 标准包含的内容实在太多了,而且有很多特性对新的 SQL 产品而言也越来越不重要了。从 SQL99 之后,标准中定义就不符合程度的再分级,而是改成了核心兼容性和特性兼容性;也没有机构来推出权威的 SQL 标准符合程度的测试认证了。开源的 MySQL 曾经在 6.0 的开发过程中试图大幅度改善对 SQL 标准的符合程度,不过后来似乎不了了之了。国产数据库作为数据库领域的后来者,曾经对标准的符合程度非常重视,还组织了专门的机构来做产品的标准符合性测试。在数据库百科全书中也有很多有关标准的章节。

SQL,或结构化查询语言,是一种用于管理和操作数据库中数据的强大工具。世界各地的企业、组织和个人都使用它来组织、分析和理解大量信息。

SQL最强大的方面之一是它能够以多种方式过滤、排序和聚合数据。这使它成为一个非常通用的工具,可用于从生成报告到分析客户行为的广泛任务。

要理解为什么以及如何使用SQL,考虑一个真实的示例会很有帮助。假设您经营一家小型在线商店,并拥有一个包含有关您的客户、产品和订单的信息的数据库。您想要分析您的销售数据以更好地了解哪些产品卖得好,哪些产品卖得不好。

一种方法是手动筛选数据库中的数据,寻找模式和趋势。但随着您的业务增长和您必须管理的数据量增加,这变得越来越耗时且容易出错。

这就是SQL的用武之地。通过使用SQL,您可以通过多种方式快速轻松地过滤、排序和聚合数据。例如,您可以使用SQL找出上个月收到的订单数量,或者生成一份报告,显示哪些产品的销售额最高。

SQL 与其他类型的编程语言之间存在一些关键差异。其中最重要的一点是 SQL 是一种声明性语言,这意味着您告诉它您希望它做什么(例如“查找过去一个月的所有订单”),而不是告诉它怎么做(例如“遍历”每个订单并检查日期”)。这使得 SQL 比其他编程语言更易于使用和理解。

另一个关键区别是 SQL 是专门为与关系数据库一起工作而设计的。这意味着它针对连接表、过滤数据和聚合数据等任务进行了优化。

每个关系型数据库都在不同程度上实现 SQL 标准,但是也有自己实现的标准,但是大部分都是基本一致,但是每个数据库都有自己的高级特性和实现标准。

当引入关系模型时,关系模型包含了一种查询数据的新方法:SQL,它是一种 声明式 的数据查询语言。

在声明式查询语言(如 SQL 或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不关注如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。

声明式查询语言是迷人的,因为它通常比命令式 API 更加简洁和容易。但更重要的是,它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在无需对查询做任何更改的情况下进行性能提升。

Oracle,MySQL,MicroSoft SQL server, PostgreSQL 是不同组织对关系型数据库的具体实现产品,他们都部分实现了 SQL 标准。

SQL 开发规范

在广泛的数据分析师和工程师团队中工作时,您是否曾为不同的 SQL 风格而苦苦挣扎。上/下关键字、前导/尾随逗号、缩进、别名规则等。

随着模型数量的增长,拥有一致的风格变得有益,以简化代码审查、新成员的入职等。SQLfluff 来拯救,它是 SQL 文件的 CLI linter。它可以突出显示并经常修复样式违规。

dbt 是另一个令人惊叹的工具,可以简化数据建模、跟踪模型之间的依赖关系并减少样板代码的数量。

gitlab-sql-style-guide

  • 优点: 1)引入混合大小写来将关键字与列名和表名分开,例如,将 SELECT 写成大写,将 Employee 写成大写,但如果不一致,例如 SELECT 是大写字母,而 from 是小写字母,使用这种风格没有任何好处。

  • 缺点:

  • 混合大小写

  • 整个查询写在一行上,一旦表和列的数量增加,就会变得不可读
  • 添加新条件或在没有现有条件的情况下运行没有灵活性

Read more: https://javarevisited.blogspot.com/2017/01/a-better-way-to-write-sql-queries.html#ixzz8R4AscrEA

改进: 1)查询分为多行,使其更具可读性

  • 问题

1)混合大小写

2)where 子句上的所有条件都在同一行,这意味着通过注释排除它们并不那么容易。

Read more

  1. 将 SQL 查询分成多行使它们更具可读性

  2. 使用适当的缩进可以轻松发现数据源,即表和联接

  3. 将条件放在单独的行上允许您通过注释其中一个条件来运行查询例如

select e.emp_id, e.emp_name, d.dept_name
 from Employee e
 inner join Department d on e.dept_id = d.dept_id
 where d.dept_name =  'finance'
-- and e.emp_name like '%A%';

顺便说一句,如果您喜欢关键字大写,您也可以编写相同的 SQL 查询,如下所示,规则相同,但只是关键字大写。

Read more

团队对齐

首先,您需要与团队交谈并解释为什么拥有某些风格规则很重要。

这一步非常重要,因为如果没有清楚地了解为什么需要 PR,没有人会乐于看到 PR 停留在审查中。

这次谈话的一个好的结果是有一套你同意遵守的初始规则。此外,如果它很大和/或由多个 dbt 项目组成,您可能只想为项目的某个区域推出它。

集成到开发工作流程中

下一步是将 SQLfluff 集成到开发工作流中。实现它的最简单方法是使用预提交挂钩。

如果您已经在使用它们,只需添加几个新的钩子会容易得多 sqlfliff fix,sqlfluff lint.

否则,您可能需要更多时间来采用预提交挂钩。值得一提的是,尽管强烈推荐此步骤,但为了使开发工作流程更加顺畅,此步骤可能是可选的。

集成到 CI/CD 中

下一步是通过将 SQLfluff 集成到 CI/CD 中来强制执行规则。

它可以以软或硬的方式完成。

在第一种情况下,它应该只发出警告,而不是阻止 PR 被合并。

在第二种情况下,PR 应该

SQL 语法和注释

-- MySQL有几种注释语法

-- 1、单行注释符"--",--注释符后需要加一个空格,注释才能生效
/* 2、用C语言风格的注释注释语句块 */
-- 3、单行注释符"#",# 注释符后直接加注释内容
SELECT 1+1 from dual;     -- 注释文字

SELECT 1+1 from dual;    # 注释文字,这个#号在MySQL中也是注释
/* 标准SQL注释采用了C语言中的注释语法, MySQL同样支持这种语法  */
-- 下面这段注释表示:当接收SQL的Server版本大于4.0.001时,后面的注释文字就可以被识别。
SELECT /*!40001 SQL_NO_CACHE */  count(*) FROM db_test.test_table
SELECT SQL_NO_CACHE  count(*) FROM db_test.test_table

字符串字面量

在标准SQL中,字符串使用的是单引号括起来的。如果字符串本身也包括单引号,则使用两个单引号(注意。不是双引号,字符串中的双引号不需要另外转义)。

但在其它的数据库中可能存在对 SQL 的扩展,比如在 MySQL 中允许使用单引号和双引号两种。

MySQL 可以使用单引号(')或者双引号(")表示值,但是 PG 只能用单引号(')表示值,PG 的双引号(")是表示系统标识符的,比如表名或者字段名。

MySQL 可以使用反单引号(`)表示系统标识符,比如表名、字段名,PG 也是不支持的。

数据库的创建与删除

-- 建库,在Unix中,库名是严格区分大小的,强烈建议库名由小写字母和下划线组成
CREATE DATABASE `database_test`  IF NOT EXISTS CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';

-- 设置某个库只读
-- MySQL 8.0.22中引入的READ ONLY选项控制是否允许修改数据库和其中的对象。允许的值为DEFAULT或0(非只读)和1(只读)。
-- 此选项对于数据库迁移非常有用,因为启用了只读功能的数据库可以迁移到另一个MySQL实例,而不必担心在操作过程中数据库可能会发生更改。

ALTER DATABASE  `database_test`   READ ONLY = 0 ;


-- 删库
DROP DATABASE database_test;


-- 修改数据库
-- 注意:在 MySQL 中,数据库创建之后库名是无法直接修改的。能改的只有字符集等属性。

表的创建

-- 文件大小写,在不同的操作系统中的文件系统,是不一样的。
-- Linux操作系统,严格区分大小写,Test 和 TEST 是两个不同的文件或文件夹。
-- Windows操作系统,不区分大小写,无法同时创建 Test 和 TEST 两个同名的文件或文件夹。

-- 对于MySQL数据库和表,要在操作系统创建与库名和表名相同的文件夹或文件。

-- lower_case_file_system 是一个只读参数,无法被修改,这个参数是用来告诉你在当前的系统平台下,是否对文件名大小写敏感。

-- lower_case_table_names是一个MySQL可配置的参数,它的取值如下:

-- 0 大小写敏感。(Unix,Linux默认) 创建的库表将原样保存在磁盘上。
-- 如 create database TeSt ; 将会创建一个TeSt的目录,create table AbCCC …将会原样生成AbCCC.frm。 SQL语句也会原样解析。

-- 1 大小写不敏感。(Windows默认) 创建库表时,MySQL将所有的库表文件转换成小写存储在磁盘上。SQL语句同样会将库表名转换成小写。
-- 如需要查询以前创建的Testtable(生成Testtable.frm文件),即便执行select * from Testtable,也会被转换成select * from testtable,致使报错表不存在。

-- 列名,索引名,存储过程、函数及事件名称在任何操作系统下都不区分大小写,列别名也不区分大小写。


-- 最佳实践:建议将lower_case_table_names值统一设置为0,在代码中对大小写进行严格区分
-- 库名,表名,字段名,统一由小写英文字符和数字组成。

-- 在各家公司,都存在各种设计规范:
-- https://support.huaweicloud.com/bestpractice-rds/rds_02_0013.html


-- lower_case_table_names 参数是全局系统变量,不可以动态修改,想要变动时,必须写入配置文件然后重启数据库生效。
-- 如果你的数据库该参数一开始为 0 ,现在想要改为 1 ,这种情况要格外注意,因为若原实例中存在大写的库表,则改为 1 重启后,这些库表将会不能访问。
-- 如果需要将 lower_case_table_names 参数从 0 改成 1 ,可以按照下面步骤修改:

-- 1、首先核实下实例中是否存在大写的库及表,若不存在大写的库表,则可以直接修改配置文件然后重启。若存在大写的库表,则需要先将大写的库表转化为小写,然后才可以修改配置文件重启。

-- 2、通过 mysqldump 备份相关库,备份完成后删除对应库,之后修改配置文件重启,最后将备份文件重新导入。此方法用时较长,一般很少用到。

-- 3、 通过 rename 语句修改

-- 将大写表重命名为小写表
rename table TEST to test;

-- 若存在大写库 则需要先创建小写库 然后将大写库里面的表转移到小写库
rename table TESTDB.test_tb to testdb.test_tb;

-- 分享两条可能用到的SQL
-- 查询实例中有大写字母的表
SELECT
 TABLE_SCHEMA,
 TABLE_NAME
FROM
 information_schema.`TABLES`
WHERE
 TABLE_SCHEMA NOT IN ( 'information_schema', 'sys', 'mysql', 'performance_schema' )
 AND table_type = 'BASE TABLE'
 AND TABLE_NAME REGEXP BINARY '[A-Z]'


-- 拼接SQL 将大写库中的表转移到小写库
SELECT
 CONCAT( 'rename table TESTDB.', TABLE_NAME, ' to testdb.', TABLE_NAME, ';' )
FROM
 information_schema.TABLES
WHERE
 TABLE_SCHEMA = 'TESTDB';


-- 建表
-- 表名,字段名,字段类型,字段的约束,字段的字符集,存储引擎等
CREATE TABLE `cwd_group` (
  `id` bigint NOT NULL,
  `group_name` varchar(255) COLLATE utf8_bin NOT NULL,
  `lower_group_name` varchar(255) COLLATE utf8_bin NOT NULL,
  `active` char(1) COLLATE utf8_bin NOT NULL,
  `local` char(1) COLLATE utf8_bin NOT NULL,
  `created_date` datetime NOT NULL,
  `updated_date` datetime NOT NULL,
  `description` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `group_type` varchar(32) COLLATE utf8_bin NOT NULL,
  `directory_id` bigint NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `cwd_group_name_dir_id` (`lower_group_name`,`directory_id`),
  KEY `idx_group_active` (`active`,`directory_id`),
  KEY `idx_group_dir_id` (`directory_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE Product (
    product_id        char(4)         NOT NULL,
    product_name      varchar(100)    NOT NULL,
    product_type      varchar(32)     NOT NULL,
    sale_price        integer                 ,
    purchase_price    integer                 ,
    regist_date       date                    ,
    PRIMARY KEY (`product_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


-- 在demo库下建表ChineseCharInfo,有两列,其中ID为自增主键
CREATE TABLE demo.ChineseCharInfo
(
    ID        int              NOT NULL     auto_increment,
    Hanzi     varchar(10)      NOT NULL,
    primary   key (ID)
)
ENGINE=InnoDB auto_increment=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;


-- 在demo库下建表ChinesePinyinInfo
CREATE TABLE demo.ChinesePinyinInfo
(
    ID         INT                  NOT NULL     AUTO_INCREMENT,
    CharID     INT                  NULL,
    Pinyin     VARCHAR(10)          NULL,
    Tone       TINYINT UNSIGNED     NULL,

    primary key (ID),
    -- 方式一:不指定外键名称,数据库自动生成
    FOREIGN KEY (CharID) REFERENCES ChineseCharInfo(ID) ON DELETE CASCADE ON UPDATE CASCADE

    -- 方式二:指定外键名称为(FK_Name)
    -- constraint FK_Name foreign key (CharID) references ChineseCharInfo(ID) on delete cascade on update cascade
)
ENGINE=innodb AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

-- 创建一个与原表结构一样的表
CREATE TABLE demo.ChineseCharInfobak LIKE demo.ChineseCharInfo

生成列

在 SQL 中,生成列(Generated Column)是指由表中其他字段计算得到的列,因此也称为计算列(Computed Column)

在 SQL 建表语句支持指定的生成列,列的取值由其定义计算出来的。有如下特点:

  1. 虚拟列字段只读,不支持 INSRET 和 UPDATE。
  2. virtualstored之间不能相互转换,必要时需要删除重建。
  3. 只能引用本表的非generated column字段,不可以引用其它表的字段。
  4. 支持创建索引。
  5. 针对virtual类型的衍生列索引,在insertupdate操作时会消耗额外的写负载,因为更新衍生列索引时需要将衍生列值计算出来,并物化到索引里。但即使这样,virtual类型也比stored类型的衍生列好,有索引就避免了每次读取数据行时都需要进行一次衍生计算,同时stored类型衍生列实际存储数据,使得聚簇索引更大更占空间。

MySQL 5.7 引入了生成列,支持虚拟存储两种类型的生成列。

  • Virtual(虚拟):这个类型的列会在读取表记录时自动计算此列的结果并返回。默认创建的是VIRTUAL生成列。
  • Stored(存储):这个类型的列会在表中插入一条数据时自动计算对应的值,并插入到这个列中,那么这个列会作为一个常规列存在表中。

针对Virtual类型的衍生列索引,在insertupdate操作时会消耗额外的写负载,因为更新衍生列索引时需要将衍生列值计算出来,并物化到索引里。但即使这样,virtual类型也比stored了每次读取数据行时都类型的衍生列好,有索引就避免需要进行一次衍生计算,同时stored类型衍生列实际存储数据,使得聚簇索引更大更占空间。注意,出于性能的考虑,选择 Virtual 而不是 Stored

-- 通过sidea和sideb列中直角三角形的边的长度 ,并计算下斜边的长度 sidec(其他边的平方和的平方根)

-- 建表定义直角三角形(长边,短边,斜边(斜边是计算出来的))
CREATE TABLE triangle (
  sidea DOUBLE,
  sideb DOUBLE,
  sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb))
);

-- 测试数据
INSERT INTO triangle (sidea, sideb) VALUES(1,1),(3,4),(6,8);


mysql> SELECT * FROM triangle;
+-------+-------+--------------------+
| sidea | sideb | sidec              |
+-------+-------+--------------------+
|     1 |     1 | 1.4142135623730951 |
|     3 |     4 |                  5 |
|     6 |     8 |                 10 |
+-------+-------+--------------------+


-- 表生成列语法
col_name data_type [GENERATED ALWAYS] AS (expr)
  [VIRTUAL | STORED] [NOT NULL | NULL]
  [UNIQUE [KEY]] [[PRIMARY] KEY]
  [COMMENT 'string']

-- 其中,GENERATED ALWAYS可以省略;
-- AS定义了生成列的表达式;
-- VIRTUAL表示创建虚拟生成列,虚拟列的值不会存储,而是在读取时BEFORE触发器之后立即计算;
-- STORED表示存储生成列;默认创建的是VIRTUAL生成列。



CREATE TABLE `iam`.`biz_cloudsign_login`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `business_system_code` int(11) NOT NULL,
  `user_department` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `employee_num` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `identity_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_ip` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `random_num` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `cert_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `encrypted_token` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `updated_at` datetime(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,

) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;



SELECT
    DATE_FORMAT(v.updated_at,   '%Y-%m-%d'),
    count(DATE_FORMAT(v.updated_at,   '%Y-%m-%d'))
FROM
    biz_cloudsign_login v
    INNER JOIN ( SELECT MAX( id ) 'id' FROM biz_cloudsign_login GROUP BY employee_num,DATE_FORMAT(updated_at, '%Y-%m-%d') ) c ON v.id = c.id
    GROUP BY DATE_FORMAT(v.updated_at,   '%Y-%m-%d')


-- 在查询字段和分组字段加上索引
ALTER TABLE `iam`.`biz_cloudsign_login`
ADD INDEX `idx_updated_at`(`updated_at`),
ADD INDEX `idx_employee_num_updated_at`(`employee_num`, `updated_at`);


-- 有时候需要对字段上加函数然后进行GROUP BY。查看执行计划,发现出现 Using temporary, 分组条件并没有走索引。
-- 因为mysql 5.7 的函数会导致索引失效。我们可以通过添加一个冗余字段来保存函数的计算结果,然后添加索引,这时候的GROUP BY就会走索引了。

-- 加个新字段,虚拟列。
ALTER TABLE `iam`.`biz_cloudsign_login`
ADD COLUMN `update_at_date` date GENERATED ALWAYS AS (date_format(`updated_at`,'%Y-%m-%d')) Virtual NULL AFTER `updated_at`;

-- 对虚拟字段加上索引
ALTER TABLE `iam`.`biz_cloudsign_login`
ADD INDEX `idx_updated_at`(`update_at_date`),
ADD INDEX `idx_employee_num_updated_at`(`employee_num`, `update_at_date`);

数据插入

SQL 标准主要提供了两种插入数据的语法:

-- 第一种语法用于将指定的数据插入目标表
INSERT INTO  VALUES 
-- 第二种语法可以将一个查询结果插入目标表
INSERT INTO  SELECT 

在 MySQL 中可以使用 INSERT 语句向数据库已有的表中插入一行或者多行元组数据。

--单条插入
INSERT INTO items(name,city,price,number,picture)  VALUES ('耐克运动鞋','广州',500,1000,'003.jpg');



--批量插入
INSERT INTO  items(name,city,price,number,picture)
 VALUES
('耐克运动鞋','广州',500,1000,'003.jpg'),
('耐克运动鞋2','广州2',500,1000,'002.jpg');

-- 1、要根据情况设置一次批量插入的数据量,数据量大,在网络中传输的事件也越久,出现问题的可能也越大;

-- 2、除了网络,还要看机器配置,MySQL Server配置差了,sql写得再好,效率也不会太高;

-- 3、另外批量这个词,是指一次插入多条数据,我们除了要注意数据的条数,还要注意一条数据的大小,举个例子:比如一条记录的数据量有1M,10条记录的数据量就10M,这时一次插100条,100M数据,嘿嘿,你试试看!!所以,一次插入多少数据,一定要经过多次测试后再决定,别人1次插100条最优,你可能1次插10条才最优,没有绝对的最优值(批量插入未必总是比单条插入效率高)。

-- 4、数据库有个参数,max_allowed_packet,也就是每一个包(sql)命令大小,默认是1M,那么sql的长度大于1M就会报错。你可能会说,咱们把这个参数设成10M,100M不就行了???对呀,没毛病,但你是DBA吗?你有权限吗?即使调大这一个参数,你要知道影响的可不止你这一张表,而是整个DB Server,那影响的可是很多库,很多表。

-- 5、批量插入并不是越快越好,我们可能希望越快越好,这很正常,节省时间嘛。但是我们一定要知道,数据库分读写,有集群,这就意味着,需要同步!!!如果有分库分表分区的情况,如果短时间内插入的数据量太大,数据库同步可能就会比较迷了,读写数据不一致的情况在所难免了,可能会因为一张表的批量插入,影响整个DB服务组的同步,同时还要考虑并发问题。

--如果使用 Oracle 数据库,我们需要在执行 INSERT 语句之后使用 COMMIT 语句提交修改 
-- REPLACE INSERT语句

-- REPLACE INTO是INSERT INTO的加强版,既可以更新数据也可以插入数据;
-- 在向表中插入数据的时候,经常遇到这样的情况:1. 首先判断数据是否存在; 2. 如果不存在,则插入;3.如果存在,则更新。

-- REPLACE INTO用于实时覆盖写入数据。写入数据时,会先根据主键判断待写入的数据是否已经存在于表中,并根据判断结果选择不同的方式写入数据:

-- 使用REPLACE INTO时,首先会根据主键或者唯一索引查看表中是否有该条记录,有的话则先删除,然后插入数据;没有该记录则直接插入。

CREATE TABLE `stu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `cid` int(11) NOT NULL,
  `name` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
-- 其中id为自增主键,cid为唯一索引。

-- 插入两条数据
INSERT INTO stu(cid, name) VALUES(1, '1111');
INSERT INTO stu(cid, name) VALUES(2, '2222');
REPLACE INTO stu(cid, name) VALUES(3,'3333');


-- 1    1   1111
-- 2    2   2222
-- 3    3   3333


REPLACE INTO stu(cid, name) VALUES(3,'33331');
-- 使用REPLACE INTO时,首先会根据主键或者唯一索引查看表中是否有该条记录,有的话则先删除,然后插入数据;没有该记录则直接插入。
-- 原先id为3的记录变成了id为4了,说明原先id为3的记录先删除了,然后新插入了id为4(id自增)的新记录

-- 1    1   1111
-- 2    2   2222
-- 4    3   33331


--再次执行replace,id为4和cid为2的原记录都存在,则被删除。然后再插入这条新纪录
REPLACE INTO stu(id, cid, name) VALUES(4, 2,'42')
> Affected rows: 3
> 查询时间: 0.007s
-- 1    1   1111
-- 4    2   42
-- INSERT INTO…SELECT…FROM 语句用于快速地从一个或多个表中取出数据,并将这些数据作为行数据插入另一个表中。
-- SELECT 子句返回的是一个查询到的结果集,INSERT 语句将这个结果集插入指定表中,结果集中的每行数据的字段数、字段的数据类型都必须与被操作的表完全一致。

数据库在插入、更新或者删除数据之前会执行数据的完整性检查。如果违反约束,将会返回错误信息,而不会修改数据。

如果我们在定义表的字段时指定了默认值,也可以使用 DEFAULT 插入默认值

数据更新

SQL 标准使用 UPDATE 语句更新表中的数据。

-- UPDATE 语句的基本语法如下
UPDATE t
SET col1 = expr1,
    col2 = expr2,
    ...
    [WHERE condition];

-- 其中 t 是更新操作的目标表,SET 子句指定了需要更新的列和更新后的值,多个字段使用逗号进行分隔。
-- 如果指定了 WHERE 子句,只有满足条件的数据行才会被更新。如果没有指定条件,将会更新表中的所有数据行

-- 更新赵云的月薪和奖金
UPDATE emp_devp
SET salary = salary + 1000,
    bonus = 8000
WHERE emp_name = '赵云';

数据删除

SQL 标准使用 DELETE 语句删除表中的数据。

DELETE FROM t
    [WHERE conditions];

-- 其中 t 是删除操作的目标表。
-- 如果指定了 WHERE 子句,只有满足条件的数据行才会被删除。如果没有指定条件,将会删除表中所有的数据行。


-- leetcode. 196. Delete Duplicate Emails 删除表中重复的数据

-- Person表中有ID(主键)和EMAIL两列,删除email重复的行,仅保留id最小的那一行
DELETE p from Person p, Person q where p.Id>q.Id AND q.Email=p.Email

DELETE FROM Person
WHERE Id NOT IN (
    SELECT Id FROM (
        SELECT MIN(Id) AS Id
        FROM Person
        GROUP BY Email
    ) AS min_id  -- 此处需使用别名,否则会发生报错
);

如果我们想要删除表中的全部数据,数据量比较少时可以直接使用 DELETE 语句,但是这种方式对于数据量很大的表所需的时间比较长。

此时,我们可以考虑使用快速删除全表数据的 TRUNCATE 语句。也称为截断表

-- Oracle、MySQL、Microsoft SQL Server以及PostgreSQL
TRUNCATE TABLE emp_devp;

-- SQLite 没有提供 TRUNCATE 语句,不过它对 DELETE 语句进行了优化,如果我们不指定 WHERE 子句,实际的效果等同于 TRUNCATE 语句。

DELETE 和 TRUNCATE 语句都可以用于删除数据,但是这两种删除方式存在一些区别:

  • DELETE 语句通过 WHERE 子句删除指定的数据行,如果不指定过滤条件将会删除所有的数据。DELETE 属于数据操作语言(DML),删除数据后可以选择提交或者回滚,如果删除的数据较多时速度比较慢。

  • TRUNCATE 语句用于快速删除表中的所有数据,并且释放表的存储空间。TRUNCATE 属于数据定义语言(DDL),删除数据时默认提交,无法回滚。TRUNCATE 语句相当于删除并重建表,通常执行速度很快。

合并数据

SQL 标准于 2003 年增加了一个新的数据操作语句:MERGE(合并),它可以同时完成 INSERT 语句和 UPDATE 语句,甚至 DELETE 语句的操作。

MERGE 其实就是不存在则 INSERT,存在则 UPDATE,所以可以把它拆分成:

UPDATE ... WHERE EXIST (SELECT 1 FROM ... WHERE 条件)
INSERT ... WHERE NOT EXIST(SELECT 1 FROM ... WHERE 条件)

MERGE 语句尝试根据键字段将源表与目标表进行比较,然后进行一些处理。 MERGE 语句实际上将 INSERT,UPDATE 和 DELETE 操作完全结合在一起。 尽管 MERGE 语句比简单的 INSERT 或 UPDATE 稍微复杂一点,但是一旦您掌握了基本概念,就可以比使用单个 INSERT 或 UPDATE 更容易地更频繁地使用此 SQL MERGE。

在典型 SQL 数据仓库解决方案中,通常非常重要的一点是要维护仓库中的数据历史记录,并参考要馈送到 ETL 工具的源数据。

最常见的用例是尝试维护数据仓库中的尺寸变化缓慢(SCD)。

在这种情况下,您需要将新记录插入数据仓库,从仓库中删除或标记不再位于源中的记录,并更新仓库中已在源中更新的记录的值。

缓慢变化维的提出是因为在现实世界中,维度的属性并不是静态的,它会随着时间的流逝发生缓慢的变化,与数据增长较为快速的事实表相比,维度变化相对缓慢。阴齿这个就叫做缓慢变化维。

这意味着现在您可以将所有 DML 语句(INSERT,UPDATE 和 DELETE)组合成一个语句。从数据处理的角度来看,这非常有帮助,因为它减少了针对三个语句中的每个语句从磁盘进行的 I/O 操作,现在仅从源读取一次数据。

同样,MERGE 语句的性能在很大程度上取决于用于匹配源表和目标表的适当索引。

除索引外,还必须优化连接条件。 我们还应该尝试过滤源表,以便该语句仅获取必要的记录以执行必要的操作。

-- Oracle、Microsoft SQL Server 以及 PostgreSQL
MEGRE INTO target_table [AS t_alias]
USING source_table [AS s_alias]
ON (conditions)
WHEN MATCHED THEN
  UPDATE
  SET col1 = expr1,
      col2 = expr2,
      ...
WHEN NOT MATCHED THEN
  INSERT (col1, col2, ...)
  VALUES (expr1, expr2, ...);

-- 其中 target_table 是合并操作的目标表。
-- USING 子句指定了数据源,可以是一个表或者查询语句。
-- ON 子句指定了数据合并的条件,通常使用主键或者唯一键相等作为合并的条件。

-- 对于数据源中的每条记录:
-- 如果目标表中存在匹配的记录则执行 WHEN MATCHED THEN 分支的更新操作,.
-- 如果目标表中不存在匹配的记录则执行 WHEN NOT MATCHED THEN 分支的插入操作。

MySQL 和 SQLite 没有提供标准 MERGE 语句,不过我们可以使用专有的语法实现合并操作。

-- 在MySQL数据库中,如果在INSERT语句后面带上ON DUPLICATE KEY UPDATE 子句,而要插入的行与表中现有记录的惟一索引或主键中产生重复值,那么就会发生旧行的更新;如果插入的行数据与现有表中记录的唯一索引或者主键不重复,则执行新纪录插入操作。另外,ON DUPLICATE KEY UPDATE不能写WHERE条件。


-- MySQL
-- MySQL使用 ON DUPLICATE KEY UPDATE 子句合并数据,插入数据时如果主键或者唯一索引出现重复值则执行更新操作。这种专有语法不支持 DELETE 子句。
INSERT INTO emp_devp(emp_id, emp_name, sex, dept_id, manager,hire_date, job_id, salary, bonus, email)
SELECT emp_id, emp_name, sex, dept_id, manager,hire_date, job_id, salary, bonus, email
FROM employee s
WHERE dept_id = 4
ON DUPLICATE KEY UPDATE
   emp_name = s.emp_name, sex = s.sex,
   dept_id = s.dept_id, manager = s.manager,
   hire_date = s.hire_date, job_id = s.job_id,
   salary = s.salary, bonus = s.bonus,
   email = s.email;

-- PostgreSQL和SQLite
-- PostgreSQL 和 SQLite 使用 ON CONFLICT(emp_id) DO UPDATE 子句合并数据,插入数据时如果 emp_id 出现重复值则执行更新操作,EXCLUDED 代表了数据源中的记录。这种专有语法不支持 DELETE 子句。
INSERT INTO emp_devp(emp_id, emp_name, sex, dept_id, manager,hire_date, job_id, salary, bonus, email)
SELECT emp_id, emp_name, sex, dept_id, manager, hire_date, job_id, salary, bonus, email
FROM employee s
WHERE dept_id = 4
ON CONFLICT(emp_id) DO UPDATE
SET emp_name = EXCLUDED.emp_name, sex = EXCLUDED.sex,
    dept_id = EXCLUDED.dept_id, manager = EXCLUDED.manager,
    hire_date = EXCLUDED.hire_date, job_id = EXCLUDED.job_id,
    salary = EXCLUDED.salary, bonus = EXCLUDED.bonus,
    email = EXCLUDED.email;

返回数据

MySQL 对于 SQL 语句执行结果报文通常分为两类 Resultset 和 OK/ERR,针对 DML 语句则返回 OK/ERR 报文,其中包括几个影响记录,扫描记录等属性。类似 PostgreSQL/Oracle 都提供了returning clause支持 DML(INSERT/UPDATE/DELETE) 后跟 RETURNING 子句返回 Resultset。

在某些使用场景下,需要在DML操作后返回刚操作的数据行。实现这个需求一般有两种办法:

  • 一是在开启事务后在DML语句后紧跟一条 SELECT 语句。

  • 二是使用触发器等较为复杂的操作实现。

前者主要会增加一条 SELECT 语句的开销,后者则会令 SQL 的实现变得更加复杂并且不够灵活(需要创建触发器)。 因此,RETURNING 语法的设计主要针对该场景的优化,通过在 DML 语句后增加 RETURNING 关键字可以灵活高效地实现上述的需求。

--PostgreSQL
CREATE TABLE users (firstname text, lastname text, id serial primary key);

INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id;


UPDATE products SET price = price * 1.10
  WHERE price <= 99.99
  RETURNING name, price AS new_price;

DELETE FROM products
  WHERE obsoletion_date = 'today'
  RETURNING *;

使用 RETURNING 可以避免执行额外的数据库查询来收集数据,并且在否则难以可靠地识别修改的行时尤其有用。

CREATE TABLE users (
    firstname text,
    lastname text,
    id serial primary key
);

-- 在INSERT INTO或者UPDATE的时候在最后面加上RETURNING colname,PostgreSQL会在插入或者更新数据之后会返回你指定的字段。
-- 当使用 serial列来提供唯一标识符时, RETURNING可以返回分配给新行的ID:
INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id;

外键约束与级联操作

如果 DML 语句违反了外键约束,数据库会返回错误并取消数据操作。例如:

-- 违反外键约束,职位不存在
INSERT INTO employee (emp_name, sex, dept_id, manager, hire_date,
                      job_id, salary, bonus, email, comments,
                      create_by, create_ts, update_by, update_ts)
VALUES ('马超', '男', 5, 18, CURRENT_DATE, -- Microsoft SQL Server 需要替换该函数
        11, 6000, NULL, 'machao@shuguo.com', NULL,
        'Admin', CURRENT_TIMESTAMP, NULL, NULL);

以上语句在插入数据时违反了外键约束,因为 job_id=11 的记录在 job 表中不存在,我们不能给员工分配一个不存在的职位。

查询基础

从表中选取筛选数据,需要使用 SELECT 语句,也就是用表中选出 SELECT 必要数据的意思。通过 SELECT 语句查询并选取必要数据的过程称为查询(query)

-- 查询语句的基本结构

select A1,A2,A3 ... An
from r1,r2,r3 ... rn
where P

-- 注意:这个方法只是用来理解SQL语句的查询结果
-- from 多个关系后,可以理解为所有关系的笛卡尔积
-- 谓词P对关系进行过滤
-- 再从 select 中提取需要的列
-- 实际上,组成笛卡尔积的时候,会尽可能先执行谓词过滤一些数据后,再进行笛卡尔积

SQL 查询语句的执行过程

在编写了大量 SQL 脚本之后,您可能会在性能方面达到某种形式的平台期。您使用相同的策略提取见解并遇到相同类型的错误。

幸运的是,您可以通过花时间了解如何评估 SQL 中的子句来改善编写查询的体验。在这里,我们讨论 SQL 中的执行顺序并解释它的重要性。

执行顺序:SQL 查询在评估子句时遵循特定的顺序,类似于数学运算遵循 PEMDAS 或 BIDMAS 的方式。

从用户的角度来看,查询从第一个子句开始,到最后一个子句结束。但是,查询实际上并不是在执行时从上到下读取的。

查询中子句的执行顺序如下:

  1. FROM/JOIN:首先执行 FROM 和/或 JOIN 子句以确定感兴趣的数据。

  2. 查询执行从 FROM 子句开始。在此步骤中,数据库系统访问 FROM 子句中指定的表并在它们之间执行任何必要的连接。联接根据指定的联接条件合并来自不同表的相关行。此步骤检索将用于进一步处理的初始数据集。

  3. WHERE:执行 WHERE 子句,过滤掉不满足约束条件的记录。

  4. 访问和连接表后,将应用 WHERE 子句。WHERE 子句根据指定条件过滤连接表中的行。它允许您指定确定哪些行应包含在结果集中的条件。不满足条件的行将从进一步处理中剔除。

  5. GROUP BY:执行 GROUP BY 子句,根据一列或多列中的值对数据进行分组。

  6. HAVING:执行 HAVING 子句,删除创建的不满足约束的分组记录。

  7. SELECT:执行 SELECT 子句以派生所有所需的列和表达式。

  8. ORDER BY:执行 ORDER BY 子句以按升序或降序对派生值进行排序。

  9. LIMIT/OFFSET:最后,执行 LIMIT 和/或 OFFSET 子句以保留或跳过指定数量的行。

--SQL92标准
SELECT DISTINCT ...,...,...(存在聚合函数)
FROM ...,...,...(多表查询)
WHERE 多表的连接条件 AND 不包含聚合函数的过滤条件
GROUP BY ...,...
HAVING 包含聚合函数的过滤条件
ORDER BY ...,...(ASC/DESC)
LIMIT ...,...;


--SQL99标准
SELECT DISTINCT ...,...,...(存在聚合函数)             --->2
FROM ... (LEFT/RIGHT) JOIN ... ON 多表的连接条件   --->1
WHERE 不含聚合函数的过滤条件                         --->1
GROUP BY ...,...                                 --->1
HAVING 包含聚合函数的过滤条件                        --->1
ORDER BY ...,...(ASC/DESC)                              --->3
LIMIT ...,...;                                          --->3

EXIST 和 IN 查询

SELECT  *
FROM table_name
WHERE col_name IN (value1, value2,...);


-- in 操作符允许在 where 子句中规定多个值。
-- in 查询相当于多个 or 条件的叠加,比较好理解。
-- in 查询就是先将子查询条件的记录全都查出来。
-- in 查询的子条件返回结果必须只有一个字段。


-- 查找拥有员工的部门
SELECT *
FROM department d
WHERE EXISTS (SELECT 1 FROM employee WHERE dept_id = d.dept_id);


SELECT *
FROM department d
WHERE dept_id IN (SELECT dept_id FROM employee);

SELECT *
FROM department d
WHERE dept_id = ANY (SELECT dept_id FROM employee);

"所有的 x 都满足条件 P" 或者 "存在(至少一个)满足条件 P 的 x",前者称为:"全称量词",后者称为"存在量词",分别记作 ∀(A 的下倒)、∃(E 的左倒)。

SQL 中的 EXISTS 谓词实现了谓词逻辑中的存在量词,然而遗憾的是,SQL 却并没有实现全称量词。但是没有全称量词并不算是 SQL 的致命缺陷,因为全称量词和存在量词只要定义了一个,另一个就可以被推导出来。

EXISTS 运算符用于判断子查询结果的存在性。只要子查询返回了任何结果,就表示满足查询条件;如果子查询没有返回任何结果,就表示不满足查询条件。

在 SQL 中,为了表达全称量化,需要将 "所有的行都满足条件 P" 这样的命题转换成 "不存在不满足条件 P 的行"

-- 学生成绩表
DROP TABLE IF EXISTS tbl_student_score;
CREATE TABLE tbl_student_score (
  id INT(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  sno VARCHAR(12) NOT NULL COMMENT '学号',
    subject VARCHAR(5) NOT NULL COMMENT '课程',
    score TINYINT(3) NOT NULL COMMENT '分数',
  PRIMARY KEY (id)
);
INSERT INTO tbl_student_score(sno,subject,score) VALUES
('20190607001','数学',100),
('20190607001','语文',80),
('20190607001','物理',80),
('20190608003','数学',80),
('20190608003','语文',95),
('20190609006','数学',40),
('20190609006','语文',90),
('20190610011','数学',80);

SELECT * FROM tbl_student_score;


-- 查询出"所有科目分数都在 50 分以上的学生"
-- 20190607001、20190608003、20190610011 这三个学生满足条件,我们需要将这 3 个学生查出来

-- 将查询条件“所有科目分数都在 50 分以上” 转换成它的双重否定 “没有一个科目分数不满 50 分”,然后用 NOT EXISTS 来表示转换后的命题

-- 没有一个科目分数不满 50 分
SELECT DISTINCT sno
FROM tbl_student_score tss1
WHERE NOT EXISTS -- 不存在满足以下条件的行
(    SELECT * FROM tbl_student_score tss2
    WHERE tss2.sno = tss1.sno
    AND tss2.score < 50    -- 分数不满50 分的科目
);

在使用 IN 和 NOT IN 时是无法选取出 NULL 数据的。 实际结果也是如此,NULL 终究还是需要使用 IS NULL 和 IS NOT NULL 来进行判断。

WITH 查询(CTE)

由于业务的客观复杂性,有时候会写出长达 2000 行的单条 SQL 语句,其中包含大量的聚合和多层子查询嵌套,维护此类 SQL 堪称开发人员的噩梦。

CTE 通用表达式(Common table expression)简称 CTE,由 SQL:1999 标准引入。它是一种更加便捷的复用查询结果的方法。

它可以极大提升开发人员和 DBA 编写复杂业务逻辑 SQL 的效率,增强代码的可维护性。

可以认为是在单个 SELECT、INSERT、UPDATE、DELETE 或 CREATE VIEW 语句的执行范围内定义的临时结果集。

CTE 与派生表类似,具体表现在不存储为对象,并且只在查询期间有效。与派生表的不同之处在于,CTE 可自引用,还可在同一查询中引用多次。

在使用编程语言时,我们通常会定义一些变量和函数(方法)。变量可以被重复使用;函数(方法)可以将代码模块化,从而提高程序的可读性与可维护性。

与此类似,SQL 中的通用表表达式也能够实现查询结果的模块化和重复利用,简化复杂的连接查询和子查询。

--CTE语句如下
-- cte_name 指定了 CTE 的名称,后面是可选的字段名。
-- AS关键字后面的子查询是CTE 的定义语句,定义了它的表结构和数据。
-- 最后的 SELECT 是主查询语句,它可以引用前面定义的 CTE。除了 SELECT 之外,主查询语句也可以是 INSERT、UPDAT 或 DELETE 等。

WITH cte_name(col1, col2, ...) AS (
 subquery
)
SELECT * FROM cte_name;

WITH
  cte1 AS (SELECT a, b FROM table1),
  cte2 AS (SELECT c, d FROM table2)
SELECT b, d FROM cte1 JOIN cte2
WHERE cte1.a = cte2.c;



-- 列表中的名称数量必须与查询结果中的字段数量相同
WITH cte (col1, col2) AS
(
  SELECT 1, 2
  UNION ALL
  SELECT 3, 4
)
SELECT * FROM cte;


-- 子查询的CTE写法
-- CTE 将子查询的结果集用 with 语句提到最前面,取一个别名,再在后面的语句中使用
with derived(...) as (
    subquery
)
select ... from derived, table_name1 ……;


-- CTE 可以引用其他CTE
with derived_one as (
    subquery
),
derived_two as (
    select …… from derived_one
)
select …… from derived_one, derived_two ……;

-- 在同一个语句级别中只允许存在一个WITH子句,有效的语法格式是为一个WITH子句定义多个从句,使用逗号进行分隔:
-- 在包含WITH子句的查询中,可以使用CTE的名称访问相应 CTE 的结果集。
-- 前面定义的CTE可以在其他的CTE中进行引用,因此 CTE 可以基于前面的 CTE 进行定义。
-- 引用自己的 CTE 被称为递归 CTE。递归 CTE 的使用场景包括生成序列,遍历层次数据或树状结构的数据。
-- 通用表表达式属于 DML 语句的可选部分。




--找出预算值最大的系:系名,预算
-- 方法一
select dept_name , budget
 from department ,
 (SELECT max( budget )as budget FROM department) as max_budget

   where department.budget = max_budget.budget


-- 先用with语句查出一个临时表 max_budget ( VALUE ),
这里面存了所有系里面的最大的预算值。再将这个表与系表关联查询
WITH max_budget (value) AS ( SELECT max( budget ) FROM department ) SELECT
budget
FROM
    department,
    max_budget
WHERE
    department.budget = max_budget.value

CTE 相较于派生表有 4 个明显的优势:

  • 更好的可读性
    SELECT ...
    FROM t1 LEFT JOIN ((SELECT ... FROM ...) AS dt JOIN t2 ON ...) ON ...


    WITH dt AS (SELECT ... FROM ...)
    SELECT ...
    FROM t1 LEFT JOIN (dt JOIN t2 ON ...) ON ...

  • 可以被多次引用
    -- 传统派生表子查询写法:每个子查询都要写完成SQL

    SELECT ...
    FROM (SELECT a, b, SUM(c) s FROM t1 GROUP BY a, b) AS d1
    JOIN (SELECT a, b, SUM(c) s FROM t1 GROUP BY a, b) AS d2 ON d1.b = d2.a;

    -- CTE写法:一次生成,多次引用

    WITH d AS (SELECT a, b, SUM(c) s FROM t1 GROUP BY a, b)
    SELECT ... FROM d AS d1 JOIN d AS d2 ON d1.b = d2.a;

  • 可以引用其他的 CTE
    -- 传统这种不行,会报表不存在,要用更复杂的子查询
    SELECT ...
    FROM (SELECT ... FROM ...) AS d1, (SELECT ... FROM d1 ...) AS d2 ...


    -- 派生表写法
    WITH d1 AS (SELECT ... FROM ...),
    d2 AS (SELECT ... FROM d1 ...)
    SELECT
    FROM d1, d2 ...
  • 性能的提升

递归 CTE

递归 CTE 是一种特殊的 CTE,其子查询会引用自己的名字。

WITH子句必须以WITH RECURSIVE开头。

递归 CTE 子查询包括两部分:seed查询recursive查询,由UNION[ALL]UNION DISTINCT分隔。

-- 递归CTE ,  MySQL、PostgreSQL 以及 SQLite
WITH RECURSIVE derived(n) AS (
 SELECT 1
 UNION ALL
 SELECT n + 1 FROM derived WHERE n < 5
)
 SELECT * FROM derived;

-- 以上语句的执行结果是一个连续的数字序列:1,2,3,4,5

-- 如果在WITH子句中引用了自己,WITH子句必须使用WITH RECURSIVE。(如果没有 CTE 引用自己,也可以使用RECURSIVE,但不强制。)

-- 递归 CTE 的子查询由两部分组成,中间使用UNION [ALL]或者UNION DISTINCT进行连接:

-- 第一个SELECT语句用于生成初始数据行,该语句不会引用 CTE 自身。
-- 第二个SELECT语句在它的FROM子句中引用了 CTE自身,通过递归产生更多的结果。
-- 当第二个语句不会产生更多的新数据时结束递归。因此,递归 CTE由一个非递归的SELECT语句和一个递归的SELECT语句组成。
-- CTE最终结果中的字段类型由非递归的SELECT语句决定,所有字段都可以为空。查询结果的字段类型与递归SELECT语句无关。
-- 如果递归部分和非递归部分使用UNION DISTINCT进行连接,查询结果将会排除重复的数据行。
-- 这种方式可以用于执行传递闭包(transitive closure,例如两个地点之间的乘车路线)的查询,防止无限循环。

-- 递归部分的每次迭代只针对上次迭代生成的新数据行进行操作。
-- 如果递归部分包含多个查询块,迭代时每个查询块的执行顺序不固定,每个查询块基于它自己前一次迭代的结果,或者上次迭代结束后其他查询块生成的结果进行操作。


-- 前面递归 CTE 示例中的非递归语句如下,它会产生一条初始化的数据:
SELECT 1
-- 它的递归部分如下:
SELECT n + 1 FROM cte WHERE n < 5
-- 每次迭代时,SELECT语句将会产生一个比上一次结果中的 n 大 1 的新值。
-- 第一次迭代基于初始值(1)进行操作,生成 1+1=2;
-- 第二次迭代基于第一次迭代的结果(2),生成 2+1=3;如此等等。
-- 迭代一直执行到递归结束,此处为 n 的值大于或等于 5。
-- 如果递归部分产生的结果比非递归部分的字段长度更大,需要在非递归部分指定一个更宽的字段类型,避免数据被截断。



-- 假设用树形结构来描述一个部门的组织架构

-- DDL  用id来标识一个人,用manager_id来表示向上一个人汇报
CREATE TABLE db_test.employees_mgr (
    id INT PRIMARY KEY NOT NULL,
    name VARCHAR ( 100 ) NOT NULL,
    manager_id INT NULL,
    INDEX ( manager_id ),
    FOREIGN KEY ( manager_id ) REFERENCES employees_mgr ( id )
);


-- test data
insert into db_test.employees_mgr values
(333, "Yasmina", null), /* Yasmina is the CEO (manager_id is null) */
(198, "John", 333),     /* John has id 198 and reports to 333 (Yasmina) */
(692, "Tarek", 333),    /* Tarek has id 692 and reports to 333 (Yasmina) */
(29, "Pedro ", 198),    /* Pedro has id 29 and reports to 198 (John) */
(4610, "Sarah", 29);    /* Sarah has id 4610 and reports to 29 (Pedro) */


-- 要查询出如下这种树形结果

+------+---------+--------------------+
| id   | name    | path               |
+------+---------+--------------------+
|  333 | Yasmina | 333                |
|  198 | John    | 333->198           |
|  692 | Tarek   | 333->692           |
|   29 | Pedro   | 333->198->29       |
| 4610 | Sarah   | 333->198->29->4610 |
+------+---------+--------------------+
5 rows in set (0.00 sec)
mysql>


-- MySQL8.0中支持with子句
-- 通过递归CTE来查询用户的层级关系
-- 规划一个ID,name,path这样的一个层级临时表
 with recursive employee_paths (id, name, path) as (
     --先查最顶层节点
     select id, name, cast(id as char(200))
     from db_test.employees_mgr
     where manager_id is null
     union all
     -- 递归查询
     select e.id, e.name, concat(ep.path, '->', e.id)
     from employee_paths as ep
     join db_test.employees_mgr as e
     on ep.id = e.manager_id
    )
     select * from employee_paths;

查询排序

理解 SQL 最重要的一点就是要明白表不保证是有序的,因为表是为了代表一个集合(如果有重复项,则是多集),而集合是无序的。

在 SQL 世界中,顺序不是一组数据的固有属性。因此,除非您使用 order by 子句查询您的数据,否则您无法从 RDBMS 保证您的数据将按特定顺序返回——甚至以一致的顺序返回。

在关系模型中,所有操作和关系都基于关系代数和关系(集合)中的结果,但在SQL中情况略有不同,因 SELECT 查询并不保证返回一个真正的集合(即,由唯一行组成的无序集合)。

首先,SQL 不要求表必须符合集合条件。SQL 表可以没有键,行也不一定具有唯一性,在这些情况下表都不是集合,而是多集(multiset)或包(bag)。但即使正在查询的表具有主键、也符合集合的条件,针对这个表的 SELECT 查询任然可能返回包含重复的结果。

在描述 SELECT 查询的输出时,经常会使用结果集这个属于,不过结果集并不一定非得严格满足数学意义上的集合条件。

如果在查询表时不指定ORDER BY子句,那么虽然查询可以返回一个结果表,但 MySQL Server 可以自由地按任意顺序对结果中的行进行排序。(注意,这个不一定可靠)

为了确保结果中的行按照一定的顺序进行排序,唯一的方法就是显示地指定一个ORDER BY子句。

SQL支持多个字段进行ORDER BY排序,各字段之间用逗号","隔开。

 [ORDER BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]


-- ORDER BY后面可以接列名,别名,列号(数字)、表达式、函数、分组函数等
-- 如果不显示指出是升序ASC还是降序DESC,则默认为是升序
SELECT * FROM tablename ORDER BY column1,column2,column3 ;

-- 如果排序字段中存在相同的数据,那么它们的排序顺序是随机的。为了进一步明确这些数据的排序顺序,可以使用多列排序。
-- ORDER BY column1,column2         表示:column1和column2都是升序
-- ORDER BY column1,column2 DESC    表示:column1升序,column2降序

空值排序

空值(NULL)在数据库中表示未知或者缺失的数据。如果排序的字段中存在空值时,应该如何处理呢?

不同数据库系统对于空值的排序位置采用了不同的处理方式。

  • MySQL、Microsoft SQL Server 以及 SQLite 认为排序时空值最小,升序排序时空值排在最前,降序排序时空值排在最后。

By default, PostgreSQL considers NULL values larger than any non-NULL value

  • Oracle 和 PostgreSQL 认为排序时空值最大,升序排序时空值排在最后,降序排序时空值排在最前。

Oracle、PostgreSQL 以及 SQLite 支持使用 NULLS FIRSTNULLS LAST 指定空值的排序位置。

中文排序

在创建数据库或者表时,我们通常会指定一个字符集和排序规则。字符集(Charset)决定了数据库能够存储哪些字符,比如 ASCII 字符集只能存储简单的英文、数字和一些控制字符,GB2312 字符集可以存储中文,Unicode 字符集能够支持世界上的各种文字。

排序规则(Collation)定义了字符集中字符的排序顺序,包括是否区分大小写、是否区分重音等。对于中文而言,排序方式与英文有所不同,中文通常需要按照拼音、偏旁部首或者笔画进行排序。

如果想要支持中文排序,最简单的方式就是使用支持中文排序的字符集和排序规则。如果使用的字符集和排序规则不满足我们的排序需求,可以通过其他方法实现。

-- 对字符串类型中的中文字段排序

--

最近做项目遇到一个需求:

对数据按照更新时间和创建时间进行综合排序,即对数据的操作时间进行排序,但是数据表中没有操作时间这个字段,需要根据更新时间和创建时间进行处理。更新时间存在时,按照更新时间排序,更新时间不存在时,使用创建时间排序,最后更新时间和创建时间一起排序。

派生表和子查询

在数据库中,我们经常使用子查询和派生表来进行查询。

derived table中文译为派生表,关于派生表的含义,翻阅了 MySQL 的官方手册,并没有找到相对应的解释,不过在 SQL92 标准中有对它进行定义。

A derived table is a table derived directly or indirectly from one or more other tables by the evaluation of a .The values of a derived table are derived from the values of the underlying tables when the is evaluated.

不同数据库对于 FROM 子句中的子查询称呼不同。例如,MySQL 称之为派生表(Derived Table),Oracle 则称之为内联视图(Inline View)。

-- 子查询
-- 在另一个查询(外部查询)中嵌套另一个查询语句(内部查询),并使用内部查询的结果值作为外部查询条件。
-- 子查询在where中
-- where条件比对的值是从其他表查出来的。
-- 其实不光是 where ,select 后面跟的字段名也可以是从其他表中查出来的。

SELECT
       customerNumber, checkNumber, amount
FROM
      payments
WHERE
      amount = (SELECT  MAX(amount) FROMpayments);


-- 标量子查询 (一行一列)
-- 那些只返回一个单一值的子查询称之为标量子查询:子查询里面的查询结果只返回一行一列一个值的情况。
-- 子查询可以像常量一样被用于 SELECT、WHERE、GROUP BY、HAVING 以及 ORDER BY 等子句中。
SELECT (SELECT m1 FROM e1 LIMIT 1);
SELECT * FROM e1 WHERE m1 = (SELECT MIN(m2) FROM e2);
SELECT * FROM e1 WHERE m1 < (SELECT MIN(m2) FROM e2);

-- 计算员工月薪与平均月薪之间的差值
SELECT emp_name AS "员工姓名", salary AS "月薪",
       salary - (SELECT AVG(salary) FROM employee) AS "差值"
FROM employee;


-- 行子查询(一行多列)
-- 顾名思义,就是返回一条记录的子查询,不过这条记录需要包含多个列(只包含一个列就成了标量子查询了)。
-- 其中的(SELECT m2, n2 FROM e2 LIMIT 1)就是一个行子查询
-- 整条语句的含义就是要从 e1 表中找一些记录,这些记录的 m1 和 n1 列分别等于子查询结果中的 m2 和 n2 列
SELECT * FROM e1 WHERE (m1, n1) = (SELECT m2, n2 FROM e2 LIMIT 1);

-- 查找所有与"孙乾"在同一个部门并且职位相同的员工

-- Oracle、MySQL、PostgreSQL 以及 SQLite
SELECT emp_name, dept_id, job_id
FROM employee
WHERE (dept_id, job_id) = (SELECT dept_id, job_id
                           FROM employee
                           WHERE emp_name = '孙乾')
AND emp_name != '孙乾';

-- 其中,外部查询的 WHERE 子句中包含一个行子查询,返回了"孙乾"所在的部门编号和职位编号,这两个值构成了一个记录。
-- 然后,外部查询使用该记录作为条件进行数据过滤,AND 运算符用于排除"孙乾"自己。
-- Microsoft SQL Server 目前不支持行子查询。

-- 列子查询(一列数据)
-- 列子查询自然就是查询出一个列的数据,不过这个列的数据需要包含多条记录
-- 其中的(SELECT m2 FROM e2)就是一个列子查询,表明查询出 e2 表的 m2 列 的所有值作为外层查询 IN 语句的参数。
SELECT * FROM e1 WHERE m1 IN (SELECT m2 FROM e2);

-- 表子查询(二维多行多列)
-- 顾名思义,就是子查询的结果既包含很多条记录,又包含很多个列
-- 对于返回多行数据的子查询,我们可以使用 IN 或者 NOT IN 运算符进行比较。
-- 其中的(SELECT m2, n2 FROM e2)就是一个表子查询、此SQL必须要在m1,n1都满足的条件下方可成立
SELECT * FROM e1 WHERE (m1, n1) IN (SELECT m2, n2 FROM e2);
-- 因为单个值(外部查询条件中的字段)与多个值(子查询返回的多个元组记录)的比较不能使用比较运算符(=、!=、<、<=、>、>= 等)。

-- ALL 运算符与比较运算符(=、!=、<、<=、>、>=)的组合分别表示等于、不等于、小于、小于或等于、大于、大于或等于子查询结果中的全部数据。


-- From子句中的子查询
-- 派生表(子查询)
-- FROM后面跟的表是通过其他查询查出来的,这种查询叫派生表,派生表必须要有别名,以便稍后在查询中引用其名称。
-- 这种子查询后边的 AS t 表明这个子 查询的结果就相当于一个名称为 t 的表,这个名叫 t 的表的列就是子查询结果中的列(m和n)。
-- 这个放在 FROM 子句中的子查询本质上相当于一个表,但又和我们平常使用的表有点儿不一样,MySQL 把这种由子查询结果集组成的临时表称之为派生表。
SELECT m, n FROM (SELECT m2 + 1 AS m, n2 AS n FROM e2 WHERE m2 > 2) AS t;
-- 派生表也是从 select 语句中返回的虚拟表。
SELECT
    column_list
FROM
    (SELECT column_list ... FROM table_1) derived_table_name
WHERE derived_table_name.c1 > 0;


-- 一般来说,子查询可以像普通查询一样包含各种子句,例如 JOIN、WHERE、GROUP BY 等,甚至可以嵌套其他的子查询。但是需要注意,不同数据库对于子查询中 ORDER BY 子句的处理方式存在差异。

-- 通常来说,子查询中的排序没有实际意义,不会影响到查询结果和显示顺序。只有外部查询中的 ORDER BY 子句能够决定最终结果的显示顺序。

联表查询

笛卡尔积/交叉连接

笛卡尔积是指在数学中,两个集合 X 和 Y 的笛卡尓积(Cartesian product),又称直积,表示为 X × Y,第一个对象是 X 的成员,而第二个对象是 Y 的所有可能有序对的其中一个成员。

笛卡尔积又叫笛卡尔乘积,是一个叫笛卡尔的人提出来的。 简单的说就是两个集合相乘的结果。

假设集合 A={a, b},集合 B={0, 1, 2},则两个集合的笛卡尔积为{(a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2)}。

遍历左表的每一行数据,用左表每一行数据分别于与右表的每一行数据做关联

 select * from stu cross join class;
 select * from stu,class;
 select * from stu as a ,  class as b join on 1=1 ;
 -- 可以看出,笛卡尔乘积的运算量超级大,一般不会使用笛卡尔乘积做表的关联查询

注意

在 MySQL 中, CROSS JOIN 等价于 INNER JOIN , 这两个可以互换使用。但是在标准 SQL 中,这两个并不一样。

自然连接

对于两个表,自然连接是先找出两个表所有共用的属性,然后在 共用属性上做匹配,找出相同的行进行连接。(一定要注意连接谓词是所有的共有属性集合,即两个表上所有相同的列名)

-- 标准SQL写法一
select user_name,dept_name from user natural join dept ;     --这个限制比较大,要求字段名称一致。
-- 标准SQL写法二
select user_name,dept_name from  user, dept where user.id = dept.id  ...  --所有共有属性都连接起来。
-- MySQL写法三


-- select name , id , title from A1 natural join A2 natural join A3
-- select name , id , title from A1 natural join A2 , A3 where A2.id = A3.id
-- 这两个写法的结果可能不一样,
-- 第一个可以认为将A1 A2进行自然连接的结果,再与A3进行自然连接
-- 第二个可以认为将A1 A2进行自然连接的结果,再与A3进行等值连接

等值连接

对两个表,等值连接是明确一组属性上进行匹配。然后进行连接。(连接谓词是在选定的属性集合),所以自然连接是一种特殊的等值连接。这种方法可能更加通用。

-- 在这个例子中,只在ID上进行匹配。要求两个表都要有ID这个字段。
-- 使用 using 语法 , select ... from T1 join T2 using(id)
select user_name,dept_name from user join dept using (id) ;
-- 使用 join on 关键字,on条件允许在参与连接的关系上设置连接谓词。
select  user_name,dept_name from user join dept on user.id = dept.id ;

-- tb1 inner join tb2 on something
-- 不保留未匹配的元组
select  user_name,dept_name from students  left outer join dept on user.id = dept.id ;

内连接

内连接包括自然连接,不等值连接和等值连接。上面这些连接,都是常规连接,都可以理解成内连接,两个表中的列互相匹配时,只按照少的那个表匹配。

外连接

左外连接(LEFT OUTER JOIN):结果表中除了包括满足连接条件的行外,还包括左表的所有行。

【例】 查找所有学生情况,以及他们选修的课程号,若学生未选修任何课,也要包括其情况

left 左边的表为主表,left 右边的表为从表。返回结果行数以 left 左边的表的行数为最后的数据行,对于左表中有些数据行在右表中找不到它所匹配的数据行记录时候,返回结果的时候这些行后面通常会以 `null`` 来填充。

本例执行时,若有学生未选任何课程,则结果表中相应行的课程号字段值为 NULL。

右外连接(RIGHT OUTER JOIN):结果表中除了包括满足连接条件的行外,还包括右表的所有行。

完全外连接(FULL OUTER JOIN):结果表中除了包括满足连接条件的行外,还包括两个表的所有行

参考

半连接

半连接 SEMI JOIN 是指在两表关联时,当第二个表中存在一个或多个匹配记录时,返回第一个表的记录。

与普通 JOIN 不同,SEMI JOIN 中第一个表里的记录最多只返回一次。SEMI JOIN 通常无法直接用 SQL 语句来表示,而是由 IN 或 EXISTS 子查询转换得到。

半连接只会返回左表中的数据,右表只用于条件判断。另外,即使右表中存在多个匹配的数据,左边中的数据只返回一次。半连接通常用于存在性判断,例如哪些顾客购买了产品,而不需要知道他们购买的具体产品和数量。

-- 查找拥有员工的部门
SELECT *
FROM department d
WHERE EXISTS (SELECT 1 FROM employee WHERE dept_id = d.dept_id);

-- 也可以用IN查询
SELECT *
FROM department d
WHERE dept_id IN (SELECT dept_id FROM employee);

-- 也可以用 =ANY 操作符
SELECT *
FROM department d
WHERE dept_id = ANY (SELECT dept_id FROM employee);

反连接

反连接返回左表中与右表不匹配的数据行,通常体现为 NOT EXISTS 或者 NOT IN 子查询。反连接的逻辑与半连接正好相反。

反连接只会返回左表中的数据,右表只用于条件判断。反查询常见的应用包括:查找没有员工的部门信息,或者没有购买任何产品的顾客信息等。

-- 查找没有员工的部门
SELECT *
FROM department d
WHERE NOT EXISTS (SELECT 1 FROM employee WHERE dept_id = d.dept_id);

不等值连接

不等值连接:在连接条件使用除等于运算符以外的其它比较运算符比较被连接的列的列值。这些运算符包括>、>=、<=、<、!>、!<和<>。

分组查询

标准 SQL 规定,在对表进行聚合查询的时候,只能在 SELECT 子句中写下面 3 种内容:通过 GROUP BY 子句指定的聚合键、聚合函数(SUM 、AVG 等)、常量。

在标准 SQL,正常的如果对一个表进行分组查询,分组会进行去重,每一组查询的结果集应该只有一行数据。

分组,主要是使用 GROUP BY 子句,一般是按照 WHERE 条件过滤之后的结果集进行分组, GROUP BY 一般和聚合函数一起使用才有意义。

注意,WHERE子句只能指定行的条件,而不能指定组的条件。

很多人都知道聚合查询的限制,但是很少有人能正确地理解为什么会有这样的约束。

标准 SQL 规定:分组查询中的要查询的列必须是 group by 中要分组的列或聚集函数或常量。

表 tbl_student_class 中的 cname 存储的是每位学生的班级信息,但需要注意的是,这里的 cname 只是每个学生的属性,并不是小组的属性。

GROUP BY 又是聚合操作,操作的对象就是由多个学生组成的小组,因此,小组的属性只能是平均或者总和等统计性质的属性

询问每个学生的 cname 是可以的,但是询问由按照班级分组后的多个学生组成的小组的 cname 就没有意义了。

对于小组来说,只有"一共多少学生"或者"最大学号是多少?"这样的问法才是有意义的。强行将适用于个体的属性套用于团体之上,纯粹是一种分类错误。

GROUP BY 的作用是将一个个元素划分成若干个子集,使用 GROUP BY 聚合之后,SQL 的操作对象便由 0 阶的"行"变为了 1 阶的"行的集合"。

此时行的属性便不能使用了。SQL 的世界其实是层级分明的等级社会,将低阶概念的属性用在高阶概念上会导致秩序的混乱,这是不允许的。

按多字段分组,所有字段值相同的才能算一组。GROUP BY X, Y 意思是将所有具有相同 X 字段值和 Y 字段值的记录放到一个分组里。

参考

参考 2

-- 查询每个系的男教师平均工资(系名,平均工资)
-- where过滤出所有男教师。group by按系分组,聚合计算查出需要的列
select dept_name ,avg(salary) from instructor  where employe_type="male" group by dept_name


-- 对于下面这样的两个表:

-- 项目(项目名称,项目经理,价格)
-- 消费(消费流水号,会员手机号,项目名称,消费金额,消费日期)
-- 每个项目只能有一个项目经理,一个项目经理只能负责一个项目


-- 问题:统计出所负责项目消费总金额大于等于10万的项目和项目经理信息



SELECT
    项目.项目名称,
    项目.项目经理,
    sum( 消费金额 )
FROM
    项目,消费
WHERE
    项目.项目名称 = 消费.项目名称
GROUP BY
    项目.项目名称 项目.项目经理
HAVING
    sum( 消费金额 ) > 10000
ORDER BY
    sum( 消费金额 ) DESC

分组合计小计

SQL 中可以使用 GROUP BY 子句的扩展选项:ROLLUPROLLUP 可以生成按照层级进行汇总的结果,类似于财务报表中的小计、合计和总计。

CREATE TABLE `tbl_ware` (
  `ware_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `ware_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
  `ware_category` VARCHAR(100) NOT NULL COMMENT '商品类别',
  `sale_unit_price` INT COMMENT '销售单价',
  `purchase_unit_price` INT COMMENT '进货单价',
  `registration_date` DATE COMMENT '登记日期',
  PRIMARY KEY (`ware_id`) USING BTREE
) ENGINE=InnoDB COMMENT='产品';

INSERT INTO tbl_ware VALUES
(1,'T恤衫','衣服',100, 50,'2023-12-15'),
(2,'打孔器','办公用品',25, 10,'2023-12-15'),
(3,'运动T恤','衣服',150, 50,'2023-12-15'),
(4,'菜刀','厨房用具',75, 30,'2023-12-15'),
(5,'高压锅','厨房用具',600, 200,'2023-12-15'),
(6,'叉子','厨房用具',7, 3,'2023-12-15'),
(7,'菜板','厨房用具',98, 30,'2023-12-15'),
(8,'圆珠笔','办公用品',5, 2,'2023-12-15');


-- 统计每个类别产品的总进价
SELECT ware_category AS '商品类别' , SUM(purchase_unit_price) AS '总进价' FROM tbl_ware GROUP BY ware_category;

-- 如果还要同时统计所有总进价:两个SELECT结果集连接起来
SELECT ware_category AS '商品类别' , SUM(purchase_unit_price) AS '总进价' FROM tbl_ware GROUP BY ware_category;
UNION ALL
SELECT ware_category AS '合计' , SUM(purchase_unit_price) AS '总进价' FROM tbl_ware ;


-- 最后一行其实就是合计,WITH ROLLUP 是 MySQL 的独有写法
SELECT ware_category AS '商品类别' , SUM(purchase_unit_price) AS '总进价' FROM tbl_ware GROUP BY ware_category WITH ROLLUP;

MySQL group by 隐式排序

隐式排序和降序索引

分组原理和优化

MySQL 内部对 GROUP BY 的实现方式有三种,松散索引,紧凑索引,临时文件(文件排序)。

在没有合适的索引可用的时候,通常先扫描整个表提取数据并创建一个临时表,然后按照group by指定的列进行排序。在这个临时表里面,对于每一个 group 的数据行来说是连续在一起的。

完成排序之后,就可以发现所有的 groups,并可以执行聚集函数(aggregate function)。可以看到,在没有使用索引的时候,需要创建临时表和排序。在执行计划中通常可以看到"Using temporary; Using filesort"。

explain SELECT city ,count(*) AS 'NUM' FROM user GROUP BY city;

在 Extra 字段里面,我们可以看到以下信息:

  • 用到了 Using temporary, 表示执行时创建了一个内部临时表。

注意这里的临时表可能是内存上的临时表,也有可能是硬盘上的临时表,当然,如果临时表比较小,就是基于内存的,可以肯定的是:基于内存的临时表的性能高,时间消耗肯定要比基于硬盘的临时表的实际消耗小。

  • 用到了 Using filesort, 表示执行过程中没有使用索引的排序,而是使用临时文件。

"Using filesort"是 MySQL 的 EXPLAIN 输出中的一个短语,表示查询需要使用临时文件对结果集进行排序。这可能发生在查询包括ORDER BY子句或GROUP BY子句时,而数据库无法使用索引满足排序顺序。

使用临时文件对大型结果集进行排序可能会导致磁盘 I/O 和内存使用方面的昂贵开销,因此最好尽可能避免"Using filesort"

一些避免文件排序的策略包括使用适当的索引优化查询,限制结果集的大小或修改查询以使用不同的排序算法。

那么 group by 语句为啥会同时用到临时表和临时文件排序呢?

首先看下整个执行流程:

  • 在执行过程中首先创建内存临时表,表里有 city, num 两个字段,city 为主键。
  • 扫描 user 表,依次取出一行数据,数据中 city 字段的值为 c;
  • 如果临时表中没有主键为 c 的行, 则插入一条新纪录(c , 1);
  • 如果存在,则更新该行为 (c, num + 1);
  • 遍历完后,再根据 city 进行排序,最后将结果集返回给客户端。
分组操作如何利用索引?

因为innodb使用基于B+ tree的索引组织表,因此索引上的列满足天然有序性,对于组合索引,对组合键值有序;这个特性可以被用于索引扫描不同的group,而不需要扫描全部的索引列。

因而使用索引进行group by的最重要的前提条件是所有group by的参照列(分组依据的列)来自于同一个索引,且索引按照顺序存储所有的 keys(即 BTREE index,而 HASH index 没有顺序的概念)

  • 在松散索引扫描方式下,分组操作和范围预测(如果有的话)一起执行完成的。

  • 在紧凑索引扫描方式下,先对索引执行范围扫描(range scan),再对结果元组进行分组。

MySQL完全利用索引扫描来实现GROUP BY的时候,并不需要扫描所有满足条件的索引键即可完成分组操的方式,称为loose index scan,它可以最大限度的减少需要扫描的ROWS

松散索引扫描和紧凑索引扫描的最大区别是是否需要扫描整个索引或者整个范围扫描

松散索引(Loose Index Scan)

松散索引扫描相当于Oracle中的跳跃索引扫描(skip index scan),就是不需要连续的扫描索引中得每一个元组,扫描时仅考虑索引中的一部分。

  • 当查询中没有where条件的时候,松散索引扫描读取的索引元组的个数和groups的数量相同。

  • 如果where条件包含范围预测,松散索引扫描查找每个group中第一个满足范围条件,然后再读取最少可能数的 keys。松散索引扫描只需要读取很少量的数据就可以完成group by操作,因而执行效率非常高。

使用松散索引扫描需要满足以下条件:

  1. 查询在单一表上。
  2. group by指定的所有列是索引的一个最左前缀,并且没有其它的列。比如表 t1( c1,c2,c3,c4)上建立了索引(c1,c2,c3)。如果查询包含group by c1,c2,那么可以使用松散索引扫描。但是group by c2,c3(不是索引最左前缀)和group by c1,c2,c4(c4 字段不在索引中)。
  3. 如果在选择列表select list中存在聚集函数,只能使用 min()max()两个聚集函数,并且指定的是同一列(如果min()max()同时存在)。这一列必须在索引中,且紧跟着group by指定的列。比如select t1,t2,min(t3),max(t3) from t1 group by c1,c2
  4. 如果查询中存在除了group by指定的列之外的索引其他部分,那么必须以常量的形式出现(除了min()max()两个聚集函数)。比如:
-- 不能使用松散索引扫描
select c1,c3 from t1 group by c1,c2;
-- 可以使用松散索引扫描
select c1,c3 from t1 where c3 =  3 group by c1,c2;
  1. 索引中的列必须索引整个数据列的值(full column values must be indexed),而不是一个前缀索引。比如,c1 varchar(20), INDEX (c1(10)) 这个索引没发用作松散索引扫描。(建前缀索引,与上面提到的索引的最左前缀是不同的)

如果查询能够使用松散索引扫描,那么执行计划中 Etra 中提示"using index for group-by"

自从 MySQL5.5 开始,松散索引扫描可以作用于在select list中其它形式的聚集函数,除了min()max()之外,还支持:

  1. AVG(DISTINCT), SUM(DISTINCT)COUNT(DISTINCT)可以使用松散索引扫描。AVG(DISTINCT), SUM(DISTINCT)只能使用单一列作为参数。而COUNT(DISTINCT)可以使用多列参数。
  2. 在查询中必须没有group bydistinct条件。
  3. 之前声明的松散扫描限制条件同样起作用。

在官方文档中提到,当有GROUP BY子句时,在特定的情况下可以使用松散索引,减少扫描的次数。官方文档中给出了以下的一些情况可以使用到松散索引

an index idx(c1,c2,c3) on table t1(c1,c2,c3,c4)

CREATE TABLE `t1` (
    `c1` int(11) DEFAULT NULL,
    `c2` int(11) DEFAULT NULL,
    `c3` int(11) DEFAULT NULL,
    `c4` int(11) DEFAULT NULL,
    KEY `idx_g` (`c1`,`c2`,`c3`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 下面的查询可以使用松散索引扫描
SELECT c1, c2 FROM t1 GROUP BY c1, c2;
SELECT DISTINCT c1, c2 FROM t1;
SELECT c1, MIN(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT MAX(c3), MIN(c3), c1, c2 FROM t1 WHERE c2 > const GROUP BY c1, c2;
SELECT c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;


-- 下面的查询不能使用松散索引扫描
SELECT c1, SUM(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 GROUP BY c2, c3;
SELECT c1, c3 FROM t1 GROUP BY c1, c2;
紧凑索引扫描(Tight Index Scan)

紧凑索引扫描可能是**全索引扫描**或者**范围索引扫描**,具体也要取决于查询条件。

当松散索引扫描条件没有满足的时候,group by仍然有可能避免创建临时表。如果在 where 条件有范围扫描,那么紧凑索引扫描仅读取满足这些条件的 keys(索引元组),否则执行全索引扫描。

这种方式读取所有 where 条件定义的范围内的 keys,或者扫描整个索引,因而称作**紧凑索引扫描**。对于紧凑索引扫描,只有在所有满足范围条件的 keys 被找到之后才会执行分组操作。

如果紧凑索引扫描起作用,那么必须满足:

在查询中存在常量相等 where 条件字段(索引中的字段),且该字段在group by指定的字段的前面或者中间。来自于相等条件的常量能够填充搜索 keys 中的 gaps,因而可以构成一个索引的完整前缀。

索引前缀能够用于索引查找。如果要求对 group by 的结果进行排序,并且查找字段组成一个索引前缀,那么 MySQL 同样可以避免额外的排序操作。

-- (1)SQL执行计划 "Using where; Using index" 使用紧凑索引扫描,索引列范围查询
explain SELECT DISTINCT `c1` FROM t1 WHERE `c2`>'B';

-- (2)SQL执行计划 "Using where; Using index for group-by" 使用松散索引扫描,索引列等值查询
explain SELECT DISTINCT `c1` FROM t1 WHERE `c2`='B';

-- (3)SQL执行计划 "Using index for group-by" 使用松散索引扫描
explain SELECT MIN(c2) from t1 group by c1;

-- (4)SQL执行计划 "Using index" 使用min/max之外的其它聚集函数,则不能使用松散索引扫描,使用紧凑索引扫描
explain SELECT SUM(c2) from t1 group by c1;

-- (5)SQL执行计划 "Using index for group-by" 使用松散索引扫描,满足索引前缀
explain SELECT `c1`,`c2` FROM t1 GROUP BY `c1`,`c2`;

-- (6)SQL执行计划 "Using where; Using index for group-by" 使用松散索引扫描,满足索引前缀、索引列等值查询
explain SELECT `c1`,`c2`,`c3` FROM t1  WHERE c3='C' GROUP BY `c1`,`c2`;

-- (7)SQL执行计划 "Using where; Using index for group-by" 使用松散索引扫描,虽然不满足索引前缀,但前缀中的列为常量
explain SELECT `c1`,`c2`,`c3` FROM t1  WHERE c1='C' GROUP BY `c1`,`c2`,`c3`;

-- (8)SQL执行计划 "Using index; Using temporary; Using filesort",使用临时表,不满足前缀索引,分组无法走索引,需要临时表并对分组内元素排序
explain SELECT `c2`,`c3` FROM t1 GROUP BY `c2`,`c3`;

-- (9)SQL执行计划 "Using index; Using temporary; Using filesort",使用临时表,不满足前缀索引,分组无法走索引,需要临时表并对分组内元素排序
explain SELECT `c1`,`c3` FROM t1 GROUP BY `c1`,`c3`;

-- (10)SQL执行计划 "Using where; Using index",使用紧凑索引扫描,却别于(9)使用临时表,尽管不满足前缀索引,但前缀中的列为常量
explain SELECT `c1`,`c3` FROM t1 WHERE c2='B' GROUP BY `c1`,`c3`;
尽量使用内存表

如果group by需要统计的数据不多,我们可以尽量只使用内存临时表;因为如果group by的过程因为内存临时表放不下数据,从而用到磁盘临时表的话,是比较耗时的。因此可以适当调大tmp_table_size参数,来避免用到磁盘临时表。

使用 SQL_BIG_RESULT 优化

如果数据量实在太大怎么办呢?总不能无限调大tmp_table_size吧?但也不能眼睁睁看着数据先放到内存临时表,随着数据插入发现到达上限,再转成磁盘临时表吧?这样就有点不智能啦。

因此,如果预估数据量比较大,我们使用SQL_BIG_RESULT这个提示直接用磁盘临时表。MySQl 优化器发现,磁盘临时表是 B+树存储,存储效率不如数组来得高。因此会直接用数组来存。

SELECT SQL_BIG_RESULT city,count(*) AS num FROM staff GROUP BY city;

对分组限定条件

对分组的结果限定查询查询条件,使用 having 子句,为要分组的标准限定条件。即对分组之后再对数据进行过滤

HAVING 操作的对象是组,那么其使用的要素是有一定限制的,能够使用的要素有 3 种: 常数 、 聚合函数 和 聚合键 ,聚合键也就是 GROUP BY 子句中指定的列名。

where是在分组(聚合)前对记录进行筛选,而having是在分组结束后的结果里筛选,最后返回过滤后的结果。

HAVING子句必须写在GROUP BY子句之后,其在RDBMS内部的执行顺序也排在GROUP BY子句之后。

--仅查询平均工资大于4000的系,列出这样系的系名和平均工资

select dept_name ,avg(salary) from instructor group by dept_name having avg(salary) > 4000;

select user_id  from user group by user_id

-- 另外一种思路,把所有系的平均工资查出来,再从这个结果里面查平均工资大于4000的
select   dept_name , avg(salary) from ( select dept_name ,avg(salary)  from instructor group by dept_name ) where avg(salary) > 4000;

参考

去重查询

按照SQL标准,DISTINCT是一种用于去除SELECT语句返回结果中重复行的关键字。在使用SELECT语句查询数据时,如果结果集中包含重复的行,可以使用SELECT DISTINCT语句来去除这些重复的行

-- distinct支持单列、多列的去重方式

SELECT name FROM users;                 --返回一个包含重复行的结果集
SELECT DISTINCT name FROM users;        --去除重复的行
-- 需要注意的是,DISTINCT 关键字会对查询的性能产生一定的影响,因为它需要对结果集进行排序和去重的操作。因此,在使用 DISTINCT 关键字时需要谨慎,尽可能地使用索引来优化查询,以提高查询的性能。

SELECT DISTINCT columns FROM table_name WHERE where_conditions;   --DISTINCT用法
-- 按照SQL标准,DISTINCT子句将所有NULL值视为相同的值,如果列具有NULL值,并且对该列使用DISTINCT子句,MySQL将保留一个NULL值,并删除其它的NULL值。



-- 多列的去重则是根据指定的去重的列信息来进行,即只有所有指定的列信息都相同,才会被认为是重复的信息。
SELECT DISTINCT column1,column2 FROM table_name WHERE where_conditions;


-- 这条语句中的 DISTINCT 表示返回不同部门 id 和性别的组合值。
SELECT DISTINCT dept_id, sex FROM employee;

-- GROUP BY也可以达到同样的效果
SELECT dept_id, sexFROM employee GROUP BY dept_id, sex;
-- 所有列都相同,才被认为是重复的。
SELECT DISTINCT * FROM TABLE

-- 两条记录之间之后只有部分字段的值是有重复的,但是主键唯一
SELECT * FROM TABLE WHERE ID IN (SELECT MAX(ID) FROM TABLE GROUP BY [去除重复的字段名列表,....])

GROUP BY主要的使用场景是在分组聚合。具体来说,GROUP BY子句通常用于将查询结果按照一个或多个列进行分组,然后对每个组进行聚合计算。

例如,假设一个表存储了每个人的姓名、年龄和所在城市,可以使用GROUP BY子句按照城市对人进行分组,并计算每个城市的平均年龄或人口数量等统计信息。

GROUP BY子句通常与聚合函数(例如 COUNT、SUM、AVG、MAX 和 MIN)一起使用,以计算每个组的聚合值。例如,可以使用 GROUP BY 子句和 COUNT 函数来计算每个城市中的人数。

但是,除了分组聚合,GROUP BY还可以用来进行数据去重

需要注意的是:GROUP BY 子句会对结果集进行排序,因此可能会导致使用临时文件排序。如果查询中包含 ORDER BY 子句,使用不当会产生临时文件排序,容易产生慢 SQL 问题。

MySQL 查询去重的小结

  • 在语义相同,有索引的情况下,group by 和 distinct 效率相同; group by 和 distinct 都能使用索引,而索引天然有序,可以避免排序,因此二者效率相同;此情况下,group by 和 distinct 近乎等价,distinct 可以被看做是特殊的 group by;

  • 在语义相同,无索引的情况下,distinct 效率高于 group by; 原因是 distinct 和 group by 都会进行分组操作,但 group by 在 Mysql8.0 之前会进行隐式排序,导致触发 filesort,执行效率更低; 从 Mysql8.0 开始,Mysql 就删除了隐式排序;所以 Mysql8.0 后,此时在语义相同,无索引的情况下,group by 和 distinct 的执行效率也是近乎等价的;

  • 更推荐使用 group by; group by 语义更为清晰,可对数据进行更为复杂的一些处理;group by 的使用灵活性更高,group by 能根据分组情况,对数据进行更为复杂的处理,例如通过 having 对数据进行过滤,或通过聚合函数对数据进行运算;

分组去重的关系和区别

集合查询

-- SQL语句中存在3个著名的量词,SOME ALL 和 ANY
-- ALL:只有当其所有数据都满足条件时,条件才成立
-- ANY:只要有一条数据满足条件,条件就成立
-- SOME:其中存在一些数据满足条件,作用和Any大致相同 常规的使用中看作一致即可

-- AS 更名的用处:给查询结果的列名改一个名字,给要查询的表改一个名字。尤其是在同一个表中进行比较。

-- 教师表  instructor(ID, name, dept_name, salary)
-- 找出满足下面条件的所有教师的姓名:他们的工资比 Biology 系教师的最低工资要高

-- 方法一
-- 将一个表和它自己进行笛卡尔积运算,然后再过滤
select distinct T.name from instructor as T, instructor as S where T.salary > S.salary and S.dep_tname='Biology';

-- >some 比较:  至少比集合中某一个值要大
-- 大于some就是:比最小的还要大
-- 小于some就是,比最大的还要小
--方法二
--等价上面的写法,使用 > some 格式判断,子查询查出 Biology 系所有的工资组成集合。再用 >some 比较
select name from instructor where salary > some (select salary from instructor where dep_tname='Biology' );



-- 找出满足下面条件的所有教师的姓名:他们的工资比 Biology 系每个教师的最高工资要高

-- 方法一:子查询中找出Biology系的最高工资
select name from instructor where salary >  (select MAX(salary) from instructor where dep_tname='Biology'  );

-- 方法二:大于所有
-- >all 比较,比集合中的所有值都大。
select name from instructor where salary > all (select salary from instructor where dep_tname='Biology' );

-- 找出平均工资最高的系

-- 按系名分组,计算每个系的平均工资,并将结果按照平均工资从高到低排序。最后,只返回排序结果中的第一行,即平均成绩最高的那个系。

SELECT
  dept_name,
  AVG(salary) AS avg_salary
FROM
  instructor
GROUP BY
  dept_name
ORDER BY
  avg_salary DESC
LIMIT
  1;


-- 方法:先查出所有系的平均工资做为一个集合,再在所有系的平均的工资里面比较,找出最大值。
select dept_name from instructor group by dept_name having avg(salary) >=all (select avg(salary) from instructor group by deptname);

集合运算

SQL 面向集合特性最明显的体现就是 UNION(并集运算)、INTERSECT(交集运算)和 EXCEPT/MINUS(差集运算)。

这些集合运算符的作用都是将两个集合并成一个集合,因此需要满足以下条件:

  • 两边的集合中字段的数量和顺序必须相同;
  • 两边的集合中对应字段的类型必须匹配或兼容。

具体来说,UNIONUNION ALL 用于计算两个集合的并集,返回出现在第一个查询结果或者第二个查询结果中的数据。它们的区别在于 UNION 排除了结果中的重复数据,UNION ALL 保留了重复数据。

SQL 中的关系概念来自数学中的集合理论,因此 UNIONINTERSECTEXCEPT 分别来自集合论中的并集($\cup$)交集($\cap$)差集($\setminus$)运算。需要注意的是,集合理论中的集合不允许存在重复的数据,但是 SQL 允许。

因此,SQL 中的集合也被称为多重集合(multiset);多重集合与集合理论中的集合都是无序的,但是 SQL 可以通过 ORDER BY 子句对查询结果进行排序。

-- 并集(union)

-- 并集 union 自动去重(效率很低),将两个结果集求并集,如果想合并没有刻意要删除重复行。可以保留重复,则使用union all

-- UNION 必须由两条或两条以上的 SELECT 语句组成,语句之间用关键字 UNION 分隔(因此,如果组合4条 SELECT 语句,将要使用3个UNION 关键字)。
-- UNION 中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
-- 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。

-- 找出在2009年秋季或在2010年春季开课的课程信息
(select course_id from section where semester='Fall' and year=2009) union (select course_id from section where semester='Spring' and year=2010);

-- 也等价于下面这条查询
select course_id from section where (semester='Fall' and year=2009) or (semester='Spring' and year=2010);



-- 交集(intersect all)

-- 交集 intersect all
-- 找出在2009年秋季和2010年春季同时开课的课程信息
(select course_id from section where semester='Fall' and year=2009) intersect all (select course_id from section where semester='Spring' and year=2010);


-- 差运算 (expect)
-- EXCEPT 子句/运算符用于将两个 SELECT 语句结合在一起,并返回第一个 SELECT 语句的结果中那些不存在于第二个 SELECT 语句结果的记录
-- 找出在2009年秋季开课,2010年春季没开课的课程信息
(select course_id from section where semester='Fall' and year=2009) expect (select course_id from section where semester='Spring' and year=2010);



-- 集合成员资格测试  in ,测试元组是否是集合中的成员。集合一般是由select查出来的。用于筛出在集合中的元素。

-- 找出在2009年秋季和2010年春季同时开课的课程信息
-- in关键字用于测试关素是否是集合中的成员。对于这个例子,3个查询条件:1、秋季;2、2009学期,3、课程id必须2010年春季开课的课程id一样。
-- 使用in关键字来描述这个查询。
select course_id from section where semester='Fall' and year=2009 and course_id in (select course_id from section where semester='Spring' and year=2010);


-- 集合成员资格测试 not in ,测试元组是否是集合中的成员。用于筛出不在集合中的元素。

行转列/列转行

行列转换在做报表分析时是经常遇到的

  • 行转列:将多行数据转换成一行显示,或将一列数据转换成多列显示。

  • 列转行:将一行数据转换成多行显示,或将多列数据转换成一列显示。

create table rowtocolumn (name string, subject string, result bigint);
insert into table rowtocolumn values
('张三' , '语文' , 74),
('张三' , '数学' , 83),
('张三' , '物理' , 93),
('李四' , '语文' , 74),
('李四' , '数学' , 84),
('李四' , '物理' , 94);

SQL 中的 null 和 DEFAULT

根据 ANSI SQL-92 规范,NULL既不是空字符串(对于字符或日期时间数据类型),也不是零值(对于数字数据类型)。

为了确保所有 null 被统一处理,ANSI SQL-92 规范规定所有数据类型的 null 必须是相同的。

在 postgresql 14 中或更早的版本,唯一性约束将 null 与 null 视为是不相同的。这与 sql 标准是相同的,简而言之,null 表示 unknown。因而,null 值也就不违反唯一性约束。

对于给定记录,当属性没有要存储的数据时,它由 SQL 值 NULL 表示。NULL 不是一个「值」,而是「没有值」。

  • Oracle 将 NULL 和空字符串都当作 NULL 来处理。Oracle 需要一个 byte 来存储 NULL。

  • PostgreSQL 将 NULL 和空字符串分开处理,NULL 是 NULL,空字符串是空字符串。PostgreSQL 不需要空间来存储 NULL。

  • SQL Server 也是将 NULL 和空字符串分开处理,NULL 是 NULL,空字符串是空字符串。SQL Server 不需要空间来存储 NULL。

NULL 和空字符串上的唯一性约束

  • Oracle 中,具有唯一性约束的列,可以包含任意数量的 NULL 和空字符串;

  • SQLServer 中,只是允许有一个 NULL、一个空字符串。

  • 对于 NULL,PostgreSQL 的处理方式类似 Oracle,而对于空字符串,PostgreSQL 的处理方式类似 SQLServer。

  • MySQL ,唯一索引创建一个约束,使得索引中的所有值都必须是不同的。如果尝试添加一个键值与现有行匹配的新行,则会发生错误。如果在唯一索引中为列指定前缀值,则列值在前缀长度内必须是唯一的。唯一索引允许包含空值的列有多个空值。

NULL

在 SQL 中建表,每个字段后面都加上 NULL 或 NOT NULL 修饰符来指定该字段是否可以为空(NULL),还是说必须填上数据(NOT NULL)。

MySQL 默认情况下指定字段为 NULL 修饰符,如果一个字段指定为 NOT NULL,MySQL 则不允许向该字段插入空值(这里面说的空值都为 NULL),因为这是"规定"。

但是在自增列和 TIMESTAMP 字段中,这个规则并不适用。向这些字段中插入 NULL 值将会导致插入下一个自动增加的值或者当前的时间戳。

在 MySQL 中 null 不能使用任何运算符与其他字段或者变量(函数、存储过程)进行运算。若使用运算数据就可能会有问题。

在写 SQL 条件语句时经常用到 不等于 != 的筛选条件。

此时要注意此条件会将字段为 Null 的数据也当做满足不等于的条件而将数据筛选掉。(也就是说会忽略过滤掉为 null 的数据,导致数据不准确)。

元数据查询

-- 列出模式中的所有表
    SELECT table_name
    FROM information_schema.tables
    WHERE table_schema = 'dbo'

--列出表中的列
   SELECT column_name, data_type, ordinal_position
    FROM information_schema.columns
    WHERE table_schema = 'dbo'
    AND table_name   = 'EMP'
--列出表的索引列
    SELECT a.name table_name,
        b.name index_name,
        d.name column_name,
        c.index_column_id
    FROM sys.tables a,
        sys.indexes b,
        sys.index_columns c,
        sys.columns d
    WHERE a.object_id = b.object_id
         AND b.object_id = c.object_id
         AND b.index_id  = c.index_id
         AND c.object_id = d.object_id
         AND c.column_id = d.column_id
         AND a.name      = 'EMP'
--5.4 列出表的约束
     SELECT a.table_name,
         a.constraint_name,
         b.column_name,
         a.constraint_type
     FROM information_schema.table_constraints a,
          information_schema.key_column_usage b
     WHERE a.table_name      = 'EMP'
           AND a.table_schema    = 'dbo'
           AND a.table_name      = b.table_name
           AND a.table_schema    = b.table_schema
           AND a.constraint_name = b.constraint_name

--列出没有相应索引的外键
    SELECT
    fkeys.table_name,
    fkeys.constraint_name,
    fkeys.column_name,
    ind_cols.index_name
FROM
    (
    SELECT
        a.object_id,
        d.column_id,
        a.NAME table_name,
        b.NAME constraint_name,
        d.NAME column_name
    FROM
        sys.TABLES a
        JOIN sys.foreign_keys b ON ( a.NAME = 'EMP' AND a.object_id = b.parent_object_id )
        JOIN sys.foreign_key_columns c ON ( b.object_id = c.constraint_object_id )
        JOIN sys.COLUMNS d ON ( c.constraint_column_id = d.column_id AND a.object_id = d.object_id )
    ) fkeys
    LEFT JOIN (
    SELECT
        a.NAME index_name,
        b.object_id,
        b.column_id
    FROM
        sys.indexes a,
        sys.index_columns b
    WHERE
        a.index_id = b.index_id
    ) ind_cols ON ( fkeys.object_id = ind_cols.object_id AND fkeys.column_id = ind_cols.column_id )
WHERE
    ind_cols.index_name IS NULL

运算符

数据库,除了数据的存取之外,还支持各种运算,主要包括 算数运算逻辑运算

算数运算符 作用
+ 加法
- 减法
* 乘法
/ 或 DIV 除法
% 或 MOD 取余
运算符
= 等于
<>, !=
>
<
>=
<=
BETWEEN
NOT BETWEEN
IN 判断操作数是否为 IN 列表中的一个值
NOT IN
<=> 安全的等于
LIKE

逻辑连接词

汉语里的或、且、非

逻辑运算符
NOT 或 ! 表示逻辑非
AND 或 && 表示逻辑与运算
OR 或 ||
<
>=

运算符优先级

优先级由低到高排列 运算符 含义
1 =(赋值运算)、:=
2 II、OR
3 XOR
4 &&、AND
5 NOT
6 BETWEEN、CASE、WHEN、THEN、ELSE
7 =(比较运算)、<=>、>=、>、<=、<、<>、!=、 IS、LIKE、REGEXP、IN
8 |
9 &
10 <<、>>
11 -(减号)、+(加号)
12 *、/、% *、/、%
13 ^
14 -(负号)、〜(位反转)
15 !

运算符的优先级决定了不同的运算符在表达式中计算的先后顺序,一般情况下,级别高的运算符优先进行计算,如果级别相同,MySQL 按表达式的顺序从左到右依次计算,在无法确定优先级的情况下,可以使用圆括号"()"来改变优先级,并且这样会使计算过程更加清晰。

Select * from users where id=1 and 1=1; 这条语句为什么能够选择出 id=1 的内容,and 1=1 到底起作用了没有?

三个条件用 and 和 or 进行连接。在 sql 中,我们 and 的运算优先级大于 or 的元算优先级。

对于 MySQL 三大数据类型,数值类型,字符,时间

当两个不同类型的数据进行运算时,为了使得它们能够兼容,MySQL 可能会执行隐式的数据类型转换。例如,MySQL 在需要时会自动将字符串转换为数字,反之亦然。

  • 如果任意一个参数为 NULL,比较运算符的结果为 NULL,<=> 相等比较运算符除外。NULL <=> NULL 的运算结果为 true,不需要进行类型转换。
  • 如果两个参数都是字符串,执行字符串比较。

函数

SQL 主要的功能就是对数据进行处理和分析。为了提高数据处理的效率,SQL 为我们提供了许多预定义的功能模块,也就是函数(Function)。

SQL 函数是一种具有某种数据处理功能的模块,它可以接收零个或多个输入值,并且返回一个输出值。SQL 中的函数主要分为以下两种类型:

  • 标量函数(Scalar Function),针对每个输入参数返回一个输出结果。例如 ABS(x)函数可以计算 x 的绝对值。
  • 聚合函数(Aggregate Function),基于一组输入参数进行汇总并返回一个结果。例如 AVG(x)函数可以计算一组数据的平均值。

数学函数

ABS(x)                              -- 返回x的绝对值

BIN(x)                              -- 进制转转:返回x的二进制(OCT返回八进制,HEX返回十六进制),其中x为正整数
CONV(N,from_base,to_base)           -- 进制转换:数字按照进制互转:N为要被转换的数字,原始进制,目标进制

CEILING(x)                          -- 向上取整:返回大于x的最小整数值
FLOOR(x)                            -- 向下取整:返回小于x的最大整数值

EXP(x)                              -- 返回值e(自然对数的底)的x次方
LN(x)                               -- 返回x的自然对数
LOG(x,y)                            -- 返回x的以y为底的对数

GREATEST(x1,x2,...,xn)              -- 返回集合中最大的值
LEAST(x1,x2,...,xn)                 -- 返回集合中最小的值

MOD(x,y)                            -- 返回x/y的模(余数)
PI()                                -- 返回pi的值(圆周率)
RAND()                              -- 返回0到1内的随机值,可以通过提供一个参数(种子)使RAND()随机数生成器生成一个指定的值。
ROUND(x,y)                          -- 返回参数x的四舍五入的有y位小数的值
SIGN(x)                             -- 返回代表数字x的符号的值
SQRT(x)                             -- 返回一个数的平方根
TRUNCATE(x,y)                       -- 返回数字x截短为y位小数的结果

-- 数学函数,因为其操作得都是数字,建议仅作用于数字类型的字段或常量上

聚合函数

Aggregate Function翻译成聚合函数,从多行输入计算单个结果行。返回的结果是基于行组而不是单行(与 single row funtion 单行函数的区别)。

它可以出现在选择列表、order by 子语、having 子语中。

通常我们见到的 select 分组字段, sum() from 表名 group by 分组字段 [having ...]形式的分组汇总 SQL,其中的 SUM 函数就是聚合函数,常用的聚合函数还有 COUNT、AVG、MAX、MIN 等。

聚合函数有时也被称为汇总函数、分组函数。

聚集函数是以值的一个集合为输入、返回单个值的函数,标准 SQL 提供了五个固有的聚集函数:

  • 平均值 avg
  • 最小值 min
  • 最大值 max
  • 总和 sum
  • 计数 count
--查询Biology系的平均工资
select avg(salary) from instructor where dept_name='Biology'

字符串函数

字符函数用于字符数据的处理,例如字符串的拼接、大小写转换、子串的查找和替换等。

ASCII(char)                             -- 返回字符的ASCII码值      select ASCII("a") ——> 97

-- 连接字符串
CONCAT(s1,s2...,sn)                     -- 将s1,s2...,s等多个字符串拼接连接成一个字符串  select CONCAT("a","b","c"); ——> abc
                                        -- Oracle中的CONCAT函数一次只能连接两个字符 Oracle和PostgreSQL也提供了连接运算符(||),Microsoft SQL Server使用加号(+)作为连接运算符。
-- MySQL、Microsoft SQL Server以及PostgreSQL
CONCAT_WS(sep,s1,s2...,sn)              -- 将s1,s2...,sn连接成字符串,并用sep字符间隔   select CONCAT_WS("-","a","b","c") ——> a-b-c

-- 查找子串
SUBSTRING(s, n, m)                      -- 函数返回字符串s中从位置n开始的m个字符的子串。 SELECT SUBSTRING('数据库', 1, 2); ——> 数据
FIELD(str,str1,str2,str3,...)           -- 返回str在(str1,str2,str3,...)中出现的位置, SELECT FIELD('Bb', 'Aa', 'Bb', 'Cc', 'Dd', 'Ff');
POSITION(substr,str)                    -- 返回子串substr在字符串str中第一次出现的位置

-- 获取子串
-- MySQL、Microsoft SQL Server以及PostgreSQL提供了LEFT(s, n)和RIGHT(s, n)函数,分别用于返回字符串开头和结尾的n个字符。

-- 子串查找与替换
INSERT(str,x,y,instr)                   -- 将字符串str从第x位置开始,y个字符长的子串替换为字符串instr,返回结果

FIND_IN_SET(str,list)                   -- 分析逗号分隔的list列表,如果发现str,返回str在list中的位置  SELECT FIND_IN_SET('b','a,b,c,d');

-- 大小写转换
LCASE(str)LOWER(str)                  -- 返回将字符串str中所有字符改变为小写后的结果
LEFT(str,x)                             -- 返回字符串str中最左边的x个字符



-- 字符串长度
-- 字符串的长度可以按照两种方式进行计算:字符数量和字节数量。
-- CHAR_LENGTH(s)函数用于计算字符串中的字符数量
-- OCTET_LENGTH(s)函数用于计算字符串包含的字节数量。
-- 字符串“数据库”包含3个字符,在UTF-8编码中占用9个字节。MySQL和PostgreSQL实现了这两个标准函数。
SELECT CHAR_LENGTH('数据库'), OCTET_LENGTH('数据库');  --3 9
LENGTH(str)                             -- 返回字符串str的字节数(字节数)   select length(_utf8 '中'); ——> 3
CHAR_LENGTH(str)                        -- 返回字符串的字符数量(字符个数)   select  char_length(_utf8 '中') ——> 1
BIT_LENGTH(str)                         -- 返回字符串的比特长度

-- 截断字符串

-- LTRIM(s)函数可以删除字符串开头的空格,RTRIM(s)函数可以删除字符串尾部的空格,这两个函数是TRIM函数的简化版
LTRIM(str)                              -- 从字符串str中切掉开头的空格
RTRIM(str)                              -- 返回字符串str尾部的空格


QUOTE(str)                              -- 用反斜杠转义str中的单引号
REPEAT(str,srchstr,rplcstr)             -- 返回字符串str重复x次的结果
REVERSE(str)                            -- 返回颠倒字符串str的结果
REPLACE(str,old_string,new_string);     -- REPLACE()函数有三个参数,它将string中的old_string替换为new_string字符串。
RIGHT(str,x)                            -- 返回字符串str中最右边的x个字符

STRCMP(s1,s2)                           -- 比较字符串s1和s2
TRIM(str)                               -- 去除字符串首部和尾部的所有空格
UCASE(str)UPPER(str)                  -- 返回将字符串str中所有字符转变为大写后的结果


select length(_utf8 '€'), char_length(_utf8 '€')
--> 3, 1



mysql> SET @dolphin:='海豚';
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT LENGTH(@dolphin), CHAR_LENGTH(@dolphin);
+------------------+-----------------------+
| LENGTH(@dolphin) | CHAR_LENGTH(@dolphin) |
+------------------+-----------------------+
|                6 |                     2 |
+------------------+-----------------------+
1 row in set (0.00 sec)

日期时间函数

NOW()
-- NOW()函数返回当前的日期和时间,包括年月日时分秒。例如:2024-01-05 07:31:28。

CURRENT_TIMESTAMP()
-- CURRENT_TIMESTAMP()函数返回当前的日期和时间,包括年月日时分秒。例如:2024-01-05 07:31:28。
-- 与NOW()函数类似,但是CURRENT_TIMESTAMP()是标准SQL语法,而NOW()是MySQL特有的函数。
-- 也可以用变量CURRENT_TIMESTAMP表示当前日期和时间

-- 设置字段属性为时间戳类型,且把默认值设置成CURRENT_TIMESTAMP的情况下。如果你设置了时间随当前时间戳更新( ON UPDATE CURRENT_TIMESTAMP),那么当你更新字段时,只有字段值发生变化了,你的时间字段才会变为UPDATE操作时的时间

CREATE TABLE t1 (
  ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);


SYSDATE()
-- SYSDATE()函数返回当前的日期和时间,与NOW()函数类似,包括年月日时分秒。例如:2024-01-05 07:31:28。
-- 但是SYSDATE()函数在每次调用时会从操作系统获取当前时间,而NOW()函数在连接建立时获取一次,并在连接期间返回相同的值。

select now(), sysdate(),sleep(3), now(),sysdate();
2019-12-29 23:49:27 2019-12-29 23:49:27 0   2019-12-29 23:49:27 2019-12-29 23:49:30



CURDATE()
-- CURDATE()函数返回当前的日期,只包括年月日,不包括时间。例如:2024-01-05。


CURRENT_DATE()
-- CURRENT_DATE()函数返回当前的日期,只包括年月日,不包括时间。例如:2024-01-05。

其返回值的格式为 YYYY-MM-DD’,其中 YYYY 表示年份,‘MM 表示月份,‘DD 表示日期。


CURTIME()
-- CURTIME()函数返回当前的时间,只包括时分秒,不包括日期。例如:07:31:28。

CURRENT_TIME():
-- CURRENT_TIME()函数返回当前的时间,只包括时分秒,不包括日期。例如:07:31:28。


-- 当前日期:2022-05-27
select DATE(CURRENT_TIME)

-- sysdate() 日期时间函数跟 now() 类似。一般很少用到
-- 不同之处在于:now() 在执行开始时值就得到了, sysdate() 在函数执行时动态得到值。



-- 查询一天
select * from 表名 where to_days(时间字段名) = to_days(now());
select * from 表名 where date(时间字段名) = curdate();

--昨天
select * from 表名 where to_days( now( ) ) - to_days( 时间字段名) <= 1;

--7天
select * from 表名 where date_sub(curdate(), interval 7 day) <= date(时间字段名);

--本周内
select * from 表名 where 时间字段名 between current_date()-7 and sysdate();

--近30天
select * from 表名 where date_sub(curdate(), interval 30 day) <= date(时间字段名);
select * from tbl_name where to_days(now()) - to_days(date_col) <= 30;

--本月
select * from 表名 where date_format( 时间字段名, '%Y%m' ) = date_format(curdate( ) , '%Y%m' )

-- 查询一个月
select * from table where date_sub(curdate(), interval 1 month) <= date(column_time);

-- 上一月
select * from 表名 where period_diff( date_format( now( ) , '%Y%m' ) , date_format( 时间字段名, '%Y%m' ) ) =1

其他函数

-- 当前MySQL版本  5.7.26-log
select version()

-- 字符串长度: 3 ,3 ,1   length返回字节长度,char_length返回字符数量
select length("我") , length("你") , char_length("你");

-- 日期格式化  2019-12-27
select date_format(now(),'%y-%m-%d');



-- md5加密:8a6f60827608e7f1ae29d1abcecffc3a
select md5("andyqian");
-- 字符串拼接 andyqian
select concat("andy","qian");

-- if函数判断

select t.name,if(t.weight<80,'正常','肥胖') 体重 from t_customer t

参考

触发器

触发器是每当发生与表相关的事件时自动调用的函数。事件可以是以下任何一种:INSERT,UPDATE,DELETE 或 TRUNCATE。

触发器是绑定到表的特殊的用户自定义函数。触发器和用户自定义函数的不同之处在于触发事件(当表发生变化时)发生时会自动调用

触发器一旦由某用户定义,任何用户对触发器规定的数据进行更新操作,均自动激活相应的触发器采取应对措施。

触发器本质上是一条 SQL 语句,当对数据库中的表做更新操作时,它自动被系统执行。

触发器三要素:

  • 事件:引起触发器动作的事件,通常是更新操作。

  • 条件:触发器将测试条件是否满足。如果条件满足,就执行相应的操作,否则什么也不做。

  • 动作:如果触发器测试满足预订的条件,就由 DBMS 执行这些动作。这些动作可以是一系列对数据库的操作

-- 创建触发器的语法

CREATE [DEFINER = user] TRIGGER trigger_name tirgger_time trigger_event
ON tbl_name FOR EACH ROW
BEGIN
    trigger_stmt
END

-- trigger_name:触发器的名称
-- tirgger_time:触发时机,为BEFORE或者AFTER
-- rigger_event:触发事件,为INSERT、DELETE或者UPDATE
-- tb_name:表示建立触发器的表明,就是在哪张表上建立触发器
-- trigger_stmt:触发器的动作程序体,可以是一条SQL语句或者是用BEGIN和END包含的多条语句

存储过程

流程控制语句

语句
CASE
IF()
IFNULL()
NULLIF()

窗口函数

窗口函数(Window Function)是 SQL2003 标准中定义的一项新特性,并在 SQL2011SQL2016 中又加以完善,添加了若干处拓展。窗口函数也被称为联机分析处理(OLAP)函数,在 Oracle 中也称分析函数(Analytic Function)。

窗口函数不同于我们熟悉的普通函数和聚合函数,它为每行数据进行一次计算:输入多行(一个窗口)、返回一个值。在报表等分析型查询中,窗口函数能优雅地表达某些需求,发挥不可替代的作用。

大部分数据库,尤其是商业数据库都支持 SQL 标准中定义的部分窗口函数,但是 MySQL 一直没有支持这个特性。窗口函数在 MySQL 社区一直呼声很高,却一直没有被实现,直到 MairaDB 10.2 和 MySQL8.0 才响应了客户的需求,实现了部分窗口函数。  Oracle 11gSQL Server 2008DB2 9.7PostgreSQL 8.4 都支持窗口函数。

MySQL8 开始支持窗口函数,包括 rank()lag()ntile() 等非聚合窗口函数。以及部分聚合函数现在可以用作窗口函数,例如:sum()avg()

窗口函数(Window Function),又被叫做分析函数(Analytics Function),通常在需要对数据进行分组汇总计算时使用,因此与聚合函数有一定的相似性。但与聚合函数不同的是,聚合函数通过对数据进行分组,仅能够输出分组汇总结果,而原始数据则无法展现在结果中。而窗口函数则可以同时将原始数据和聚集分析结果同时显示出来。

通常情况下,SQL 操作数据是基于同行 (row) 或者同列(column)的。无论是 WHERE 条件还是还是函数聚合,默认都是同一行的不同列,或者是同一列的不同行进行。而现实的数据分析需求很大一部分都是要基于"前因后果"的,因此在数据整合的操作过程中,就无法避免要面对"位移"的逻辑处理,所谓位移就是参考点和观察点的偏移量。比如,我们在做留存分析时,通常会非常关注日注册用户的留存率,例如 1-7 日留存率(如下表),也就是用户注册后过 1 天、2 天....到第 7 天里,每一天还剩多少比例的活跃(即有过登录或浏览)用户量。

窗口函数出现在 SELECT 子句的表达式列表中,它最显著的特点就是 OVER 关键字。语法定义如下:

--DDL
CREATE TABLE `tbl_ware` (
  `ware_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `ware_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
  `ware_category` VARCHAR(100) NOT NULL COMMENT '商品类别',
    `sale_unit_price` INT COMMENT '销售单价',
    `purchase_unit_price` INT COMMENT '进货单价',
    `registration_date` DATE COMMENT '等级日期',
  PRIMARY KEY (`ware_id`) USING BTREE
) ENGINE=InnoDB COMMENT='产品';

-- DML
INSERT INTO tbl_ware VALUES
(1,'T恤衫','衣服',100, 50,'2023-12-15'),
(2,'打孔器','办公用品',25, 10,'2023-12-15'),
(3,'运动T恤','衣服',150, 50,'2023-12-15'),
(4,'菜刀','厨房用具',75, 30,'2023-12-15'),
(5,'高压锅','厨房用具',600, 200,'2023-12-15'),
(6,'叉子','厨房用具',7, 3,'2023-12-15'),
(7,'菜板','厨房用具',98, 30,'2023-12-15'),
(8,'圆珠笔','办公用品',5, 2,'2023-12-15');


-- 按售价从高到低进行排名:普通排序
SELECT * FROM tbl_ware ORDER BY sale_unit_price DESC;

-- 按售价从高到低进行排名:窗口函数
SELECT *, RANK() OVER(ORDER BY sale_unit_price DESC) AS ranking FROM tbl_ware;



-- 对 tbl_ware 按类别进行分组,然后组内按售价从高到低进行排名
-- PARTITION BY 对表的横向进行分组,类似 GROUP BY ,但不具备聚合功能: 通过 PARTITION BY 分组后的记录集合称为「窗口」,代表范围。这也是「窗口函数」名称的由来
-- ORDER BY 则决定了分组后的组内排序:纵向排序的规则,与 SELECT 子句末尾的 ORDER BY 子句完全相同
SELECT *, RANK() OVER(PARTITION BY ware_category ORDER BY sale_unit_price DESC) AS ranking FROM tbl_ware;





window_function (expression) [null_treatment]  OVER (
   [ PARTITION BY part_list ]
   [ ORDER BY order_list ]
   [ { ROWS | RANGE } BETWEEN frame_start AND frame_end ] )

-- window_function 是窗口函数的名称
-- expression 是窗口函数操作的对象,可以是字段或者表达式;
-- OVER 子句包含三个部分:分区(PARTITION BY)、排序(ORDER BY)以及窗口大小(frame_clause)


SELECT  SUM(amount) AS sum_amount
FROM sales_data WHERE saledate = '2019-01-01';

select saledate, product, sum(amount) over() as sum_amount
from sales_data
where saledate = '2019-01-01'

窗口函数 window_function 可以分为以下 3 类:

  • 聚合(Aggregate):AVG(), COUNT(), MIN(), MAX(), SUM()...

  • 取值(Value):FIRST_VALUE(), LAST_VALUE(), LEAD(), LAG()...

  • 排序(Ranking):RANK(), DENSE_RANK(), ROW_NUMBER(), NTILE()...

其中包括以下可选项:

  • PARTITION BY 表示将数据先按 part_list 进行分区,作用类似于 GROUP BY 分组;如果指定了分区选项,窗口函数将会分别针对每个分区单独进行分析(相同的分为一个区,然后对每个分区里面的值是使用 window_function 进行计算)。

  • ORDER BY 表示将各个分区内的数据按 order_list 进行排序

  • FRAME ,表示当前窗口包含哪些数据。滑动窗口有两种指定范围的方式,一种是基于行,一种是基于范围。

  • ROWS 选择前后几行,例如 ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示往前 3 行到往后 3 行,一共 7 行数据(或小于 7 行,如果碰到了边界)

  • RANGE 选择数据范围,例如 RANGE BETWEEN 3 PRECEDING AND 3 FOLLOWING 表示所有值在 [c-3,c+3] 这个范围内的行,c 为当前行的值

  • ROWS 和 RANGE 的范围边界也可以用 CURRENT ROW

Frame 定义并非所有窗口函数都适用,比如 ROW_NUMBER()、RANK()、LEAD() 等。这些函数总是应用于整个分区,而非当前 Frame。

SELECT dealer_id, emp_name, sales,
       ROW_NUMBER() OVER (PARTITION BY dealer_id ORDER BY sales) AS rank,
       AVG(sales) OVER (PARTITION BY dealer_id) AS avgsales
FROM sales

窗口

row_number() 则在排序相同时不重复,会根据顺序排序。

rank()排序相同时会重复,总数不会变 ,意思是会出现 1、1、3 这样的排序结果;

dense_rank() 排序相同时会重复,总数会减少,意思是会出现 1、1、2 这样的排序结果。

想看各个 department 内部收入最多的人,这时候不能全表 order by 了

SQL 标准支持 4 种用于排名计算的窗口函数。它们是:ROW_NUMBER、NTILE、RANK 和 DENSE_RANK。在标准中,前两者是一类,后两者是另一类。

ROW_NUMBER 函数根据指定的顺序,从 1 开始计算连续的行号。NTILE 函数把窗口分区里的数据行分成数量大致相等的块(根据输入的块数和指定的窗口排序)。

ROW_NUMBER

  • 功能:用于实现分区内记录编号

  • 语法:row_number() over (partition by col1 order by col2)

  • 示例:统计每个部门薪资最高的前两名

select
  empno,
  ename,
  salary,
  deptno,
  row_number() over (partition by deptno order by salary desc) as numb
from
  db_emp.tb_emp;

--此时为排名,要求取前两名,作为临时表t再套一层查询即可

select
*
from
    (select
          empno,
          ename,
          salary,
          deptno,
          row_number() over (partition by deptno order by salary desc) as numb
    from
      db_emp.tb_emp) t
where t.numb < 3;

RANK

  • 功能:用于实现分区内排名编号[会留空位]

  • 与 row_number 的区别:

  • row_number:如果排序时数值相同,继续编号

  • rank:如果排序时数值相同,编号相同,但留下空位

  • 语法:rank() over (partition by col1 order by col2)

  • 示例:统计每个部门薪资排名

select
  empno,
  ename,
  salary,
  deptno,
  rank() over (partition by deptno order by salary desc) as numb
from
  db_emp.tb_emp;
USE db_test;
DROP TABLE IF EXISTS  student ;

CREATE TABLE  student (
    name VARCHAR(20) NOT NULL,
    subject VARCHAR(20) NOT NULL,
    score INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into student(name,subject,score)
    values
("小明","语文",66),
("小明","数学",67),
("小明","英语",69),
("小红","语文",65),
("小红","数学",99),
("小红","英语",88),
("小强","语文",60),
("小强","数学",72),
("小强","英语",92);


-- 计算每个学生自己的各科排名(组内排序)
SELECT name, subject, score,
      row_number() OVER (partition BY name ORDER BY score DESC) as n
FROM student ;




--每个科目最高的成绩以及对应的同学和科目

SELECT name, subject, score FROM (
    SELECT name, subject, score,
      ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score  ) AS n
FROM student ) AS top WHERE top.n <2



--思路分析,首先按照科目分组并聚合求最大值。将其结果集与原表关联,查出同学名称即可。

    SELECT
        b.name,
        b.subject,
        b.score
    FROM
        (
            SELECT
                subject,
                MAX(score) AS score
                FROM
                    student
                GROUP BY
                    subject
        ) a
    JOIN student b ON a.subject = b.subject
    AND a.score = b.score

窗口函数和聚合函数的区别

窗口函数仅仅只会将结果附加到当前的结果上,它不会对已有的行或列做任何修改。

而 Group By 的做法完全不同:对于各个 Group 它仅仅会保留一行聚合结果。

因为 SQL 及关系代数都是以 multi-set 为基础定义的,结果集本身并没有顺序可言,ORDER BY 仅仅是最终呈现结果的顺序。

MySQL 官网

参考

参考

参考

参考

SQL 的思维方式

SQL 本身是一种 DSL(领域描述语言),它由数学家(早期计算机技术大都是由领域应用中的数学专家主导的)发明用来表达数据查询的描述性语言。一般地,大学计算机课程中,都设有数据库基础、数据库原理这类课程,其中会有非常长的篇幅介绍 SQL 语言,介绍 select、update、insert、delete,都是非常标准的通用语法,只是大学老师通常不会跟你说,实际工程中使用到的 SQL 要比教程中的复杂得多。

为什么只是 select、update、insert、delete,因为我们在使用数据时,从数据表中查询数据、修改删除表中数据、往表中添加数据等操作。

数学家很早以前就把帮我们把数据使用的操作需求归纳好了。

DML Data Manipulate language,数据操作语言。上面提到的都是,比较标准化。市面上的多个不同的数据库产品上运行 DML,语法都比较接近,有时还通用(mssql 的语法特殊符号比较多)。

DDL Data Define language,数据定义语言。上面提到了数据操作语言,其中 数据 是被操作的对象。这些对象的属性(表结构、字段的数据类型、物理特性等)使用 DDL 来定义。所以,使用数据前,需要在数据库中使用 DDL ,创建相关的环境(表),然后才可能使用 DML(DML 所操作的表、视图和字段都需要实际上已经存在的)。不同的数据库还提供了不同的环境 DDL,如表空间维护等,视具体特定的数据库而定。

某种定义上,SQL 与 javascript、python 等脚本语言的性质是一致的,需要运行环境把它解释并编译成更低层次的代码来执行逻辑。运行 SQL 的环境(数据库)本身就可以看成是一个带有大量数据环境变量的超级 runtime,这些数据环境变量(表)的值(表中的数据)是事先预置(从其它地方迁移过来或者不断积累更新(CRUD)所形成的状态)。

关系型数据库,有一种叫execute plan(执行计划)的东西,有些数据库还提供了select * from ... for explain的语法,来直接得到它。

它可以把一个完整的 SQL 分解多个级联嵌套的步骤,使用一个多于 2 个表或者视图关联的查询,就可以非常较明显的看到执行计划中的顺序。

执行计划并不只是数据操作过程的可视化,它还是数据库将 SQL 编译成低层次数据访问代码的高级可视化视图。故而,这个执行计划是数据库专用的(不同的数据库,所生成的执行计划不同,即使同一个 SQL,在表数据大小不同时所产生的执行计划都可能不一样。这与数据库内部引擎的实现有关)。也因此,执行计划在 SQL 调优时非常有用,相当于一个逻辑过程的步骤化分解。通过改变这个执行计划,或者拆解 SQL 来得到其他想要的执行计划(需要非常丰富的经验积累)。如果有前端基础的同学,浏览器有 DOM inspector,执行计划就类似于 SQL 的 inspector,用来观察 SQL 最终执行的过程。

在大数据产品,上面所说的执行计划同样存在,SQL 被直白的编译成 java 程序。可能因为 oracle 的高市占率原因,hivesql 与 oracle 的 SQL 实现兼容性非常高,如果有基于 oracle 实现的 OLAP 型分析应用,存储过程迁移成本会低很多。

select ... from ... [where ...] 从某数据源(这里指具体的数据集)获取目标信息(具体的属性),当然也可以添加过滤条件

update ... set ... [where] ...] 对目标数据进行修改。这里隐藏了一个概念,目标数据集。 经验初浅的,关注 set …(要如何修改) 经验丰富,一般更习惯于先关注 update … [where …] (要修改什么,为什么修改) 更高级别的问题。

delete from ... [where ...] 与 update 的逻辑类似。

insert into ... (values(...) | select ... from [where ...]) insert into 有两个重要的但非常不同的场景。

  • 在功能应用系统中,最常见的是 insert into ... values(...),实现向系统里添加业务数据(非常重要的数据生产方式);
  • 在数据开发领域,通常所需要的数据已经存在系统中的某个位置,这时往往使用的是 insert into ... select ... from ...[where ...]。

merge into insert into 与 update 的结合体。除 merge 外,还有诸如 update set [where …] [delete …] 这种变种。

先抛开容易搞混乱的变种,它对简化问题理解没有帮助。我们的目的是从上面的语法来提炼帮助理解、从而形成容易理解、记忆、使用的模式,这才是主要矛盾。

select、update、delete、insert,可以分为 数据状态发生变化(修改、删除、插入)、**数据状态不会发生变化(查询)**两大类。

可以进一步的抽象成下图的形式。

查询是确定数据源、目标集的过程,增删改为对目标集的二次加工。有时可能会觉得二次加工时非常花费精力,其实查询才是重点,在这个确定数据源与目标集的过程中,往往涉及业务问题的理解、分析,加工只是附带的操作需求。本节所说的思维方式,以及后文所要谈的内容,都是围绕这个部分。

熟悉数据开发的人,可能都会有这种经历,SQL 不管再怎么长,它的代码量其实非常少的。若业务问题的理解不准确,编写的查询缺漏条件,那么查错并编写出正确的 SQL 所花费的时间,往往比编写出原来那个为了任务而凑数的 SQL 所花时间要多很多。一些死要面子的人始终不肯承认这一点。