623.md 11.8 KB
Newer Older
Lab机器人's avatar
readme  
Lab机器人 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
# SQL Query Guidelines

> 原文:[https://docs.gitlab.com/ee/development/sql.html](https://docs.gitlab.com/ee/development/sql.html)

*   [Using LIKE Statements](#using-like-statements)
*   [LIKE & Indexes](#like--indexes)
*   [Reliably referencing database columns](#reliably-referencing-database-columns)
    *   [Good (prefer)](#good-prefer)
    *   [Bad (avoid)](#bad-avoid)
*   [Plucking IDs](#plucking-ids)
*   [Inherit from ApplicationRecord](#inherit-from-applicationrecord)
*   [Use UNIONs](#use-unions)
*   [Ordering by Creation Date](#ordering-by-creation-date)
*   [Use WHERE EXISTS instead of WHERE IN](#use-where-exists-instead-of-where-in)
*   [`.find_or_create_by` is not atomic](#find_or_create_by-is-not-atomic)

# SQL Query Guidelines[](#sql-query-guidelines "Permalink")

本文档介绍了使用 ActiveRecord / Arel 或原始 SQL 查询编写 SQL 查询时要遵循的各种准则.

## Using LIKE Statements[](#using-like-statements "Permalink")

搜索数据的最常见方法是使用`LIKE`语句. 例如,要获取标题以" WIP:"开头的所有问题,您可以编写以下查询:

```
SELECT *
FROM issues
WHERE title LIKE 'WIP:%'; 
```

在 PostgreSQL 上, `LIKE`语句区分大小写. 要执行不区分大小写的`LIKE` ,必须改为使用`ILIKE` .

要自动处理此问题,您应该使用 Arel 而不是原始 SQL 片段使用`LIKE`查询,因为 Arel 在 PostgreSQL 上自动使用`ILIKE` .

```
Issue.where('title LIKE ?', 'WIP:%') 
```

您可以这样写:

```
Issue.where(Issue.arel_table[:title].matches('WIP:%')) 
```

根据所使用的数据库,此处的`matches`生成正确的`LIKE` / `ILIKE`语句.

如果您需要链接多个`OR`条件,也可以使用 Arel 进行此操作:

```
table = Issue.arel_table

Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%'))) 
```

在 PostgreSQL 上,这将产生:

```
SELECT *
FROM issues
WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%') 
```

## LIKE & Indexes[](#like--indexes "Permalink")

在一开始使用带有通配符的`LIKE` / `ILIKE`时,PostgreSQL 将不使用任何索引. 例如,这将不使用任何索引:

```
SELECT *
FROM issues
WHERE title ILIKE '%WIP:%'; 
```

因为`ILIKE`的值以通配符开头,所以数据库无法使用索引,因为它不知道从何处开始扫描索引.

幸运的是,PostgreSQL *确实*提供了一种解决方案:trigram GIN 索引. 可以如下创建这些索引:

```
CREATE INDEX [CONCURRENTLY] index_name_here
ON table_name
USING GIN(column_name gin_trgm_ops); 
```

这里的关键是`GIN(column_name gin_trgm_ops)`部分. 这将创建一个[GIN 索引](https://s0www0postgresql0org.icopy.site/docs/current/gin.html) ,并将操作符类设置为`gin_trgm_ops` . 这些索引*可*通过使用`ILIKE` / `LIKE` ,并可能导致大大改进的性能. 这些索引的缺点之一是它们很容易变大(取决于索引的数据量).

为了使这些索引的命名保持一致,请使用以下命名模式:

```
index_TABLE_on_COLUMN_trigram 
```

例如,一个`issues.title`的 GIN / `issues.title`索引将称为`index_issues_on_title_trigram` .

Due to these indexes taking quite some time to be built they should be built concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of just `CREATE INDEX`. Concurrent indexes can *not* be created inside a transaction. Transactions for migrations can be disabled using the following pattern:

```
class MigrationName < ActiveRecord::Migration[4.2]
  disable_ddl_transaction!
end 
```

例如:

```
class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration[4.2]
  disable_ddl_transaction!

  def up
    execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
    execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
  end

  def down
    remove_index :users, :index_on_users_lower_username
    remove_index :users, :index_on_users_lower_email
  end
end 
```

## Reliably referencing database columns[](#reliably-referencing-database-columns "Permalink")

默认情况下,ActiveRecord 返回查询的数据库表中的所有列. 在某些情况下,可能需要自定义返回的行,例如:

*   仅指定几列以减少从数据库返回的数据量.
*   包括`JOIN`关系中的列.
*   执行计算( `SUM``COUNT` ).

在此示例中,我们指定列,但不指定其表:

*   `projects`表的`path`
*   `merge_requests`表中的`user_id`

查询:

```
# bad, avoid
Project.select("path, user_id").joins(:merge_requests) # SELECT path, user_id FROM "projects" ... 
```

稍后,一项新功能将一个额外的列添加到`projects`表: `user_id` . 在部署期间,可能会在很短的时间范围内执行数据库迁移,但是尚未部署新版本的应用程序代码. 当上述查询在此期间执行时,查询将失败,并显示以下错误消息: `PG::AmbiguousColumn: ERROR: column reference "user_id" is ambiguous`

问题是由从数据库中选择属性的方式引起的. 的`user_id`列存在于两个`users``merge_requests`表. 查询计划者无法确定在查找`user_id`列时要使用哪个表.

在编写自定义的`SELECT`语句时,最好**使用表名明确指定列** .

### Good (prefer)[](#good-prefer "Permalink")

```
Project.select(:path, 'merge_requests.user_id').joins(:merge_requests)

# SELECT "projects"."path", merge_requests.user_id as user_id FROM "projects" ... 
```

```
Project.select(:path, :'merge_requests.user_id').joins(:merge_requests)

# SELECT "projects"."path", "merge_requests"."id" as user_id FROM "projects" ... 
```

使用 Arel( `arel_table` )的示例:

```
Project.select(:path, MergeRequest.arel_table[:user_id]).joins(:merge_requests)

# SELECT "projects"."path", "merge_requests"."user_id" FROM "projects" ... 
```

编写原始 SQL 查询时:

```
SELECT projects.path, merge_requests.user_id FROM "projects"... 
```

When the raw SQL query is parameterized (needs escaping):

```
include ActiveRecord::ConnectionAdapters::Quoting

"""
SELECT #{quote_table_name('projects')}.#{quote_column_name('path')}, #{quote_table_name('merge_requests')}.#{quote_column_name('user_id')} FROM ...
""" 
```

### Bad (avoid)[](#bad-avoid "Permalink")

```
Project.select('id, path, user_id').joins(:merge_requests).to_sql

# SELECT id, path, user_id FROM "projects" ... 
```

```
Project.select("path", "user_id").joins(:merge_requests)
# SELECT "projects"."path", "user_id" FROM "projects" ...

# or

Project.select(:path, :user_id).joins(:merge_requests)
# SELECT "projects"."path", "user_id" FROM "projects" ... 
```

给定列列表后,ActiveRecord 尝试将参数与`projects`表中定义的列进行匹配,并自动在表名前添加前缀. 在这种情况下, `id`列不会有问题,但是`user_id`列可能返回意外数据:

```
Project.select(:id, :user_id).joins(:merge_requests)

# Before deployment (user_id is taken from the merge_requests table):
# SELECT "projects"."id", "user_id" FROM "projects" ...

# After deployment (user_id is taken from the projects table):
# SELECT "projects"."id", "projects"."user_id" FROM "projects" ... 
```

## Plucking IDs[](#plucking-ids "Permalink")

这还不够强调: **永远不要**使用 ActiveRecord 的`pluck`将一组值插入内存中,而只是将它们用作另一个查询的参数. 例如,这将使数据库**非常**悲伤:

```
projects = Project.all.pluck(:id)

MergeRequest.where(source_project_id: projects) 
```

相反,您可以只使用性能更好的子查询:

```
MergeRequest.where(source_project_id: Project.all.select(:id)) 
```

*唯一*应该使用`pluck`时间是您实际上需要对 Ruby 本身中的值进行操作(例如,将它们写入文件中)时. 在几乎所有其他情况下,您都应该问自己"我不仅可以使用子查询吗?".

根据我们的`CodeReuse/ActiveRecord`缔约方会议,您应仅在模型代码中使用诸如`pluck(:id)``pluck(:user_id)`之类的形式. 在前一种情况下,可以改用`ApplicationRecord` `.pluck_primary_key`帮助器方法. 在后者中,您应该在相关模型中添加一个小的辅助方法.

## Inherit from ApplicationRecord[](#inherit-from-applicationrecord "Permalink")

GitLab 代码库中的大多数模型应继承自`ApplicationRecord` ,而不是`ActiveRecord::Base` . 这样可以轻松添加辅助方法.

在数据库迁移中创建的模型存在此规则的例外. 由于这些应与应用程序代码隔离,因此它们应继续从`ActiveRecord::Base`继承子类.

## Use UNIONs[](#use-unions "Permalink")

UNION 在大多数 Rails 应用程序中并不是很常用,但是它们非常强大且有用. 在大多数应用程序中,查询倾向于使用大量 JOIN 来获取相关数据或基于特定条件的数据,但是 JOIN 性能会随着所涉及数据的增长而迅速恶化.

例如,如果要获取名称包含值*或*名称空间名称包含值的项目列表,大多数人会编写以下查询:

```
SELECT *
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE projects.name ILIKE '%gitlab%'
OR namespaces.name ILIKE '%gitlab%'; 
```

使用大型数据库,此查询可能很容易花费大约 800 毫秒来运行. 使用 UNION,我们改为编写以下内容:

```
SELECT projects.*
FROM projects
WHERE projects.name ILIKE '%gitlab%'

UNION

SELECT projects.*
FROM projects
JOIN namespaces ON namespaces.id = projects.namespace_id
WHERE namespaces.name ILIKE '%gitlab%'; 
```

反过来,此查询只需要 15 毫秒即可完成,同时返回完全相同的记录.

这并不意味着您应该在所有地方开始使用 UNION,但是在查询中使用大量 JOIN 并根据联接的数据过滤掉记录时要牢记这一点.

GitLab 带有一个`Gitlab::SQL::Union`类,可用于构建多个`ActiveRecord::Relation`对象的 UNION. 您可以按如下方式使用此类:

```
union = Gitlab::SQL::Union.new([projects, more_projects, ...])

Project.from("(#{union.to_sql}) projects") 
```

## Ordering by Creation Date[](#ordering-by-creation-date "Permalink")

根据记录的创建时间对记录进行排序时,只需按`id`列进行排序即可,而不必按`created_at`进行排序. 因为 ID 始终是唯一的,并且按照创建行的顺序递增,所以这将产生完全相同的结果. 这也意味着,由于默认情况下已经对`id`进行了索引,因此无需在`created_at`上添加索引以确保一致的性能.

## Use WHERE EXISTS instead of WHERE IN[](#use-where-exists-instead-of-where-in "Permalink")

虽然可以使用`WHERE IN``WHERE EXISTS`来生成相同的数据,但建议尽可能使用`WHERE EXISTS` . 尽管在许多情况下 PostgreSQL 可以很好地优化`WHERE IN`但在许多情况下`WHERE EXISTS`会好得多.

在 Rails 中,您必须通过创建 SQL 片段来使用它:

```
Project.where('EXISTS (?)', User.select(1).where('projects.creator_id = users.id AND users.foo = X')) 
```

然后,将按照以下内容生成查询:

```
SELECT *
FROM projects
WHERE EXISTS (
    SELECT 1
    FROM users
    WHERE projects.creator_id = users.id
    AND users.foo = X
) 
```

## `.find_or_create_by` is not atomic[](#find_or_create_by-is-not-atomic "Permalink")

`.find_or_create_by``.first_or_create`等方法的固有模式是它们不是原子的. 这意味着,它首先运行`SELECT` ,如果没有结果,则执行`INSERT` . 考虑到并发过程,因此存在竞争条件,这可能导致尝试插入两个相似的记录. 例如,这可能是不希望的,或者可能由于约束冲突而导致查询之一失败.

使用事务不能解决此问题.

为了解决这个问题,我们添加了`ApplicationRecord.safe_find_or_create_by` .

可以像平常的`find_or_create_by`一样使用此方法,但是它将调用包装在*新*事务中,如果由于`ActiveRecord::RecordNotUnique`错误而失败,则将重试.

为了能够使用此方法,请确保要在其上使用的模型继承自`ApplicationRecord` .