如何给Large Delete操作提速近千倍?(一)

博客围绕从大表清理过时数据的任务展开,介绍了批量绑定(Bulk Binding)知识。阐述了其概念、优点,说明了输入集合用FORALL改善DML操作性能,输出集合用BULK COLLECT提高查询性能,还通过实例对比FOR.. LOOP和FORALL效率,展示了批量绑定在优化数据操作上的显著效果。

本文已经发表在ITPUB优化技术丛书,未经许可,不得转载。

1.   背景描述

1.1. 任务描述

这个任务是需要从一系列大表中清理3个省的大批过时数据,具体的清理过程简单的说就是:

首先根据不同的miscid的值创建不同的临时表,类似于:

 

CREATE TABLE temp_mid

AS

SELECT mid FROM ssr WHERE SUBSTR(ssid,1,7) IN

(SELECT prefixnum FROM prefix WHERE mcid='0012');

 

      

然后通过这个临时表连接另一个大表,做删除工作:

DELETE SSF

      WHERE mid IN (SELECT mid

                      FROM TEMP_MID_HUBEI);

 

 

上述任务根据不同的关键字,需要执行几十次,如果不加任何优化的话,每一次都需要执行几十个小时。由于需求、操作和优化思路大体相同,下面我们就以上面的例子详细说说实际应用中如何一步步优化提速到近千倍的过程。

 

 

1.2. 数量级统计和描述

首先统计这个操作涉及到的几张表:

 

SELECT COUNT(*) FROM PREFIX;

 

SELECT COUNT(*) SSR FROM SSR;

 

SELECT COUNT(*) SSF FROM SSF;

 

SELECT COUNT(*) AS SSF_0012 FROM SSF WHERE MID IN (SELECT MID FROM TEMP_MID_HUBEI);

 

 

上述脚本的执行过程如下(请注意,由于创建临时表 TEMP_MID_HUBEI 的过程比较简单,因此这里没有赘述,仅仅是从建立临时表后的删除操作开始分析的):

 

SQL> @LUNAR.SQL

 

PREFIX

----------

     51854

 

ELAPSED: 00:00:00.14

 

SSF

-----------

   83446270

 

ELAPSED: 00:04:53.27

 

SSR

----------

  43466645

 

ELAPSED: 00:03:08.00

 

SSF_0012

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

          131098

 

ELAPSED: 00:00:57.02

SQL>

 

 

我们注意到,这里面,我们需要做的是从一个8300多万行的大表中,通过和一个130多万行的表进行连接并删除其中的大部分数据。整个操作的过程,要求所有的表都可以实时访问,并且除了我们手工建立的临时表(TEMP_MID_HUBEI)以外,其他的表都可以实时访问和修改。

 

 

 

2.   背景知识——Bulk Binding

在下面的优化过程中,我们使用了批量绑定(Bulk Binding)的思想,因此首先对这一知识作些解释。

 

 

2.1. 什么是Bulk Binding

sql语句中(动态地)给PL/SQL变量赋值叫做绑定(Binding)。一次绑定一个完整的集合叫做批量绑定(Bulk Binding)。

Oracle 8i开始,在PL/SQL可以使用两个新的数据操纵语言(DML)语句:BULK COLLECTFORALL。这两个语句在PL/SQL内部按数组进行数据处理。

 

 

2.2. Bulk binds的优点是什么呢?

       批量绑定(Bulk binds)通过最小化在PL/SQLSQL引擎之间的上下文切换提高了性能,它以一个完整的集合(如,varray, nested tables, index-by table, or host array)为单位(一批一批的)向前或者向后绑定变量。在Oracle 8i以前,每个SQL语句的执行需要在PL/SQLSQL引擎之前切换上下文,使用绑定变量后,就只需要一次上下文切换。

其中,BULK COLLECT提供对数据的高速检索,FORALL可大大改进INSERTUPDATEDELETE操作的性能。

 

 

