• Oracle收集对表收集统计信息导致全表扫描直接路径读?


     

    direct path read深入解析

    前言

    最近碰到一件很奇葩的事情,因为某条SQL执行缓慢,原因是走了笛卡尔(两组大数据结果集),而且笛卡尔还是NL的一个部分,要循环31M次。

    很容易发现是统计信息没有导致的,收集完就好了。

    结果过了个51假期回来有人反馈其他的SQL慢了,看了下慢的SQL涉及到的表就刚好是上次收集统计信息的表。

    统计问题SQL的历史执行情况,发现时间刚好在那天收集完后开始变慢。咋回事。

    后边发现慢的原因是对收集的那两张表做全表扫描时候用了直接路径读(DPR)。暂定为表A和表B。SQL的语法结构是对A,B重复做2次left join,所以慢的SQL中A,B表会被扫描各3次,全部都是DPR。

     ???收集统计信息会导致DPR???

    事件概要

    对事情做一个简述,不展开来说,引出研究和得到的结论就行。

    1. SQL变慢,发现时间刚好是收集相关表的统计信息之后变慢的
    2. 收集cursor_awr('&sql_id')可以得到执行计划变过的
    3. 使用hint回退原来的执行计划,还是慢,综合对比无论是新旧执行计划,都走了全表扫描(DPR),statistics_level=all收集详细的执行情况也是发现主要时间花在DRP上(很奇怪就是以前应该也慢才对,毕竟表A、B都很大,而且各3次全表扫)
    4. ashrpti、awrsqrpt相关sql的信息,发现快的时候物理读远远小于慢的时候的物理读
    5. 通过session级别禁用DPR之后,反而更慢了。产生gc类等待,以及"db file sequential read"和"db file scattered read"等。

    因此基本可以确认全表扫描导致DPR变慢的,而导致DPR的原因应该就是收集了表的统计信息。

    研究过程

    首先百度搜索和mos上都找遍了,没有类似的案例。

    关于DPR的很容易可以搜索到一些关键的信息,这里自行搜索就行(有些文章的结论就并不完全正确)。

    我们从DPR的产生条件出发引出实验内容。

    第一,大表是DPR的前置条件,那么什么样的表才算是大表呢?

    Oracle有个隐含参数控制的这个阀值,'_small_table_threshold',单位是块,简称为STT。

    第二,超过表的VLOT的值,无论是否设置10949,均为发生DPR。(_serial_direct_read的关闭能否阻止尚未验证)。

    第三,表已经被缓存在buffer cache中的块占比,超过表50%的话,不会发生DPR。

    第四,表的脏块率,超过25%的脏块也不会发生DPR。

    为了研究方便,使用sys用户创建视图查询隐含参数。

    create or replace view h$parameter
    as
    select a.ksppinm name,
    a.ksppdesc description,
    b.ksppstvl session_value,
    c.ksppstvl system_value
    from x$ksppi a,x$ksppcv b,x$ksppsv c
    where a.indx=b.indx and a.indx=c.indx;

    在我的环境下(11.2.0.4.0),查询的结果如下:

    set line 500
    col name for a50
    col session_value for a50
    col system_value for a50
    select name,session_value,system_value from h$parameter where name in 
    ('_db_block_buffers','_very_large_object_threshold','_small_table_threshold','_direct_read_decision_statistics_driven','_statistics_based_srf_enabled');
    
    
    NAME                                               SESSION_VALUE                                      SYSTEM_VALUE
    -------------------------------------------------- -------------------------------------------------- --------------------------------------------------
    _db_block_buffers                                  32830                                              32830
    _small_table_threshold                             656                                                656
    _very_large_object_threshold                       500                                                500
    _direct_read_decision_statistics_driven            TRUE                                               TRUE
    _statistics_based_srf_enabled                      TRUE                                               TRUE

    这里关于STT以及VLOT和MTT的计算方法,有个公式

    STT = _small_table_threshold
    MTT = 5 * _small_table_threshold
    VLOT = (_very_large_object_threshold/100) * _db_block_buffers

    算得的几个具体的数值:

    STT = 656

    MTT = 3280

    VLOT = 164150

    那么我们可以构造一张表,循环对表insert一波数据,然后全表扫描看看是否发生DPR,如果发生DPR了,打印出插入了多少行数据。如果没发生DPR,那么truncate表,重新insert知道满足DPR为止。

    先构造一张表T:

    create table t (id int,name varchar2(2000)) tablespace test;

    然后用以下这么个函数实现,函数改造自某个大神,使用sys用户创建(后边的实验都用sys用户)

    CREATE OR REPLACE FUNCTION GET_ADR_TRSH(P_STEP  IN NUMBER DEFAULT 1,
                                            P_START IN NUMBER DEFAULT 0,
                                            P_STOP  IN NUMBER DEFAULT NULL)
      RETURN NUMBER IS
      L_PRD    NUMBER;
      L_CNT    NUMBER;
      L_BLOCKS NUMBER := 0;
      L_START  NUMBER := P_START;
    BEGIN
      EXECUTE IMMEDIATE 'truncate table t';
      LOOP
        INSERT
        INTO T
          SELECT level,rpad('a',1800,'+')
            FROM DUAL
          CONNECT BY LEVEL <= P_STEP + L_START;
        COMMIT;
        L_BLOCKS := L_BLOCKS + P_STEP + L_START;
        L_START  := 0;
        --EXECUTE IMMEDIATE 'analyze table t delete statistics';
        --EXECUTE IMMEDIATE 'analyze table t compute statistics';
        EXECUTE IMMEDIATE 'alter system flush buffer_cache';
        SELECT /*+ full(t) */
         COUNT(*)
          INTO L_CNT
          FROM T;
        SELECT VALUE
          INTO L_PRD
          FROM V$SEGMENT_STATISTICS
         WHERE OWNER = USER
           AND OBJECT_NAME = 'T'
           AND STATISTIC_NAME = 'physical reads direct';
        EXIT WHEN(L_PRD > 0);
      END LOOP;
      RETURN L_BLOCKS;
    END;
    /

    说明一下:因为我的表结构是name字段2000字节,而块大小为8192字节,因此这个insert的数据每个块刚好只能容纳4行。

    再运行以下语句,会得到插入多少行发生DPR的结果。

    set serveroutput on
    DECLARE
      L_TRSH NUMBER;
    BEGIN
      L_TRSH := GET_ADR_TRSH();
      DBMS_OUTPUT.PUT_LINE(L_TRSH);
    END;
    /

     我的环境跑的结果如下:

    SYS@zkm> set serveroutput on
    SYS@zkm> DECLARE
      2    L_TRSH NUMBER;
      3  BEGIN
      4    L_TRSH := GET_ADR_TRSH();
      5    DBMS_OUTPUT.PUT_LINE(L_TRSH);
      6  END;
      7  /
    2489
    
    PL/SQL procedure successfully completed.
    
    SYS@zkm> select count(*) from t;
    
      COUNT(*)
    ----------
          2489

    表示我insert了2489行数据后,全表扫描的话会产生DPR。

    SYS@zkm> select dbms_rowid.rowid_relative_fno(rowid) file#,dbms_rowid.rowid_block_number(rowid) block#,count(*) from t group by dbms_rowid.rowid_relative_fno(rowid),dbms_rowid.rowid_block_number(rowid) order by 2;
    
         FILE#     BLOCK#   COUNT(*)
    ---------- ---------- ----------
             6        131          4
    ...省略大部分内容...
             6        766          4
             6        767          4
             6        851          1
    
    623 rows selected.

    总共623行,代表T表有623个快,由于在第623个块产生已经满足DPR了,因此第623个块insert了一行数据就停止了。

    那么,这个和我们前边的大表阀值STT=656有区别,这是怎么回事?

    我们知道,表在没有统计信息的时候,dba_tables.blocks这个字段是空的,那么全表扫描的时候,不知道表的块数(姑且用Object_Size表示)怎么和STT=656做比较来确定是否DPR呢?

    结合我前言中遇到的问题,我猜测是用的dba_segments.blocks这个值(收集统计信息后走了DPR嘛)。后来找到一篇文章,里边的大佬也是说用的dba_segments.blocks。

    但是我在做实验的时候其实是不对的。准确的说,根据我的实验结果,Object_Size应该使用的是L3段头块中"Extent Control Header"下边的#blocks below处的值。

    我们接着上边的实验往下验证。

    首先确定L3段头块的信息:

    SYS@zkm> select HEADER_FILE,HEADER_BLOCK from dba_segments where owner='SYS' and segment_name='T'; 
    
    HEADER_FILE HEADER_BLOCK
    ----------- ------------
              6          130

    查看dba_segments.blocks这个值和L3里边"Extent Control Header"下边的#blocks below处的值,我们来做个比较:

    SYS@zkm> select blocks from dba_segments where owner='SYS' and segment_name='T';
    
        BLOCKS
    ----------
           768
    SYS@zkm> select value from v$diag_info where name like '%De%';
    
    VALUE
    --------------------------------------------------------------------------------
    /u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8267.trc
    
    SYS@zkm> alter system dump datafile 6 block 130;
    
    System altered.
    
    SYS@zkm> !more /u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8267.trc
    ...省略部分内容...
      Extent Control Header
      -----------------------------------------------------------------
      Extent Header:: spare1: 0      spare2: 0      #extents: 21     #blocks: 768   
                      last map  0x00000000  #maps: 0      offset: 2716  
          Highwater::  0x01800380  ext#: 20     blk#: 128    ext size: 128   
      #blocks in seg. hdr's freelists: 0     
      #blocks below: 748   
      mapblk  0x00000000  offset: 20    
                       Unlocked
      --------------------------------------------------------
      Low HighWater Mark : 
          Highwater::  0x01800300  ext#: 19     blk#: 128    ext size: 128   
      #blocks in seg. hdr's freelists: 0     
      #blocks below: 640   
      mapblk  0x00000000  offset: 19    
      Level 1 BMB for High HWM block: 0x01800301
      Level 1 BMB for Low HWM block: 0x01800281
      --------------------------------------------------------
      Segment Type: 1 nl2: 1      blksz: 8192   fbsz: 0      
      L2 Array start offset:  0x00001434
      First Level 3 BMB:  0x00000000
      L2 Hint for inserts:  0x01800081
      Last Level 1 BMB:  0x01800301
      Last Level II BMB:  0x01800081
      Last Level III BMB:  0x00000000
         Map Header:: next  0x00000000  #extents: 21   obj#: 96513  flag: 0x10000000
      Inc # 0 
      Extent Map
      -----------------------------------------------------------------
       0x01800080  length: 8     
       0x01800088  length: 8     
       0x01800090  length: 8     
       0x01800098  length: 8     
       0x018000a0  length: 8     
       0x018000a8  length: 8     
       0x018000b0  length: 8     
       0x018000b8  length: 8     
       0x018000c0  length: 8     
       0x018000c8  length: 8     
       0x018000d0  length: 8     
       0x018000d8  length: 8     
       0x018000e0  length: 8     
       0x018000e8  length: 8     
       0x018000f0  length: 8     
       0x018000f8  length: 8     
       0x01800100  length: 128   
       0x01800180  length: 128   
       0x01800200  length: 128   
       0x01800280  length: 128   
       0x01800300  length: 128   
      
      Auxillary Map
      --------------------------------------------------------
       Extent 0     :  L1 dba:  0x01800080 Data dba:  0x01800083
       Extent 1     :  L1 dba:  0x01800080 Data dba:  0x01800088
       Extent 2     :  L1 dba:  0x01800090 Data dba:  0x01800091
       Extent 3     :  L1 dba:  0x01800090 Data dba:  0x01800098
       Extent 4     :  L1 dba:  0x018000a0 Data dba:  0x018000a1
       Extent 5     :  L1 dba:  0x018000a0 Data dba:  0x018000a8
       Extent 6     :  L1 dba:  0x018000b0 Data dba:  0x018000b1
       Extent 7     :  L1 dba:  0x018000b0 Data dba:  0x018000b8
       Extent 8     :  L1 dba:  0x018000c0 Data dba:  0x018000c1
       Extent 9     :  L1 dba:  0x018000c0 Data dba:  0x018000c8
       Extent 10    :  L1 dba:  0x018000d0 Data dba:  0x018000d1
       Extent 11    :  L1 dba:  0x018000d0 Data dba:  0x018000d8
       Extent 12    :  L1 dba:  0x018000e0 Data dba:  0x018000e1
       Extent 13    :  L1 dba:  0x018000e0 Data dba:  0x018000e8
       Extent 14    :  L1 dba:  0x018000f0 Data dba:  0x018000f1
       Extent 15    :  L1 dba:  0x018000f0 Data dba:  0x018000f8
       Extent 16    :  L1 dba:  0x01800100 Data dba:  0x01800102
       Extent 17    :  L1 dba:  0x01800180 Data dba:  0x01800182
       Extent 18    :  L1 dba:  0x01800200 Data dba:  0x01800202
       Extent 19    :  L1 dba:  0x01800280 Data dba:  0x01800282
       Extent 20    :  L1 dba:  0x01800300 Data dba:  0x01800302
      --------------------------------------------------------
      
       Second Level Bitmap block DBAs 
       --------------------------------------------------------
       DBA 1:   0x01800081
      
    End dump data blocks tsn: 7 file#: 6 minblk 130 maxblk 130

    可以看出dba_segments.blocks为768,而"Extent Control Header"下边的#blocks below处的值为748。

    那么其实Object_Size的值是等于748的。

    前边提到的T表有623个块意义在当前就显得不是特别重要的,因为Oracle的判断基准以Object_size=748,并不是实际上的623个块。

    如何验证Object_size的值是否准确呢?新开一个session(就叫s1会话吧),作如下操作:

    SYS@zkm> select value from v$diag_info where name like '%De%';
    
    VALUE
    --------------------------------------------------------------------------------
    /u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8363.trc
    
    SYS@zkm> alter system flush buffer_cache;
    
    System altered.
    
    SYS@zkm> /
    
    System altered.
    
    SYS@zkm> /
    
    System altered.
    
    SYS@zkm> alter session set events 'trace[NSMTIO] disk=medium';
    
    Session altered.
    
    SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T;
    
      COUNT(*)
    ----------
          2489

    查看trc文件内容:

    NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
    NSMTIO: kcbdpc:DirectRead: tsn: 7, objd: 96513, objn: 96511
    ckpt: 1, nblks: 748, ntcache: 1, ntdist:0
    NSMTIO: Additional Info: VLOT=164150
    Object# = 96513, Object_Size = 748 blocks
    SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

    可以直接看到:

    Object_Size = 748 blocks

    VLOT=164150

    这两个值都和前边得到的结果一致。

    从其他内容看,我们可以得到判断条件是

    当满足[MTT < OBJECT_SIZE < VLOT]的时候,会检查cost(比如cache率,前边提到的第三),本地还是远程之类的等。

    实际上,MTT的值为3280不可能<Object_size,MTT在11gR2已经废弃。

    大量实验可以得出结论:可以看到在11.2版本下,小于SST不走DirectRead,大于VLOT一定走DirectRead,介于SST和VLOT的表,需要考虑是否为本地表或者远程表

    然后这里最后走了DPR了(DirectRead)。

    其他信息:

    object_id=96511,data_object_id=96513

    ntcache: 1表示缓存了一个块

    值得一提的是,如果T表存在统计信息,也就是dba_tables.blocks有值的话,Oracle会直接使用该值作为Object_size的值。我们验证下,

    新开一个session,收集统计信息之后。

    SYS@zkm> analyze table t compute statistics;
    
    Table analyzed.
    
    SYS@zkm> select blocks from dba_tables where owner='SYS' and table_name='T' union all select blocks from dba_segments where owner='SYS' and segment_name='T'; 
    
        BLOCKS
    ----------
           748          --dba_tables.blocks
           768          --dba_segments.blocks

    刚好和原来一样,不过你可以尝试truncate表T, 他的值还是用748,但其实为0了。这里实验过程不写出来了。

    这个其实可以验证收集统计信息很有可能导致DPR的发生了。

    接下来我们验证下前边第三点:

    第三,表已经被缓存在buffer cache中的块占比,超过表50%的话,不会发生DPR。

    首先,删除统计信息(后边说明原因)

    然后用如下语句查询当前表被缓存在buffer cache的块的数量。

    SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;
    
       INST_ID SUM(NUM_BUF)
    ---------- ------------
             1            5
    
    SYS@zkm> analyze table t delete statistics;
    
    Table analyzed.

    表示实例1中,表T有5个块被缓存。

    由于Object_Size = 748,一半为374。因此想办法将缓存的块变为373,看是否发生DPR,然后在缓存多一个块,变为374,看是否发生DPR。

    用SELECT /*+ full(t) */ COUNT(*) FROM T where rownum<=xxxx;

    由于1个块包含4行,其中373块实际上还要减掉L3号块,因此读取372个块即可,共1488行。但实际上查询的结果有问题,我之前一篇文章有研究过全表扫描下用rownum后如何缓存块,还是测试不够充分。

    不过这里暂不做研究,最终我调试出来的rownum可以为1472。

    开多另外一个session,将多块读设置为1,禁用动态采样,在这个session级别上禁用DPR。

    直接复制的:
    set
    arraysize 5000 alter system flush buffer_cache; alter session set "_serial_direct_read"=NEVER; alter session set db_file_multiblock_read_count=1; SELECT /*+ full(t) dynamic_sampling(t 0) */ COUNT(*) FROM T where rownum<=1476; select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;
    SYS@zkm> set arraysize 5000
    SYS@zkm> alter system flush buffer_cache;
    
    System altered.
    
    SYS@zkm> alter session set "_serial_direct_read"=NEVER;
    
    Session altered.
    
    SYS@zkm> alter session set db_file_multiblock_read_count=1;
    
    Session altered.
    
    SYS@zkm> SELECT /*+ full(t) dynamic_sampling(t 0) */ COUNT(*) FROM T where rownum<=1472;
    
      COUNT(*)
    ----------
          1472
    
    SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;
    
       INST_ID SUM(NUM_BUF)
    ---------- ------------
             1          373

    回到s1会话,执行:

    SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T;
    
      COUNT(*)
    ----------
          2489
    
    SYS@zkm> 
    SYS@zkm> 
    SYS@zkm> 
    SYS@zkm> /
    
      COUNT(*)
    ----------
          2489
    
    trc多出来的信息:
    
    *** 2020-05-12 11:10:11.356
    NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
    NSMTIO: kcbdpc:DirectRead: tsn: 7, objd: 96513, objn: 96511
    ckpt: 1, nblks: 748, ntcache: 373, ntdist:0
    NSMTIO: Additional Info: VLOT=164150
    Object# = 96513, Object_Size = 748 blocks
    SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

    其中ntcache=373刚好就是我们控制的缓存373。

    这里也只走了DPR,那么我们多缓存一个块,是否就不会用DPR了呢?

    SYS@zkm> set arraysize 5000
    SYS@zkm> alter system flush buffer_cache;
    
    System altered.
    
    SYS@zkm> alter session set "_serial_direct_read"=NEVER;
    
    Session altered.
    
    SYS@zkm> alter session set db_file_multiblock_read_count=1;
    
    Session altered.
    
    SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T where rownum<=1476;
    
      COUNT(*)
    ----------
          1476
    
    SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;
    
       INST_ID SUM(NUM_BUF)
    ---------- ------------
             1          374

    回到s1,

    SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T;
    
      COUNT(*)
    ----------
          2489
    
    trc多出来的信息:
    
    *** 2020-05-12 11:11:28.249
    NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
    NSMTIO: kcbdpc:NoDirectRead:[CACHE_READ]: tsn: 7, objd: 96513, objn: 96511
    ckpt: 0, nblks: 748, ntcache: 374, ntdist:0
    NSMTIO: Additional Info: VLOT=164150
    Object# = 96513, Object_Size = 748 blocks
    SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

    当ntcache为Object_size的一半的时候,即表T有一半的块缓存在buffer cache中时候,不会做DPR。

    奇怪的是,如果你收集了统计信息,无论cache率多高,都只会做直接路径读。这点尚未搞明白为什么。

    这个时候,如果禁用参数"_statistics_based_srf_enabled"=false的话,

     再次s1执行观察trc的话,输出会变为如下显示:

    *** 2020-05-12 22:37:50.181
    NSMTIO: qertbFetch:NoDirectRead:[- STT < OBJECT_SIZE < MTT]:Obect's size: 748 (blocks), Threshold: MTT(3283 blocks),
    _object_statistics: enabled, Sage: disabled, 
    Direct Read for serial qry: enabled(::::::), Ascending SCN table scan: FALSE 
    flashback_table_scan: FALSE, Row Versions Query: FALSE
    SqlId: 4x7n0h69zwnuk, plan_hash_value: 2966233522, Object#: 96513, Parition#: 0

    这个时候的Object_size确实就是<MTT的值了。再次设置为true的话,判断cache率有变正常了。

    关于这个隐含参数,网络和mos几乎没有资料介绍。

    根据实验和网上资料我们可以总结,

    1.统计信息的有无只会影响到Object_size,当然可以通过"_direct_read_decision_statistics_driven"控制是否使用统计信息来影响DPR

    2.当Object_size < STT的话,一定不走DPR

    3.当Object_size > VLOT的话,一定走DPR(未排除参数_serial_direct_read是否影响)

    4.当Object_size介于MTT和VLOT之间的时候,会使用成本计算(Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp))去判断是否DPR。

      其中,MTT<Object_size实际上不准确(因此才说11gR2MTT是废弃的)。由于要使用成本代价,因此可以认为STT < Object_size < VLOT的时候,使用成本计算。比如包括缓存率的成本等。

    5."_direct_read_decision_statistics_driven"默认为true,当设置为false,从观察到的结果上看,MTT被启用了,从上边实验看原因是因为Oracle直接判断- STT < OBJECT_SIZE < MTT了,这个时候无法判断是否成本计算被禁用了。毕竟不再有"MTT < OBJECT_SIZE < VLOT".有时间可以构造出这么一个条件出来观察看看。

    参考

    http://www.itpub.net/thread-1815281-1-1.html

    https://www.talkwithtrend.com/Article/218485

    https://www.modb.pro/db/25067

    https://blogs.oracle.com/smartscan-deep-dive/revisiting-buffer-cache-very-large-object-threshold-vlot

  • 相关阅读:
    奢想下财务自由吧--理想生活啊
    [翻译] AGG 之贝塞尔插值
    Beginning Silverlight 4 in C#Welcome to Silverlight 4[学习笔记]
    从属性赋值到MVVM模式详解
    Beginning Silverlight 4 in C#Silverlight的布局管理学习笔记
    Beginning Silverlight 4 in C#Silverlight工具包
    不用IDE写C#的Hello World
    NHibernate使用无状态Sessions
    Beginning Silverlight 4 in C#Silverlight控件
    NHibernate使用session.Merge[翻译]
  • 原文地址:https://www.cnblogs.com/PiscesCanon/p/12872004.html
Copyright © 2020-2023  润新知