PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例

本文探讨了在PostgreSQL中使用IN谓词查询时索引未被利用的问题,分析了可能的原因,并提供了解决方案,包括如何正确创建索引以确保其在特定条件下能够被查询优化器使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有朋友A问:

where  username in ('张三','李四');username会不会走索引呢?

有朋友BCD等回答:

走不走索引和很多东西有关系的,走索引也不一定就效率高。

in 语句是走索引的, like '%xxx%'这种模糊搜索才不走索引。

其实,A朋友的问题是这样的,“IN谓词指定的有主键列的查询没有使用索引扫描”,如图一:

PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例 - 那海蓝蓝 - 那海蓝蓝的博客
 所以A朋友潜在的问题是:

   为什么有主键索引的列在进行IN谓词查询(非IN子查询)的时候,索引没有使用上?

 

分析原因如下:

解释一:

A朋友的查询语句,其实涉及的是“等价谓词重写技术”和“索引的使用”2个话题。

看上面的表,PG是不支持IN谓词重写的。但是,name列上如果存在索引,则可能利用到索引。

如果有索引列,pg是可以支持利用索引进行优化的,但是对于likeIN谓词,“等价谓词重写的优化技术,pg不支持”。

比如:

create table AA (a int unique, b varchar(10) unique, c int);

insert into aa values(1, 'a', 1);

insert into aa values(2, 'b', 2);

insert into aa values(3, 'c', 3);

explain select b from aa where b in ('a','ab');

可以得到如下的查询执行计划:

test=# explain select b from aa where b in ('a','ab');

                              QUERY PLAN

-----------------------------------------------------------------------

 Bitmap Heap Scan on aa  (cost=8.52..13.86 rows=2 width=38)

   Recheck Cond: ((b)::text = ANY ('{a,ab}'::text[]))

   ->  Bitmap Index Scan on aa_b_key  (cost=0.00..8.52 rows=2 width=0)

         Index Cond: ((b)::text = ANY ('{a,ab}'::text[]))

(4 行记录)

这表明索引是可以被使用到的。所以PostgreSQL尽管不支持IN谓词等价重写技术,但是索引还是能利用上的。

 

这时,有另外一个朋友给出解释:

如果那个情况发生了,证明你得做vacuum了,你的数据库表的统计值不对,导致pg的优化器做出了错误的判断。

 

这个问题的原因,是不是这样的呢?为了进一步定位问题,给出如下建议:

1.         name是建立了唯一索引的。

2.         如果有唯一索引,则使用索引的可能性很大。

3.         可以explain看一下查询执行计划。

4.         查查表结构、SQL语句、执行计划等等。

5.         通常觉得应该用而没有用上索引的,多是自己的语句存在小问题但不易发现。

6.         不管最终用in会不会走索引-----如果不用索引且数据量大一些,则慢。

7.         可以先看看你的查询执行计划,是否用了索引

8.         如果没有用索引,可以禁掉顺序扫描然后再执行,看查询执行计划是否使用了索引且花费比上一次低。

9.         如果那个情况发生了,证明你得做vacuum了,你的数据库表的统计值不对,导致pg的优化器做出了错误的判断。------这是种可能

10.     但放在主键索引上(前面A兄弟说的情况)下,vacuum执行后有效的可能性有、但不是很大,可以试试

 

其他朋友再问:

如果关闭顺序扫描后,执行计划使用索引。且cost低于顺序扫描。该怎么办呢?

回答:

1.         首先,这种情况(A朋友这起存在主键索引的情况)发生的概率(即不能利用索引的概率)不高。