2.3. 如何进行批量绑定(Bulk Binds)?

绑定变量包扩下面两个部分:

1)        输入集合(collections),使用FORALL语句,一般用来改善DMLINSERTUPDATEDELETE) 操作的性能;

2)        输出集合(collections),使用BULK COLLECT子句;一般用来提高查询(SELECT)的性能。

 

2.3.1.    输入集合(FORALL)

输入集合是数据通过PL/SQL引擎到SQL引擎去执行INSERT, UPDATE, DELETE语句。输入集合使用FORALL语句,下面是FORALL的语法:

 

FORALL index IN lower_bound..upper_bound

sql_statement;

 

2.3.2.    FOR.. LOOP语句和FORALL的比较

1example1):分别使用传统的FOR .. LOOP操作和我们这里介绍的FORALL 操作 lunartest表中加载1000000条记录,对比一下他们的执行效率。

 

测试过程如下:

首先创建一个用来记录操作时间的存储过程 get_time

CREATE OR REPLACE PROCEDURE get_time (t OUT NUMBER)

IS

BEGIN

   SELECT TO_CHAR (SYSDATE, 'SSSSS')

     INTO t

     FROM DUAL;

END;

 

 

然后创建一个空表,分别使用FOR .. LOOPFORALL .. LOOP插入数据,并记录和输出操作时间:

SQL> conn lunar/lunar

Connected.

SQL> SET SERVEROUTPUT ON

SQL> CREATE TABLE lunartest (pnum NUMBER(20), pname varchar2(50));

 

Table created.

 

Elapsed: 00:00:00.00

SQL> Create Or Replace PROCEDURE BulkTest IS

  2     TYPE NumTab IS TABLE OF NUMBER(20) INDEX BY BINARY_INTEGER;

  3     TYPE NameTab IS TABLE OF varchar2(50) INDEX BY BINARY_INTEGER;

  4     pnums NumTab;

  5     pnames NameTab;

  6     t1 CHAR(5);

  7     t2 CHAR(5);

  8     t3 CHAR(5);

  9  BEGIN

 10     FOR j IN 1..1000000 LOOP  -- load index-by tables

 11        pnums(j) := j;

 12        pnames(j) := 'Part No. ' || TO_CHAR(j);

 13     END LOOP;

 14

 15     get_time(t1);

 16

 17     FOR i IN 1..1000000 LOOP -- use FOR loop

 18        INSERT INTO lunartest VALUES (pnums(i), pnames(i));

 19     END LOOP;

 20

 21     get_time(t2);

 22

 23     FORALL i IN 1..1000000  --use FORALL statement

 24        INSERT INTO lunartest VALUES (pnums(i), pnames(i));

 25     get_time(t3);

 26

 27     DBMS_OUTPUT.PUT_LINE('Execution Time (secs)');

 28     DBMS_OUTPUT.PUT_LINE('---------------------');

 29     DBMS_OUTPUT.PUT_LINE('FOR loop: ' || TO_CHAR(t2 - t1));

 30     DBMS_OUTPUT.PUT_LINE('FORALL:   ' || TO_CHAR(t3 - t2));

 31  END;

 32  /

 

Procedure created.

 

Elapsed: 00:00:00.00

SQL> exec BulkTest;

Execution Time (secs)

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

FOR loop: 110

FORALL:   54

 

PL/SQL procedure successfully completed.

 

Elapsed: 00:03:24.07

SQL> select sum(bytes/1024/1024) MB from user_segments where segment_name='LUNARTEST';

 

        MB

----------

        57

 

Elapsed: 00:00:11.06

SQL>

 

 

 

 

这里我们注意到使用FOR .. LOOP语句插入1000000条记录,需要110秒;而使用FORALL语句只需要54秒。

 

2.3.3.    如何处理回滚?

对于回滚的处理,FORALL操作可以自动完成,也就是说,如果一个FORALL 语句执行失败,那么Oracle会基于隐式的SAVE POINT一次回滚SQL语句中先前执行的部分。

 

