SELECT JOIN
SQL 语法分析
JOIN 查询有点像我们的 Expression,就是可以 查询套查询无限套下去。
MySQL
SQLite
PostgreSQL
和 MySQL、SQLite 也差不多
JOIN 语法总结
** JOIN 语法有两种形态 **
- JOIN … ON
- JOIN … USING:USING 后面使用的是列
** JOIN 本身有:**
- INNER JOIN, JOIN
- LEFT JOIN,RIGHT JOIN
也就是说 JOIN 的结构大概可以描述成下面这几种,而且还可以嵌套。
- 表 JOIN 表
- (表 JOIN 表) JOIN 表
- 表 JOIN 子查询
- 子查询 JOIN 子查询
开源实例
Beego JOIN 查询
** Beego 的 JOIN 查询主要出现在两个地方 :**
QueryBuilder 接口设计了 InnerJoin、LeftJoin 和 RightJoin 三个方法
Beego 本身支持一对一、一对多和多对多的关联关 系,所以如果设置了正确的关联关系,那么 Beego 在部分情况下会生成 JOIN 查
GROM JOIN 查询
GORM中 JOIN 查询主要是为了所谓的 Preload 而服务的
相关接口与方法的设计
实现简单的 JOIN 其实不怎么复杂,主要功能包括起别名、选择列、复杂一点的是 JOIN 可以嵌套。
常用的 JOIN 结构大概就是下面这几个:
- 表 A JOIN 表 B ON …
- 表 A AS 新名字 JOIN 表 B AS 新名字 ON …
- 表 A JOIN (表 B JOIN 表 C ON …) ON …
之前处理 FROM 后面那个位置的时候是直接用的数据库表名,但是那个位置其实可以放的玩意有表名、JOIN、子查询,这明摆了是要有一个抽象的,官方文档也告诉你了,叫 table_references(这里叫表表达式)。但是这三种对象的处理方式肯定是不一样的,语句形态差的都很远,基本没有共同点。
TableReference 抽象
- Table: 代表普通的表
- Join:代表 JOIN 查询
- Subquery:子查询
type TableReference interface {
tableAlias() string
}
TableReference 可以在将来有需要的时候 不断增加方法。
JoinBuilder 和 Join 定义
var _ TableReference = Join{}
type JoinBuilder struct {
left TableReference
right TableReference
typ string
}
type Join struct {
left TableReference
right TableReference
typ string
on []Predicate
using []string
}
func (j Join) tableAlias() string {
return ""
}
JoinBuilder 里面的 On 和 Using 是终结方法,也就是 直接返回了 Join , 这种设计可以避免用户同时调用 On 或者 Using。
func (j *JoinBuilder) On(ps...Predicate) Join {
return Join {
left: j.left,
right: j.right,
on: ps,
typ: j.typ,
}
}
func (j *JoinBuilder) Using(cs...string) Join {
return Join {
left: j.left,
right: j.right,
using: cs,
typ: j.typ,
}
}
JOIN 本身也可以进一步 JOIN,所以我们 同样需要在 Join 上面定义类似的方法。 同样地,子查询也可以用来构造 JOIN 查 询。
func (j Join) Join(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: j,
right: target,
typ: "JOIN",
}
}
func (j Join) LeftJoin(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: j,
right: target,
typ: "LEFT JOIN",
}
}
func (j Join) RightJoin(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: j,
right: target,
typ: "RIGHT JOIN",
}
}
Table ( 普通表 )
Table 代表一个普通的表,它也是 JOIN 查询的起点
type Table struct {
entity any
alias string
}
func TableOf(entity any) Table {
return Table{
entity: entity,
}
}
func (t Table) tableAlias() string {
return t.alias
}
func (t Table) As(alias string) Table {
return Table {
entity: t.entity,
alias: alias,
}
}
func (t Table) Join(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: t,
right: target,
typ: "JOIN",
}
}
func (t Table) LeftJoin(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: t,
right: target,
typ: "LEFT JOIN",
}
}
func (t Table) RightJoin(target TableReference) *JoinBuilder {
return &JoinBuilder{
left: t,
right: target,
typ: "RIGHT JOIN",
}
}
重构 Selector
将 From 改为接收一个 TableReference 作为 输入,后续 subquery 同样可以复用这个方法。
func (s *Selector[T]) From(tbl TableReference) *Selector[T] {
s.table = tbl
return s
}
- case nil : 如果用户没有调用 From 方法
- case Table:用户传入了一个普通的表
- case Join:是一个 Join 查询
func (s *Selector[T]) buildTable(table TableReference) error {
switch tab := table.(type) {
case nil:
s.quote(s.model.TableName)
case Table:
m, err := s.r.Get(tab.entity)
if err != nil {
return err
}
s.quote(m.TableName)
if tab.alias != "" {
s.sb.WriteString(" AS ")
s.quote(tab.alias)
}
case Join:
return s.buildJoin(tab)
default:
return errs.NewErrUnsupportedExpressionType(tab)
}
return nil
}
func (s *Selector[T]) buildJoin(tab Join) error {
s.sb.WriteByte('(')
if err := s.buildTable(tab.left); err != nil {
return err
}
s.sb.WriteString(" ")
s.sb.WriteString(tab.typ)
s.sb.WriteString(" ")
if err := s.buildTable(tab.right); err != nil {
return err
}
if len(tab.using) > 0 {
s.sb.WriteString(" USING (")
for i, col := range tab.using {
if i > 0 {
s.sb.WriteByte(',')
}
err := s.buildColumn(Column{name: col}, false)
if err != nil {
return err
}
}
s.sb.WriteString(")")
}
if len(tab.on) > 0 {
s.sb.WriteString(" ON ")
err := s.buildPredicates(tab.on)
if err != nil {
return err
}
}
s.sb.WriteByte(')')
return nil
}
重构列校验逻辑
在原本不支持 JOIN 查询的时候,只需要 看一下操作的元数据里面有没有这个列。在支持了 JOIN 之后,那么所有的列、聚合函 数都可能有一个拥有者(owner)。 例如** t1.col1 其中 t1 就是 col1 的拥有者**。 那么校验逻辑就是:
- **如果用户指定了表,那么就检查指定的表上 面有没有这个列 **
- 如果没有指定表,就走老逻辑,也就是右图 case nil 的分支
func (s *Selector[T]) buildColumns() error {
if len(s.columns) == 0 {
s.sb.WriteByte('*')
return nil
}
for i, c := range s.columns {
if i > 0 {
s.sb.WriteByte(',')
}
switch val := c.(type) {
case Column:
// buildColumn 方法
if err := s.buildColumn(val, true); err != nil {
return err
}
case Aggregate:
if err := s.buildAggregate(val, true); err != nil {
return err
}
case RawExpr:
s.raw(val)
default:
return errs.NewErrUnsupportedSelectable(c)
}
}
return nil
}
buildColumn 方法
func (s *Selector[T]) buildColumn(c Column, useAlias bool) error {
err := s.builder.buildColumn(c.table, c.name)
if err != nil {
return err
}
if useAlias {
s.buildAs(c.alias)
}
return nil
}
// buildColumn 构造列
// 如果 table 没有指定,我们就用 model 来判断列是否存在
func (b *builder) buildColumn(table TableReference, fd string) error {
var alias string
if table != nil {
alias = table.tableAlias()
}
if alias != "" {
b.quote(alias)
b.sb.WriteByte('.')
}
colName, err := b.colName(table, fd)
if err != nil {
return err
}
b.quote(colName)
return nil
}
func (b *builder) colName(table TableReference, fd string) (string, error) {
switch tab := table.(type) {
case nil:
fdMeta, ok := b.model.FieldMap[fd]
if !ok {
return "", errs.NewErrUnknownField(fd)
}
return fdMeta.ColName, nil
case Table:
m, err := b.r.Get(tab.entity)
if err != nil {
return "", err
}
fdMeta, ok := m.FieldMap[fd]
if !ok {
return "", errs.NewErrUnknownField(fd)
}
return fdMeta.ColName, nil
case Join:
colName, err := b.colName(tab.left, fd)
if err != nil {
return colName, nil
}
return b.colName(tab.right, fd)
default:
return "", errs.NewErrUnsupportedExpressionType(tab)
}
}
总结
- GORM 的 Preload 是什么?本质上就是一个 JOIN 查询,并且严格来说,在 Go 语言里面是很难实现 lazy load的。GORM 的 Preload 就是通过 Join 把相关的数据都查询出来,并且组装成结构体 ;
- WHERE、ON 和 HAVING 的区别:在 JOIN 查询里面,一般的建议 都是尽量把条件放到 ON 上面,这样 JOIN 生成的中间数据要少很多;
- JOIN 的执行原理:可以近似理解为一个双重循环 。