2.         其次,如果真的发生了,可以在执行本条查询前,先禁掉seqscan,执行后再打开(打开的目的----保证不影响其他SQL

3.         第三,长期观察这样的情况是否一定发生?可能另外有潜在的原因,需要持续跟踪

 

分析到此,其实最可能的原因,是索引根本没有被使用到

 

于是,有朋友继续分析,如下图:

PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例 - 那海蓝蓝 - 那海蓝蓝的博客
 从图上可以看出,索引受到字符集中的collation的影响(d数据库的COLLATE的值是zh_CN.UTF8,不是C),根本没有被使用。

 

所以,问题的原因是:

要查询的数据类型和索引的数据类型不匹配,导致索引没有被使用。

解决问题的方式如下,如图二:

删除原先的索引,重新创建索引的语句为:

CREATE INDEX T3_NAME_I ON t3_name (name text_pattern_ops);

然后再执行同样的查询,从查询执行计划看,索引被使用。

PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例 - 那海蓝蓝 - 那海蓝蓝的博客
 

 查阅PG的文档,PG是这样解释这个问题的:

The operator classes text_pattern_ops, varchar_pattern_ops, and bpchar_pattern_ops support B-tree indexes on the types text, varchar, and char respectively. The difference from the default operator classes is that the values are compared strictly character by character rather than according to the locale-specific collation rules. This makes these operator classes suitable for use by queries involving pattern matching expressions (LIKE or POSIX regular expressions) when the database does not use the standard “C” locale. As an example, you might index a varchar column like this:

CREATE INDEX test_index ON test_table (col varchar_pattern_ops);

 

至此,这个问题得到解决。

 

另外,有朋友提到:

我觉得in like 只要有索引应该支持吧。其实有时觉得数据库这块的处理好有问题,原则上应该使用,如果能让用户指定索引使用方式就好啦

其实:

多数情况下,优化器已经能优化很多东西了,做得比较成熟了,所以交给优化器优化适用面更广一些。

个别时候,能指定hint则好,但使用者必须完全明确自己定义好的方式就是好的。


再另外,提到了“等价谓词查询”技术,给大家贴了张图,如图三:

PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例 - 那海蓝蓝 - 那海蓝蓝的博客
 

详情可以参见《数据库查询优化器的艺术》第8章对于PostgreSQL查询优化器的“等价谓词重写”技术的分析:

PostgreSQL中主键索引为什么不能被查询利用到?---索引使用情况一例 - 那海蓝蓝 - 那海蓝蓝的博客
 

 

<think>我们正在使用TypeORM,并且希望在批量插入数据时处理主键或唯一索引冲突:如果冲突则更新,否则插入。 在TypeORM中,我们可以使用`QueryBuilder`来实现类似MySQL的`ON DUPLICATE KEY UPDATE`功能,或者使用特定数据库的语法(如PostgreSQL的`ON CONFLICT ... DO UPDATE`)。 但是,需要注意的是,TypeORM并没有直接提供一个跨数据库的抽象方法来实现这个功能。因此,我们需要根据所使用的数据库类型来编写特定的查询。 以下以PostgreSQL和MySQL为例: 1. PostgreSQL:使用`ON CONFLICT`子句。 2. MySQL:使用`ON DUPLICATE KEY UPDATE`。 由于TypeORM的`QueryBuilder`支持原始SQL语句的插入,我们可以使用`InsertQueryBuilder`的`onConflict`方法(对于PostgreSQL)或者使用`createQueryBuilder`结合原始SQL(对于MySQL)来实现。 但是,请注意,TypeORM的版本更新可能会带来一些变化。在当前的TypeORM版本(0.3.x)中,对于PostgreSQL,我们可以使用`InsertQueryBuilder`的`onConflict`方法。对于MySQL,我们可能需要使用`getConnection().query`来执行原始SQL。 另外,我们也可以使用`Repository`的`upsert`方法(在TypeORM 0.2.45+版本中引入),它正是用于处理冲突时更新。但是,`upsert`方法在批量操作时可能效率不高,因为它会先查询是否存在,然后再决定插入或更新。不过,在0.3.x版本中,`upsert`方法进行了优化,对于批量操作也支持。 因此,我们有两种方法: 方法一:使用`Repository.upsert`方法(推荐,简单且跨数据库,但需要数据库驱动支持) 方法二:使用查询构建器(针对特定数据库) 下面分别介绍: ### 方法一:使用`upsert`方法 从TypeORM v0.2.45开始,`Repository`提供了`upsert`方法。这个方法可以插入或更新一个或多个实体。它需要数据库支持(如PostgreSQL、MySQL、SQLite等),并且需要表有唯一约束或主键。 语法: ```typescript await repository.upsert(entities, conflictPathsOrOptions); ``` 其中: - `entities`:要插入或更新的实体数组。 - `conflictPathsOrOptions`:可以是一个字符串数组(指定唯一约束的列名),或者一个选项对象(指定冲突的列和要更新的列)。 示例: ```typescript import { getManager } from 'typeorm'; // 假设有一个User实体,主键是id,还有一个唯一索引是email const userRepository = getManager().getRepository(User); // 批量插入,如果冲突(根据email唯一索引),则更新冲突的列 await userRepository.upsert([ { email: 'user1@example.com', name: 'User One' }, { email: 'user2@example.com', name: 'User Two' } ], ['email']); // 这里指定冲突的字段是email // 注意:上面的更新会更新除了email以外的其他列(如果冲突发生)。如果你只想更新部分列,可以使用选项形式: await userRepository.upsert([ { email: 'user1@example.com', name: 'User One' }, { email: 'user2@example.com', name: 'User Two' } ], { conflictPaths: ['email'], // 指定冲突的列 skipUpdateIfNoValuesChanged: true, // 可选,如果值没有改变则跳过更新(仅某些数据库支持) upsertType: 'on-conflict-do-update' // 可选,默认就是这个 }); ``` 但是,请注意,`upsert`方法在内部可能会生成一条SQL语句(对于支持的数据库),因此效率较高。 但是,在TypeORM的0.3.x版本中,`upsert`方法支持批量,并且对于PostgreSQL和MySQL都适用。 ### 方法二:使用查询构建器(针对特定数据库) 如果你使用的数据库PostgreSQL,可以使用`InsertQueryBuilder`的`onConflict`方法。 示例(PostgreSQL): ```typescript await getManager() .createQueryBuilder() .insert() .into(User) .values([ { email: 'user1@example.com', name: 'User One' }, { email: 'user2@example.com', name: 'User Two' } ]) .onConflict(`("email") DO UPDATE SET "name" = EXCLUDED."name"`) .execute(); ``` 对于MySQL,我们可以使用`ON DUPLICATE KEY UPDATE`,但是TypeORM的查询构建器没有直接提供这个方法,所以我们可以使用原始SQL: ```typescript // 对于MySQL await getManager().query(` INSERT INTO user (email, name) VALUES ('user1@example.com', 'User One'), ('user2@example.com', 'User Two') ON DUPLICATE KEY UPDATE name = VALUES(name) `); ``` 或者,我们可以使用`InsertQueryBuilder`的`orUpdate`方法(在0.2.41版本引入,但注意在0.3.x中可能有所变化)?实际上,在0.3.x版本中,`InsertQueryBuilder`提供了`orUpdate`方法用于MySQL的`ON DUPLICATE KEY UPDATE`,但需要指定更新的列。 示例(MySQL): ```typescript await getManager() .createQueryBuilder() .insert() .into(User) .values([ { email: 'user1@example.com', name: 'User One' }, { email: 'user2@example.com', name: 'User Two' } ]) .orUpdate(['name'], ['email']) // 第一个数组是要更新的列,第二个数组是冲突的列(唯一索引) .execute(); ``` 注意:`orUpdate`方法在内部会生成MySQL的`ON DUPLICATE KEY UPDATE`语句,但要求冲突的列必须是唯一索引主键。 但是,请注意,`orUpdate`方法在PostgreSQL中不可用,它只适用于MySQL和SQLite(部分支持)。 ### 总结 根据你的数据库类型和TypeORM版本,可以选择: 1. 如果使用TypeORM 0.2.45以上,推荐使用`repository.upsert`方法,因为它更简洁,且TypeORM会处理跨数据库的差异。 2. 如果使用特定数据库,并且需要更精细的控制,可以使用查询构建器的方法(PostgreSQL用`onConflict`,MySQL用`orUpdate`)或者原始SQL。 请注意,在批量操作时,使用`upsert`方法可能更高效,因为它只需要一次数据库交互。 另外,请确保你的数据库表已经设置了唯一索引(或主键),这样才能触发冲突。 最后,请根据你的实际数据库类型和TypeORM版本来选择合适的方法。 参考文档: [TypeORM Repository API](https://typeorm.io/repository-api) [TypeORM Insert Query Builder](https://typeorm.io/insert-query-builder) 由于TypeORM的文档可能会更新,建议查阅最新版本的文档。 [^1]: TypeORM支持多种数据库,但具体功能可能因数据库而异。</think>在 TypeORM 中处理批量插入时的主键或唯一索引冲突(冲突时更新,否则插入),可以通过以下方法实现: ### 方法 1:使用 `Repository.upsert()`(推荐,TypeORM v0.2.45+) ```typescript import { getRepository } from 'typeorm'; const userRepository = getRepository(User); // 批量插入/更新 await userRepository.upsert( [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ], ['id'] // 冲突字段(主键或唯一索引) ); ``` - **原理**:生成 `ON CONFLICT ... DO UPDATE`(PostgreSQL)或 `ON DUPLICATE KEY UPDATE`(MySQL)语句 - **特点**: - 自动检测冲突字段 - 批量操作高效 - 支持所有数据库[^1] - **注意**:需确保指定的冲突字段是主键或唯一索引 ### 方法 2:使用 `QueryBuilder`(精细控制) #### PostgreSQL 示例: ```typescript await userRepository .createQueryBuilder() .insert() .into(User) .values([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]) .onConflict(`("id") DO UPDATE SET "name" = EXCLUDED."name"`) .execute(); ``` #### MySQL 示例: ```typescript await userRepository .createQueryBuilder() .insert() .into(User) .values([ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]) .orUpdate(['name'], ['id']) // 更新字段, 冲突字段 .execute(); ``` ### 方法 3:事务+批量查询(兼容旧版本) ```typescript await getManager().transaction(async (manager) => { const users = [/* 实体数组 */]; for (const user of users) { const exists = await manager.findOne(User, { id: user.id }); exists ? await manager.update(User, user.id, user) : await manager.insert(User, user); } }); ``` - **适用场景**:TypeORM 旧版本或不支持 `upsert` 的数据库 - **缺点**:性能较低,非原子操作 ### 关键注意事项 1. **冲突字段**:必须指定主键或唯一索引字段 2. **数据库支持**: - PostgreSQL:需要 `ON CONFLICT` 子句 - MySQL:需要 `ON DUPLICATE KEY UPDATE` - SQLite:支持类似语法 3. **性能**:`upsert` > `QueryBuilder` > 事务循环 4. **锁机制**:批量操作时数据库可能加锁,建议控制批次大小(如每次 100-1000 条) > **最佳实践**:优先使用 `repository.upsert()`,它自动适配不同数据库语法,并优化批量操作性能[^1]。对于复杂更新逻辑(如条件更新),可使用 `QueryBuilder` 自定义 SQL。 [^1]: TypeORM 支持 MySQL、PostgreSQL、SQLite 等主流数据库的冲突处理机制,只需正确配置驱动即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值