索引选择度问题优化整理
来源:SegmentFault
时间:2023-01-09 12:18:54 298浏览 收藏
对于一个数据库开发者来说,牢固扎实的基础是十分重要的,golang学习网就来带大家一点点的掌握基础知识点。今天本篇文章带大家了解《索引选择度问题优化整理》,主要介绍了MySQL、postgresql、Java、数据库、后端,希望对大家的知识积累有所帮助,快点收藏起来吧,否则需要时就找不到了!
之前在搞宜搭元数据底层索引优化的时候,针对一些查询时快时慢,以及一些索引选择的问题,研究过,也基于看过的一些案例以及自身归纳思考,下面整理分享下;
一般我们为了加快查询速度,会设计索引,当然有索引情况下,大多是会命中去走索引查询;但是呢:
- 存储优化器去执行,就算加了索引,在一定时候有可能没用到索引,速度会更慢点,这是为什么不用?
- 有些时候同一个用户不同时间去请求,产生相同SQL语句去查询也可能出现不同的快慢性能,这又是为什么?
- 就算命中了索引,速度可能更慢,这最后又是为什么?
案例一
-- 创建测试表 CREATE TABLE `t` ( `id` int primary key auto_increment, `a` int default null, `b` int default null, KEY `a` (`a`), KEY `b` (`b`) ) ENGINE=InnoDB; -- 插入10w行测试数据 delimiter ;; create procedure idata() begin declare i int; set i=1; while(i
mysql全表扫描
explain select from t where a between 10000 and 20000; 
通过explain的执行结果我们可以看出,上面的SQL语句并没有走我们的索引a,而是直接使用了全表扫描。
-- 强制走索引a explain select from t force index(a) where a between 10000 and 20000;

通过explain的执行结果我们可以看出,上面的SQL语句我们通过force index(a)以后,确实使用了索引。
-- 开启慢日志 set global slow_query_log = true; set long_query_time = 0; -- 分别执行不走索引和走索引的SQL select from t where a between 10000 and 20000; select from t force index(a) where a between 10000 and 20000;


可以看出走索引的查询比不走索引的查询快了将近10ms。
但是存储优化器默认没走索引的查询,虽然加了索引
案例二
针对某个平台有张消息发送交流的表,规模达到数千万行级,PG存储;消息表上的主查询通常极快,但是也遇到了一些间歇的慢查询超时。慢查询不但影响了消息功能的用户体验,而且加大了整个系统的负荷,拖慢了其他功能的用户体验。
这个查询长这样:
SELECT messages.* FROM messages WHERE messages.deleted_at IS NULL AND messages.namespace = ? AND ( jsonb_extract_path_text(context, 'topic') IN (?, ?) OR jsonb_extract_path_text(context, 'topic') LIKE ? ) AND ( context @> '{"involved_parties":[{"id":1,"type":1}]}'::jsonb ) ORDER BY messages.created_at ASC在context上有两个索引
- context列上的GIN索引
- jsonb_extract_path_text(context, ‘topic’)表达式上的BTREE表达式索引
看下上面语句偶尔慢的时候QUERY PLAN:
UERY PLAN
------------------------------------------------------------------------------
Sort (cost=540.08..540.09 rows=3 width=915)
Sort Key: created_at
-> Bitmap Heap Scan on messages (cost=536.03..540.06 rows=3 width=915)
Recheck Cond: (((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[])) OR (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~~ '?%'::text)) AND (context @> '{"involved_parties": [{"id": 1, "type": 1}]}'::jsonb))
Filter: ((deleted_at IS NULL) AND ((namespace)::text = '?'::text) AND ((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[])) OR (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~~ '?%'::text)))
-> BitmapAnd (cost=536.03..536.03 rows=1 width=0)
-> BitmapOr (cost=20.13..20.13 rows=249 width=0)
-> Bitmap Index Scan on index_messages_on_topic_key_string (cost=0.00..15.55 rows=249 width=0)
Index Cond: (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[]))
-> Bitmap Index Scan on index_messages_on_topic_key_string (cost=0.00..4.57 rows=1 width=0)
Index Cond: ((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~>=~ '?'::text) AND (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~ Bitmap Index Scan on index_messages_on_context (cost=0.00..515.65 rows=29820 width=0)
Index Cond: (context @> '{"involved_parties": [{"id": 1, "type": 1}]}'::jsonb)
(这个查询计划来自EXPLAIN,由于EXPLAIN ANALYZE超时)看下上面语句快的时候QUERY PLAN:
QUERY PLAN
------------------------------------------------------------------------------
Sort (cost=667.75..667.76 rows=3 width=911) (actual time=0.093..0.094 rows=7 loops=1)
Sort Key: created_at
Sort Method: quicksort Memory: 35kB
-> Bitmap Heap Scan on messages (cost=14.93..667.73 rows=3 width=911) (actual time=0.054..0.077 rows=7 loops=1)
Recheck Cond: ((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[])) OR (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~~ '?%'::text))
Filter: ((deleted_at IS NULL) AND (context @> '{"involved_parties": [{"id": 1, "type": 1}]}'::jsonb) AND ((namespace)::text = '?'::text) AND ((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[])) OR (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~~ '?%'::text)))
Heap Blocks: exact=7
-> BitmapOr (cost=14.93..14.93 rows=163 width=0) (actual time=0.037..0.037 rows=0 loops=1)
-> Bitmap Index Scan on index_messages_on_topic_key_string (cost=0.00..10.36 rows=163 width=0) (actual time=0.029..0.029 rows=4 loops=1)
Index Cond: (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) = ANY ('{?,?}'::text[]))
-> Bitmap Index Scan on index_messages_on_topic_key_string (cost=0.00..4.57 rows=1 width=0) (actual time=0.007..0.007 rows=7 loops=1)
Index Cond: ((jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~>=~ '?'::text) AND (jsonb_extract_path_text(context, VARIADIC '{topic}'::text[]) ~图中执行计划可以看出,(context @> '{"involved_parties": [{"id": 1, "type": 1}]}'::jsonb) 没走索引更快,走了索引还更慢; 而且同一个SQL查询,有些时候走索引,有些时候又不走;
案例分析
这里针对案例二进行分析:
了解下索引区别
GIN是PostgreSQL提供的一款用于复杂值的索引引擎,一般用于数组、JSON或文本等的数据结构。GIN的设计用途是索引那些可对内部结构做细分的数据,这样就可以查找数据内部的子数据了。BTREE是PostgreSQL的默认索引引擎,能对简单值做相等性比较或范围查询。表达式索引是PostgreSQL提供的一种强力的索引类型,能对一个表达式(而不是一个列)做索引。JSONB类型一般只能用GIN这样的索引引擎,因为BTREE只支持标量类型(可以理解为“没有内部结构的简单值类型”)。因此,context列上的jsonb_extract_path_text(context, ‘topic’)表达式可以用BTREE索引,因为它返回字符串类型。不同于BTREE索引统一而一致的表示格式,GIN索引的内容可以因所用数据类型和操作符类型的不同而极为不同。而且考虑到查询参数的选择度有较高的多样性,GIN索引更适用于一些特定的查询,不像BTREE索引广泛适用于相等性比较和范围查询。
预分析
一个查询通常会先做索引扫描以初筛,再对筛选后的范围做表扫描(一个特例是,当索引扫描足以覆盖所需的所有数据列时,则无需表扫描)。为了最大化性能,索引要有较好的选择度来缩小范围,以减少甚至避免之后的表扫描。条件context @> ‘{“involved_parties”:[{“id”:1,”type”:1}]}’::jsonb能使用context列上的GIN索引,但是这并不是一个好选择,因为{“id”:1,”type”:1}这个值是存在于大多数行中的一个特殊值(这数字就很特殊,像管理员的号码)。因此,GIN索引对于这个条件的选择度很差。实际上,这个查询中的其他条件已能提供很好的选择度,所以永远不需要为这个条件使用索引。
针对快慢查询分析
慢查询路径 快查询路径


如图可见,这个慢查询计划比快查询计划更复杂。它多了一个”BitmapAnd”和一个扫描index 3的”Bitmap Index Scan”节点(index 3是context列上的GIN索引)。若index 3低效率,总体性能就会降低。
当成本估计准确时,查询计划器工作得很好。但是JSONB上的GIN索引的成本估计不是很准确的。由观测可见,它认为这个索引的选择度为0.001(这是一个硬编码的固定值),也就是说它假设任何相关的查询都会选择表中所有行的0.1%,但在我们这个场景它实际会选择90%的行,所以这个假设不成立。错误的假设使查询计划器低估了慢查询计划的成本。虽然JSONB类型的列也有一些统计信息,但好像没有起到作用。
结论
所以说有些时命中索引不一定就快,而且索引优化器也不一定是准确的,可能会执行更慢;
一些有用的原则
原则1: 少即是多
管理好索引
更多的索引并不意味着更好的性能。事实上,每增加一个索引都会降低写操作的性能。如果查询计划器选择了不高效的索引,那么查询仍然会很慢。
不要堆积索引(例如每一列都建索引就是不可取的)。试着尽可能删除一些索引吧。而且每改动一个索引都要监控其对性能的影响。
优选简单的数据库设计
RDBMS(关系型数据库系统)中的数据一般都宜用范式化设计。JSON或JSONB则是NoSQL风格的反范式化设计。
范式化和反范式化哪个更好呢?从业务的角度,要具体情况具体分析。从RDBMS的角度,范式化总是更简单更好,而反范式化则可以在某些情况作为补充。
建议1:考虑从DDD(领域驱动设计)的角度来设计数据模型。
- 实体总是可以建模为表,值对象总是可以嵌入保存在实体中(而有时为了性能,大型值对象也可以建模为表)。
- 某个关联的目标实体若为聚合根,就一定不能嵌入保存在别处(而要自成一表)。但如果关联的目标实体不是聚合根,并且关联的源实体是自包含的聚合根,那么目标实体就可以被嵌入保存。
建议2: 现代RDBMS中的可空列(nullable column)很高效,不用过于担心性能,如果多个可空列是对于可选属性(optional attribute)最简明的建模方式,就不要犹豫了,更别把JSONB当作对可空列的“优化”方式。
原则2: 统计信息要准确
PostgreSQL维护每一张表的统计信息,包括而不限于元组数(tuple number),页数(page number),最常见的值(most common values),柱状图界限(histogram bounds)和不同值的个数(number of distinct values, 可能相当于集的基数set cardinality)。有一些统计信息是采样得到的而不够准确。查询计划器会对查询生成多个可能的计划,根据统计信息和规则来估计成本,再选择最高效的那个计划。查询计划的质量取决于统计数据的准确性。准确的数据带来优秀的执行(这也是数据科学和数据驱动业务的一个好原则)。
正如所提到的,JSONB上的GIN的成本估计不是很准确的。而标量类型上的BTREE的成本估计则准确得多,但不是完全准备。因此JSONB不适合某些情况。为了追求效率,作为变通方法,可以对JSONB的某个标量类型属性建一个BTREE表达式索引。来自ScaleGrid的这片文章很好地介绍了怎样高效使用JSONB和GIN。
建议:PostgreSQL有一些特性,如表达式索引和部分索引都是强大而有成本效益的。只要基于数据分析认为有效益,都值得选用之。
原则3: 提高可观察性
无论我们是否对问题的潜在根因有推测,提高可观察性都是最好的做法。查询日志能证明导致慢请求的是慢查询,而不是应用程序代码或连接等待。自动EXPLAIN能捕获慢查询所用的真实的查询计划。
像Datadog这样的APM(应用程序性能管理)也是一个重要的工具。它能提供很多洞察:
- 这个问题是由资源不足所致吗?不,资源不足应平等影响任何SQL CRUD语句,但我们只观察到慢的SELECT。
- 这个问题发生于每天的同一时间吗?不,它能发生于任何时间。
每一次发生是独立事件吗?不,会在某个小的时间窗聚集发生多个事件。那时一定是发生了什么事才导致这个问题。
一些优化的措施
针对宜搭本身,一些可能有参考价值的优化措施,当然这里不涉及缓存,主要讲存储层面的
宜搭作为钉钉上低代码开发平台,上面承载着上百万应用,实例数据总共达到几十亿规模;不同的应用会生长出不同的场景业务,不同的场景业务也会衍生出很多业务组件,比如单行文本组件,成本组件等; 这些组件对应存储会有很多不同的索引去加速查询;
调整SQL语句,使得之前特定组件没走索引,充分走索引;比如下面gin语句的查询
针对一些特定组件的查询,优化了查询语句,使得充分利用索引,在数据量大时候查询更快;
正如上面案例说的,有了索引不一定快,没索引可能更快,所以需要根据查询场景控制并判断;
优化前
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------
Limit (cost=4085.57..4086.38 rows=36 width=51) (actual time=100.118..100.121 rows=1 loops=1)
-> Gather Motion 128:1 (slice1; segments: 128) (cost=4085.57..4086.38 rows=36 width=51) (actual time=100.111..100.111 rows=1 loops=1)
Merge Key: gmt_create, pid
-> Limit (cost=4085.57..4085.66 rows=1 width=51) (actual time=52.273..52.276 rows=1 loops=1)
-> Sort (cost=4085.57..4085.66 rows=1 width=51) (actual time=52.271..52.273 rows=1 loops=1)
Sort Key: gmt_create, pid
Sort Method: top-N heapsort Memory: 4224kB
-> Index Scan Backward using idx_app_type_table_name_gmt_create on yida_entity_instance a (cost=0.20..4084.66 rows=1 width=5
1) (actual time=0.361..52.258 rows=1 loops=1)
Index Cond: (((app_type)::text = 'APP_GXUUGZJ1ZPPBIJKLE9BH'::text) AND ((model_uuid)::text = 'FORM-EX866CB1E6TV7F6SZME2Y
QA5IKWO1052BFOWKK'::text))
Filter: (is_deleted = 'n'::bpchar) AND (json_data -> 'employeeField_kw0b5hyf_code'::text) = '["050323"]'::jsonb)
Planning time: 0.254 ms
(slice0) Executor memory: 180K bytes.
(slice1) Executor memory: 188K bytes avg x 128 workers, 188K bytes max (seg0). Work_mem: 33K bytes max.
Memory used: 2047000kB
Optimizer: Postgres query optimizer
Execution time: 101.799 ms
(16 rows)优化后
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------
---------
Limit (cost=5.55..6.36 rows=36 width=51) (actual time=28.342..28.344 rows=1 loops=1)
-> Gather Motion 128:1 (slice1; segments: 128) (cost=5.55..6.36 rows=36 width=51) (actual time=28.339..28.339 rows=1 loops=1)
Merge Key: gmt_create, pid
-> Limit (cost=5.55..5.64 rows=1 width=51) (actual time=5.173..5.175 rows=1 loops=1)
-> Sort (cost=5.55..5.64 rows=1 width=51) (actual time=5.171..5.173 rows=1 loops=1)
Sort Key: gmt_create, pid
Sort Method: top-N heapsort Memory: 4224kB
-> Bitmap Heap Scan on yida_entity_instance a (cost=4.51..4.64 rows=1 width=51) (actual time=5.156..5.159 rows=1 loops=1)
Recheck Cond: (json_data @> '{"employeeField_kw0b5hyf_code": ["050323"]}'::jsonb)
Filter: ((is_deleted = 'n'::bpchar) AND ((app_type)::text = 'APP_GXUUGZJ1ZPPBIJKLE9BH'::text) AND ((model_uuid)::text =
'FORM-EX866CB1E6TV7F6SZME2YQA5IKWO1052BFOWKK'::text))
-> Bitmap Index Scan on idx_json_data_path (cost=0.00..4.50 rows=1 width=0) (actual time=5.129..5.129 rows=4 loops=1)
Index Cond: (json_data @> '{"employeeField_kw0b5hyf_code": ["050323"]}'::jsonb)
Planning time: 0.345 ms
(slice0) Executor memory: 157K bytes.
(slice1) Executor memory: 495K bytes avg x 128 workers, 576K bytes max (seg31). Work_mem: 33K bytes max.
Memory used: 2047000kB
Optimizer: Postgres query optimizer
Execution time: 30.130 ms
(18 rows)这里主要调整了查询SQL
把a.json_data->'employeeField_kw0b5hyf_code'='["050323"]' 换成json_data @> '{"employeeField_kw0b5hyf_code": ["050323"]}'::jsonb查询,充分利用gin jsonb_path_ops的索引(相比gin jsob_ops索引更高效);
当然上面在数据量稍微大一点效果更明显,因为数据量太少会默认走表扫描,不走索引更快;
说明 在JSONB上创建GIN索引的方式有两种:使用默认的jsonb_ops操作符创建和使用jsonb_path_ops操作符创建。两者的区别在jsonb_ops的GIN索引中,JSONB数据中的每个key和value都是作为一个单独的索引项的,而jsonb_path_ops则只为每个value创建一个索引项。
大家还记得最上面案例二吗? 上面调整后的SQL语句,如果是在案例二场景当中,调整后会更慢,所以需要具体场景具体分析;
针对存储优化器的查询效率情况,自行选择最佳的扫描计划方式
正如上面案例说的,优化器也不一定是准确的,所以有些时候需要我们代码优化器自行选择最佳的扫描计划方式
比如说:我们查询场景中会有limit 1,limit 2,limit 5等这种查询语句,用limit来确保它在找到n个满足条件的行时就停下,而不用扫描整个表;
对于优化器来说,针对limit很小的数值,认为表扫描可能会更快,我们有些应用数据量很大,这种场景如果走表扫描,不走索引,效率会更慢(取决于扫描行数来命中);这种情况我们查询的时候,会针对性force index请求去强制走索引扫描计划,提升速度;
后续针对每个SQL的代价效率统计,也可以自动选择对应扫描计划,也算是对优化器针对业务场景不同下不同代价执行的一个补充;这个步骤可以理解叫“探查执行”,在宜搭专属大客户场景下,后续会基于这个进行“探查执行”,以达到大数据量下查询效率的最优解;
SQL Parse业务优化器,前置处理优化SQL查询
这里SQL Parse业务优化器,主要是针对我们业务上不合理待优化的SQL,算是前置拦截优化,与存储的还是有点区别;目的是优化器的补充,让存储更加专注于基于代价和成本的优化(CBO,cost based optimization)上,让优化器能更多的集中在理解计算进行执行计划优化这件事情上。
宜搭本身有很多业务功能,这些业务功能对接底层元数据引擎,来操作获取数据;业务上具体选择AST数据操作参数来调用元数据引擎统一API;
元数据底层获取到对应的AST参数,解析后组装SQL,这里针对性生成的不合理SQL进行优化
- 没有应用标识,底层会兜底上下文去取,如果没有的话,会抛出不合理的异常,拒绝不合理的SQL去查询,减少查询范围到具体应用层面;
- limit没传的,组装SQL会默认给个值;
- 针对select * 查询语法,默认解析成select字段
- 以及一些函数或者表达式的变换,比如日期函数大于等于值,命中不了索引,SQL优化;
- 数值介于查询,明显命中不了数据的,直接判断拦截;
- 一些不合理多条件查询,合并
- ...
最近团队有一些hc,对元数据或者低代码平台感兴趣的来试试;java研发,数据研发,技术专家等都可,欢迎来撩
简历发 edagarli.lz@alibaba-inc.com 或者微信私我hangzhoushoot
参考
Understanding Postgres GIN Indexes: The Good and the Bad
Postgres Planner not using GIN index Occasionally
Gitlab once faced a GIN related issue
Understanding Postgres query planner behaviour on GIN index
Statistics used by the query planner
When To Avoid JSONB In A PostgreSQL Schema
Using JSONB in PostgreSQL: How to Effectively Store & Index JSON Data in PostgreSQL
https://zhuanlan.zhihu.com/p/523900025
https://www.cnblogs.com/flying-tiger/p/6702796.html
https://cloud.tencent.com/developer/article/1943819
文中关于mysql的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《索引选择度问题优化整理》文章吧,也可关注golang学习网公众号了解相关技术文章。
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
261 收藏
-
455 收藏
-
381 收藏
-
336 收藏
-
152 收藏
-
404 收藏
-
339 收藏
-
429 收藏
-
159 收藏
-
数据库 · MySQL | 5天前 | 性能优化 · 执行计划 · MySQL教程 · 慢查询治理 · 数据库运维 · mysql GROUP BY优化 TempTable 内部临时表 Created_tmp_disk_tables267 收藏
-
数据库 · MySQL | 5天前 | 性能优化 · InnoDB · MySQL教程 · 数据库运维 · 高并发写入 · mysql innodb 批量写入 Change Buffer innodb_change_buffering270 收藏
-
数据库 · MySQL | 1星期前 | 性能优化 · 高并发 · InnoDB · MySQL教程 · 数据库运维 · mysql innodb AUTO_INCREMENT 高并发写入 innodb_autoinc_lock_mode254 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习
-
- 聪明的眼睛
- 这篇技术文章真是及时雨啊,太细致了,受益颇多,mark,关注大佬了!希望大佬能多写数据库相关的文章。
- 2023-03-23 15:14:06
-
- 单身的高跟鞋
- 这篇技术贴真及时,大佬加油!
- 2023-03-17 15:17:12
-
- 聪慧的飞机
- 写的不错,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢老哥分享文章内容!
- 2023-03-04 01:14:29
-
- 怕黑的蜗牛
- 太全面了,已收藏,感谢作者大大的这篇博文,我会继续支持!
- 2023-01-27 20:20:49
-
- 刻苦的绿茶
- 很好,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢博主分享文章内容!
- 2023-01-21 21:26:56
-
- 激动的老师
- 这篇文章内容真及时,太全面了,真优秀,码起来,关注楼主了!希望楼主能多写数据库相关的文章。
- 2023-01-14 06:48:47