2.3.4.    输出集合s

输出集合是数据作为一个通过SQL引擎到PL/SQL引擎的(SELECT 或者 FETCH的)结果集。

输出集合通过在SELECT INTO, FETCHINTORETURNING INTO子句中加入BULK COLLECT子句实现,下面是BULK COLLECT子句的语法:

 

... BULK COLLECT INTO collection_name[, collection_name] ....

 

 

2.3.5.    SELECT INTO中使用BULK COLLECT

这里我们结合一个实例来理解一下在SELECT INTO 语句中批量绑定的使用:

SQL> conn lunar/lunar

Connected.

SQL> SET SERVEROUTPUT ON

SQL> Create Or Replace Procedure lunartest2 Is

  2     TYPE NumTab IS TABLE OF emp.empno%TYPE;

  3     TYPE NameTab IS TABLE OF emp.ename%TYPE;

  4     enums NumTab; -- no need to initialize

  5     names NameTab;

  6  BEGIN

  7     SELECT empno, ename BULK COLLECT INTO enums, names FROM emp;

  8     FOR i in enums.FIRST..enums.LAST LOOP

  9        DBMS_OUTPUT.PUT_LINE(enums(i) || ' ' || names(i));

 10     END LOOP;

 11  END;

 12  /

 

Procedure created.

 

Elapsed: 00:00:00.08

SQL>

 

 

这里我们看到,使用SELECT .. BULK COLLECT INTO的方法和传统的SELECT .. INTO的语法基本上变化不大。现在我们看一下执行结果:

 

SQL>  select empno,ename from emp;

 

     EMPNO ENAME

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

      7369 SMITH

      7499 ALLEN

      7521 WARD

      7566 JONES

      7654 MARTIN

      7698 BLAKE

      7782 CLARK

      7788 SCOTT

      7839 KING

      7844 TURNER

      7876 ADAMS

      7900 JAMES

      7902 FORD

      7934 MILLER

 

14 rows selected.

 

Elapsed: 00:00:00.00

SQL> exec lunartest2;

7369 SMITH

7499 ALLEN

7521 WARD

7566 JONES

7654 MARTIN

7698 BLAKE

7782 CLARK

7788 SCOTT

7839 KING

7844 TURNER

7876 ADAMS

7900 JAMES

7902 FORD

7934 MILLER

 

PL/SQL procedure successfully completed.

 

Elapsed: 00:00:00.00

SQL>

 

      

       如果我们这里需要输出的记录不是14条,而是140万条甚至更多,那么BULK COLLECT的就会发挥出强悍的优势,这里我们仅仅是在功能上得到验证。

 

 

2.3.6.    FETCH INTO中使用BULK COLLECT

现在,我们来看看在FETCH INTO 语句中如何使用批量绑定:

SQL> conn lunar/lunar

Connected.

SQL> SET SERVEROUTPUT ON

SQL> Create Or Replace Procedure lunartest3 Is

  2     TYPE NameTab IS TABLE OF emp.ename%TYPE;

  3     TYPE SalTab IS TABLE OF emp.sal%TYPE;

  4     names NameTab;

  5     sals SalTab;

  6     CURSOR c1 IS SELECT ename, sal FROM emp;

  7  BEGIN

  8     OPEN c1;

  9     FETCH c1 BULK COLLECT INTO names, sals;

 10     FOR i IN names.FIRST..names.LAST LOOP

 11        DBMS_OUTPUT.PUT_LINE(names(i) || ' ' || sals(i));

 12     END LOOP;

 13     CLOSE c1;

 14  END;

 15  /

 

Procedure created.

 

Elapsed: 00:00:00.01

SQL> select ename,sal from emp;

 

ENAME             SAL

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

SMITH             800

ALLEN            1600

WARD             1250

JONES            2975

MARTIN           1250

