Exporting Data in Parallel

I recently had a customer that needed to export data from a huge table to a different machine in csv format very fast. The Database was Standard Edition 2 (SE2) so there was no Parallel Query Option to use.

The solution

Since there was no Parallel Query Option available the solution was to split the huge table logically into several parts (with the analytic function ntile) and using multiple SQLcl processes on the client machine to export these logical parts. While exporting the data we use 7z to compress the data on the fly before writing the final compressed csv file to disk. In that way we were able to reduce the runtime of the export from 20 hours to 16 minutes.

The files

run_parallel.bat

This is the main file to start the export. It will spawn a configurable number of workers. If you want to change the number of workers the variable NUM_WORKERS in this file and the variable NUM_WORKERS in the file worker.sql needs to be adjusted.

@echo off
setlocal enabledelayedexpansion

echo ============================================================
echo Parallel Batch Runner - Starting Workers
echo ============================================================
echo.

REM Set number of Workers
set NUM_WORKERS=5

REM Get database password


REM Clean up any old marker and zip files
del /Q worker_*_done.txt 2>nul
del /Q CSV_*.7z 2>nul

REM Record start time
echo Start Time: %TIME%
echo %TIME% > start_time.tmp

echo.

REM Launch workers
echo Launching %NUM_WORKERS% workers...
for /L %%i in (1,1,%NUM_WORKERS%) do (
    start "Worker %%i" /MIN cmd /c worker.bat %%i
    echo   Worker %%i launched
)

echo.
echo All workers launched.
echo Waiting for completion...
echo.

REM Wait for all workers
:WAIT_LOOP
set /A COUNT=0
for /L %%i in (1,1,%NUM_WORKERS%) do (
    if exist worker_%%i_done.txt set /A COUNT+=1
)

echo Workers completed: !COUNT!/%NUM_WORKERS%
timeout /t 5 /nobreak >nul
if !COUNT! LSS %NUM_WORKERS% goto WAIT_LOOP

REM Record end time
echo %TIME% > end_time.tmp

echo.
echo ============================================================
echo All workers completed!
echo ============================================================
echo.

REM Read times from files
set /p START_TIME=<start_time.tmp
set /p END_TIME=<end_time.tmp

echo Start Time: %START_TIME%
echo End Time:   %END_TIME%

REM Calculate elapsed (call subroutine)
call :calc_elapsed

echo.

REM Clean up
del /Q worker_*_done.txt 2>nul
del /Q start_time.tmp end_time.tmp 2>nul

echo Press any key to exit...
pause >nul
goto :EOF

:calc_elapsed
REM Extract hours, minutes, seconds from START_TIME
set START_TIME_CLEAN=%START_TIME: =0%
set START_TIME_CLEAN=%START_TIME_CLEAN:,=.%

for /f "tokens=1-3 delims=:." %%a in ("%START_TIME_CLEAN%") do (
    set HH1=%%a
    set MM1=%%b
    set SS1=%%c
)

REM Extract hours, minutes, seconds from END_TIME
set END_TIME_CLEAN=%END_TIME: =0%
set END_TIME_CLEAN=%END_TIME_CLEAN:,=.%

for /f "tokens=1-3 delims=:." %%a in ("%END_TIME_CLEAN%") do (
    set HH2=%%a
    set MM2=%%b
    set SS2=%%c
)

REM Remove leading zeros and convert to decimal
set /a HH1=1!HH1!-100
set /a MM1=1!MM1!-100
set /a SS1=1!SS1!-100
set /a HH2=1!HH2!-100
set /a MM2=1!MM2!-100
set /a SS2=1!SS2!-100

REM Convert to seconds
set /a START_SECS=HH1*3600+MM1*60+SS1
set /a END_SECS=HH2*3600+MM2*60+SS2

REM Calculate difference
set /a ELAPSED=END_SECS-START_SECS

REM Handle midnight rollover
if %ELAPSED% LSS 0 set /a ELAPSED+=86400

REM Convert back to HH:MM:SS
set /a HH=ELAPSED/3600
set /a MM=(ELAPSED%%3600)/60
set /a SS=ELAPSED%%60

REM Add leading zeros
if %HH% LSS 10 set HH=0%HH%
if %MM% LSS 10 set MM=0%MM%
if %SS% LSS 10 set SS=0%SS%

echo Elapsed Time: %HH%:%MM%:%SS%
echo Total Seconds: %ELAPSED%

goto :EOF

worker.bat

These are the individual worker files that will be spawned from run_parallel.bat. This file starts the SQLcl instance and connect to the database to export the csv data. Adjust the paths and connection data as required by your environment.

@echo off
REM Parameter %1 is the worker ID