BLAKE            2850

CLARK            2450

SCOTT            3000

KING             5000

TURNER           1500

ADAMS            1100

JAMES             950

FORD             3000

MILLER           1300

 

14 rows selected.

 

Elapsed: 00:00:00.00

SQL> exec lunartest3;

SMITH 800

ALLEN 1600

WARD 1250

JONES 2975

MARTIN 1250

BLAKE 2850

CLARK 2450

SCOTT 3000

KING 5000

TURNER 1500

ADAMS 1100

JAMES 950

FORD 3000

MILLER 1300

 

PL/SQL procedure successfully completed.

 

Elapsed: 00:00:00.00

SQL>

 

 

 

这里有一点需要特比注意,bulk-tech不能从游标(cursor)插入到一个记录的集合。

 

 

2.3.7.    使用新的游标(cursor)属性实现BULK COLLECT

Oracle 9i开始,Oradcle又提供了新的bulk binds游标属性,即%BULK_ROWCOUNT,其语法为:

 

IF SQL%BULK_ROWCOUNT(i) = ... THEN

……

ENDIF;

 

 

该游标属性的使用方法和含义与传统的SQL% ROWCOUNT基本相同。

       下面我们就结合一个实例,了解forall的强大作用和使用的注意事项。

 

 

<think>我们正在解决的是Python中datasets包没有LargeList类型的问题。根据引用[3],字典(或类似数据结构)在有序性和内存占用方面可能存在局限,但我们的问题集中在特定数据类型LargeList上。 由于引用[1]中展示的数据集结构使用了标准的列表类型('features'是字符串列表),而引用[2]提到下载数据集时可能只下载了元数据,这可能是导致我们无法获取完整数据(包括LargeList)的原因。 解决方案思路: 1. 检查是否确实需要LargeList类型,或者是否可以使用其他方式处理大型列表(例如分块处理)。 2. 确保正确下载了数据集,因为引用[2]提到只下载元数据文件可能导致数据不完整。 3. 如果datasets包本身不支持LargeList,考虑转换数据类型或使用其他支持大型列表的数据结构。 具体步骤: 步骤1:确认数据集下载完整 使用datasets.load_dataset函数时,确保下载的是完整数据集,而不仅仅是元数据。可以尝试重新下载数据集。 步骤2:检查特征类型 在引用[1]中,数据集的特征是字符串列表(如['id','summary','article']),并没有出现LargeList类型。因此,问题可能是用户的数据集有特殊需求,而标准datasets包的特征类型不支持LargeList。 步骤3:自定义特征类型(如果必要) 如果数据集中确实包含大型列表(例如,个特征是非常长的整数列表或浮点数列表),我们可以尝试使用datasets.Features自定义特征类型。但是,目前datasets包可能没有显式的LargeList类型。我们可以尝试使用以下替代方案: - 使用`datasets.Sequence`来定义可变长度的列表。例如,对于整数列表,可以定义为`datasets.Sequence(datasets.int64)`。 - 如果列表非常大,考虑将数据存储为磁盘上的文件,并在数据集中存储文件路径,然后按需加载。 步骤4:使用其他格式存储大型列表 如果单个特征的数据量过大(例如个特征包含数百万个元素),那么将其作为单个特征存储可能不合适。考虑将大型列表拆分成多个特征,或者使用种更高效的文件格式(如HDF5)来存储,然后在数据集中存储文件路径。 步骤5:扩展datasets包的功能(高级) 如果上述方法都不行,可以考虑扩展datasets包,添加对LargeList类型的支持。这需要修改datasets包的源码,实现个新的`FeatureType`。但这需要深入理解datasets包的结构。 根据引用[2]中的信息,确保使用正确的工具(如modelscope cli或git)下载完整的数据集,并检查是否有版本兼容性问题。 示例代码:如何定义Sequence特征 ```python from datasets import Dataset, Features, Sequence, Value # 假设我们需要个包含大型整数列表的数据集 features = Features({ 'id': Value('string'), 'large_list': Sequence(Value('int64')), }) # 创建数据集 data = { 'id': ['1', '2'], 'large_list': [[1,2,3,4,5], [10,20,30,40,50,60,70]], # 注意:这里每个列表长度可以不同 } dataset = Dataset.from_dict(data, features=features) ``` 如果列表非常大,上述方法可能导致内存问题。因此,在创建数据集时,我们可以考虑使用生成器来逐步提供数据,避免次性加载所有数据。 另外,考虑使用`datasets.Dataset.from_generator`方法,它允许动态生成每个样本,从而节省内存。 总结:解决datasets包没有LargeList的方法主要是使用`Sequence`特征类型,并确保数据以流式或生成器方式加载以避免内存问题。同时,确保数据集下载完整。 如果问题仍然存在,可能需要联系datasets包的开发者或寻求其他替代方案。 相关问题: 1. 如何检查datasets包中数据集是否完整下载? 2. 如何使用datasets.Sequence定义可变长度特征? 3. 如何处理超大型数据集以避免内存溢出? 4. 如何扩展datasets包以支持新的特征类型? </think>### 解决 Python datasets 包缺少 LargeList 类型的方法 在 Python 的 `datasets` 包(Hugging Face Datasets 库)中确实没有内置的 `LargeList` 数据类型,但可以通过以下方法解决: #### 1. **使用 Sequence 类型替代(推荐)** `datasets` 包提供 `Sequence` 特性类型,专门用于处理可变长列表数据(包括大型列表): ```python from datasets import Dataset, Features, Sequence, Value # 定义包含大型列表的数据集结构 features = Features({ 'id': Value('string'), 'large_data': Sequence(Value('int64')) # 替代 LargeList }) # 创建数据集实例 dataset = Dataset.from_dict({ 'id': ['item1', 'item2'], 'large_data': [[1, 2, 3, ...], [4, 5, 6, ...]] # 支持长列表 }, features=features) ``` - **优势**:内存高效,支持惰性加载 - **原理**:`Sequence` 类型在后台使用 Apache Arrow 的 `ListArray`,可处理百万级元素的列表[^1] #### 2. **数据类型转换技巧** 若从其他格式(如 Parquet)导入数据时遇到类型不兼容: ```python from datasets import Dataset import pandas as pd df = pd.read_parquet('data.parquet') df['large_list'] = df['large_list'].apply(lambda x: list(x)) # 强制转为 Python list dataset = Dataset.from_pandas(df) ``` #### 3. **文件存储优化** 当列表极大(GB 级)时,建议拆分存储: ```python features = Features({ 'id': Value('string'), 'data_path': Value('string') # 存储二进制文件路径 }) def process_example(example): # 按需从磁盘加载大型数据 with open(example['data_path'], 'rb') as f: large_data = pickle.load(f) return large_data ``` #### 4. **升级和版本兼容性** 确保环境兼容(引用[2]的兼容性问题): ```bash pip install datasets>=2.0.0 --upgrade # 新版优化了大型数据支持 ``` #### 5. **替代方案** 若仍遇瓶颈,可考虑: - 使用 `vaex` 或 `dask` 处理超大型数据集 - 采用分块加载: ```python dataset = load_dataset('my_data', streaming=True) # 流式加载 for chunk in dataset.iter(batch_size=1000): process(chunk) ``` ### 关键注意事项 1. 内存管理:`datasets` 默认使用内存映射文件,但建议监控内存使用 2. 类型验证:通过 `dataset.features` 检查数据类型是否正确转换 3. 性能优化:设置 `keep_in_memory=False` 避免OOM错误 > 关于字典类型的内存局限(如引用[3]所述),`Sequence` 通过 Apache Arrow 的列式存储规避了传统 Python 字典的内存开销问题,特别适合大型列表数据[^3]。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值