set WORKER_ID=%1

echo Worker %WORKER_ID%: Starting...

REM SQLcl options
set _JAVA_OPTIONS=-Xms32m -Xmx64m
set JAVA_HOME=C:\Program Files\Java\jdk-17
set TNS_ADMIN=%CD%

REM start SQLcl and 7z:
c:\sw\sqlcl\bin\sql -S user1/changeme@orcl @%CD%\worker.sql "%WORKER_ID%" | "C:\Program Files\7-Zip\7z.exe" a -siCSV_%WORKER_ID%.csv -aoa -mmemuse=128m -mmt=off -mx2 %CD%\CSV_%WORKER_ID%.7z

echo Worker %WORKER_ID%: Completed!

REM Create completion marker file
echo Done > worker_%WORKER_ID%_done.txt

exit /b 0

worker.sql

This file contains the SQL command each worker starts and is used to split the big table into logical parts.

SET ECHO OFF
SET FEEDBACK OFF
SET VERIFY OFF
--SET TERMOUT OFF
SET HEADING OFF
SET LINESIZE 32767
SET PAGESIZE 0
SET TRIMSPOOL ON
SET TRIMOUT ON
SET COLSEP ';'
SET DEFINE ON
SET ARRAYSIZE 5000
SET TIMING OFF
SET TERMOUT OFF
alter session set nls_date_format='YYYY-MM-DD HH24:MI';
SET TERMOUT ON

DEFINE WORKER_ID = &1
DEFINE NUM_WORKERS = 5

-- Header row
PROMPT OWNER;OBJECT_NAME;SUBOBJECT_NAME;OBJECT_ID;DATA_OBJECT_ID;OBJECT_TYPE;CREATED;LAST_DDL_TIME;TIMESTAMP;STATUS;TEMPORARY;GENERATED;SECONDARY;NAMESPACE;EDITION_NAME;SHARING;EDITIONABLE;ORACLE_MAINTAINED
 
-- Data query
SELECT OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY, NAMESPACE, EDITION_NAME, SHARING, EDITIONABLE, ORACLE_MAINTAINED
FROM
(
	SELECT
		t.*,
		NTILE(&&NUM_WORKERS) OVER (ORDER BY t.object_id) AS worker_id
	FROM
		t
)
WHERE worker_id = &&WORKER_ID;
 
--SPOOL OFF
SET TERMOUT ON
SET FEEDBACK ON
SET HEADING ON
exit

tnsnames.ora

This is just a regular tnsnames.ora file used to connect to the database. We increased the SDU size for the export to have fewer round trips over the network.

orcl =
  (DESCRIPTION =
    (SDU=32767) # don't forget to adjust the sqlnet.ora on the database server for this option to have an effect
    (ADDRESS = (PROTOCOL = TCP)(HOST = win4.fritz.box)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = orcl.fritz.box)
    )
  )

Demo

To show how these scripts are working we create a sample table t with the following command:

create table t tablespace users as select a2.* from all_objects a1, all_objects a2 where rownum<=1e6 order by a1.object_id;

Then we start .\run_parallel.bat to start the export and wait until all workers finish. The output will be similar to:

C:\sw\run_parallel>.\run_parallel.bat
============================================================
Parallel Batch Runner - Starting Workers
============================================================

Start Time:  8:49:40,90

Launching 5 workers...
  Worker 1 launched
  Worker 2 launched
  Worker 3 launched
  Worker 4 launched
  Worker 5 launched

All workers launched.
Waiting for completion...

Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 0/5
Workers completed: 5/5

============================================================
All workers completed
============================================================

Start Time:  8:49:40,90
End Time:    8:51:19,18
Elapsed Time: 00:01:39
Total Seconds: 99

Press any key to exit...

After completion the following csv files are created:

C:\sw\run_parallel>dir
 Volume in drive C is driveC
 Volume Serial Number is B48B-A701

 Directory of C:\sw\run_parallel

12.03.2026  08:51    <DIR>          .
12.03.2026  08:51    <DIR>          ..
12.03.2026  08:51           263.179 CSV_1.7z
12.03.2026  08:51           298.679 CSV_2.7z
12.03.2026  08:51           306.203 CSV_3.7z
12.03.2026  08:51           280.295 CSV_4.7z
12.03.2026  08:51           324.246 CSV_5.7z
12.03.2026  08:25             2.866 run_parallel.bat
12.03.2026  08:53               308 tnsnames.ora
12.03.2026  08:49               574 worker.bat
12.03.2026  08:40             1.035 worker.sql
               9 File(s)      1.477.385 bytes
               2 Dir(s)  735.083.913.216 bytes free
0