Képzeljük el a helyzetet: szükség van rá, hogy egy adatbázisból kinyerjünk egy jelentős mennyiségű adatot – mondjuk, az összes ügyfelünk listáját – és azt egyetlen, összefűzött CSV-sorba konvertáljuk. A feladat elsőre talán egyszerűnek tűnik, hiszen CSV exportot szinte minden adatbázis kezel, de a „egyetlen sorba” kritérium az, ami igazán megizzasztja a legtöbb fejlesztőt. Pláne, ha a projekt korlátozottsága miatt PL/SQL nélkül kell boldogulnunk. Ez egy olyan szituáció, amivel időről időre szembesülhetünk, legyen szó legacy rendszerek integrációjáról, speciális adatintegrációs feladatokról, vagy olyan környezetekről, ahol a procedurális nyelvek használata korlátozott. Ne ijedjünk meg, nem lehetetlen küldetés! Ebben a cikkben részletesen körbejárjuk, hogyan tehetjük meg ezt a bravúrt pusztán natív SQL és némi parancssori varázslat segítségével.
Miért pont egyetlen sor és miért PL/SQL nélkül?
Mielőtt belemerülnénk a technikai részletekbe, érdemes tisztázni, miért is találhatjuk magunkat ilyen szokatlan elvárásokkal szemben. 🤔
- Legacy rendszerek integrációja: Bizonyos régebbi rendszerek vagy egyedi, harmadik féltől származó szoftverek elvárhatják az adatbevitel ezen különleges formáját. Lehet, hogy egy batch feldolgozó script csak egyetlen hosszú stringet képes beolvasni, amelyet aztán maga darabol fel.
- Sávszélesség-takarékosság / Fájlméret optimalizálás: Bár ellentmondásosnak tűnhet, extrém esetekben a sorvégi karakterek (newline) kihagyásával „kompaktabbá” tehetjük a fájlt, ha azt nagy mennyiségben, nagyon szűk sávszélességen kell továbbítani. (Persze, a tömörítés sokkal hatékonyabb, de ez egy lehetséges indok lehet.)
- Adatellenőrzés / Gyors áttekintés: Néha egyszerűen csak egy nagyon gyors vizuális ellenőrzéshez van szükség a teljes adatmennyiségre, egyetlen nézhető sorban (persze, kellő óvintézkedésekkel).
- PL/SQL korlátok: Ez a kulcsmomentum. Elképzelhető, hogy nincs jogosultságunk PL/SQL blokkok írására, vagy olyan felhőalapú adatbázist használunk, amely nem támogatja ezt a funkciót, esetleg egy harmadik féltől származó alkalmazás korlátozza a hozzáférést. Ilyenkor kreatívnak kell lennünk, és a „plain SQL” vagy külső eszközök adta lehetőségeket kiaknázni.
A kihívás lényege: Sorokból oszlopok, oszlopokból egyetlen string
A hagyományos CSV export (gondoljunk csak a SPOOL
parancsra vagy egy grafikus felület export funkciójára) soronként egy-egy rekordot, azaz egy-egy ügyfelet ír ki. A mi célunk az, hogy minden ügyfél adatát összefűzzük egyetlen, hosszú stringgé, majd ezeket az ügyfél-stringeket fűzzük össze egy még hosszabb, mindent tartalmazó stringgé.
Ez lényegében kétlépcsős aggregációt jelent:
- Egy ügyfél sorának oszlopait fűzzük össze egy stringgé (pl.
UgyfelID,Nev,Email
). - Az összes ilyen ügyfél-stringet fűzzük össze egyetlen végső stringgé, egy sor-elválasztó karakterrel (pl.
Ugyfel1String|Ugyfel2String|Ugyfel3String
).
A kulcskérdés, hogy az adatbázisunk milyen funkciókat kínál ehhez a fűzéshez és aggregációhoz PL/SQL nélkül. Szerencsére a modern relációs adatbázisok rendelkeznek erre alkalmas beépített függvényekkel. 💡
Natív SQL megoldások adatbázisonként
Nézzük meg, hogyan valósítható meg ez a feladat a legnépszerűbb adatbázis-rendszerekben.
1. Oracle Database: A nagy öreg LISTAGG
Az Oracle adatbázisok a LISTAGG
aggregáló függvényt kínálják a sorok értékeinek összefűzésére. Ez a funkció tökéletesen alkalmas a feladatunkra. Képes kezelni a beépített string hossz limitet (VARCHAR2, általában 4000 byte vagy 32767 byte CLOB-ra konvertálva), bár utóbbi esetben is van egy felső határ. ⚙️
Tegyük fel, hogy van egy ugyfel_tabla
nevű táblánk a következő oszlopokkal:
CREATE TABLE ugyfel_tabla (
ugyfel_id NUMBER,
nev VARCHAR2(100),
email VARCHAR2(100),
regisztracio_datuma DATE
);
INSERT INTO ugyfel_tabla VALUES (1, 'Kovács János', '[email protected]', SYSDATE - 365);
INSERT INTO ugyfel_tabla VALUES (2, 'Nagy Anna', '[email protected]', SYSDATE - 180);
INSERT INTO ugyfel_tabla VALUES (3, 'Tóth Béla', '[email protected]', SYSDATE - 90);
-- Stb.
A megoldás két LISTAGG
lépcsőben:
SELECT
LISTAGG(ugyfel_string, '|') WITHIN GROUP (ORDER BY ugyfel_id) AS osszefuzott_csv_sor
FROM (
SELECT
ugyfel_id,
-- Itt fűzzük össze az egyes ügyfelek oszlopait egy stringgé
-- Fontos: kezeljük a speciális karaktereket és a dátum formátumát!
-- CHR(34) a dupla idézőjel karakter.
CHR(34) || ugyfel_id || CHR(34) || ',' ||
CHR(34) || REPLACE(nev, CHR(34), CHR(34) || CHR(34)) || CHR(34) || ',' ||
CHR(34) || REPLACE(email, CHR(34), CHR(34) || CHR(34)) || CHR(34) || ',' ||
CHR(34) || TO_CHAR(regisztracio_datuma, 'YYYY-MM-DD') || CHR(34) AS ugyfel_string
FROM
ugyfel_tabla
) subquery;
Magyarázat:
- A belső lekérdezés (
subquery
) felelős azért, hogy minden egyes ügyfél adatait egyetlen, CSV formátumú stringgé alakítsa (pl."1","Kovács János","[email protected]","2023-01-01"
). - Fontos, hogy a
CHR(34)
segítségével idézőjelek közé tegyük az értékeket, különösen, ha azok tartalmazhatnak vesszőt vagy sorvég karaktert. AREPLACE
függvénnyel kezeljük az idézőjelek escapelését az adatokon belül ("
->""
). - A külső
LISTAGG
ezután ezeket az ügyfél-stringeket fűzi össze egymással, a|
karaktert használva sor-elválasztónak. AzORDER BY ugyfel_id
gondoskodik a konzisztens sorrendről. - Fontos megjegyzés: Oracle 12.2-től a
LISTAGG
támogatja azON OVERFLOW TRUNCATE
vagyERROR
opciókat, ami segíthet a túl hosszú stringek kezelésében. Ha a végső string túl hosszú lenne (több mint 4000 vagy 32767 byte), akkor ez a megoldás már nem működik natívan, és alternatív megközelítésekre (pl. külső script, vagy több részre osztás) van szükség.
2. SQL Server, PostgreSQL: A megbízható STRING_AGG
Az SQL Server (2017+) és a PostgreSQL (9.0+) is rendelkezik hasonló, de talán rugalmasabb aggregáló függvénnyel: a STRING_AGG
-gel. 💾
-- SQL Server / PostgreSQL szintaxis
SELECT
STRING_AGG(ugyfel_string, '|') WITHIN GROUP (ORDER BY ugyfel_id) AS osszefuzott_csv_sor
FROM (
SELECT
ugyfel_id,
-- Ugyanaz a logika, mint az Oracle esetében: oszlopok összefűzése
-- PostgreSQL esetén '"' a dupla idézőjel escapelésére
-- SQL Server esetén CHAR(34)
CONCAT(
'"', ugyfel_id, '"', ',',
'"', REPLACE(nev, '"', '""'), '"', ',',
'"', REPLACE(email, '"', '""'), '"', ',',
'"', FORMAT(regisztracio_datuma, 'yyyy-MM-dd'), '"' -- SQL Server
-- '"', TO_CHAR(regisztracio_datuma, 'YYYY-MM-DD'), '"' -- PostgreSQL
) AS ugyfel_string
FROM
ugyfel_tabla
) AS subquery;
Különbségek és megjegyzések:
- Az
AS subquery
alias kötelező az SQL Serverben. - A string összefűzéshez SQL Serveren a
CONCAT
függvényt, vagy a+
operátort használhatjuk. PostgreSQL-en a||
operátor az elterjedt. - Dátum formázására SQL Serveren a
FORMAT
, PostgreSQL-en aTO_CHAR
a jellemző. - A
STRING_AGG
szintén rendelkezik egy maximális kimeneti hosszal (SQL Serveren VARCHAR(MAX) vagy NVARCHAR(MAX), ami akár 2GB is lehet, PostgreSQL-en TEXT, ami gyakorlatilag korlátlan). Ez sokkal rugalmasabbá teszi nagy adathalmazok esetén.
3. MySQL: A klasszikus GROUP_CONCAT
A MySQL-ben a GROUP_CONCAT
függvény szolgálja ezt a célt, és meglehetősen rugalmas. 🐳
SELECT
GROUP_CONCAT(ugyfel_string SEPARATOR '|') AS osszefuzott_csv_sor
FROM (
SELECT
ugyfel_id,
-- Oszlopok összefűzése
-- MySQL-ben is CHR(34) vagy QUOTE() függvény
CONCAT_WS(',',
QUOTE(ugyfel_id),
QUOTE(nev),
QUOTE(email),
QUOTE(DATE_FORMAT(regisztracio_datuma, '%Y-%m-%d'))
) AS ugyfel_string
FROM
ugyfel_tabla
) AS subquery;
Megjegyzések:
- A
GROUP_CONCAT
alapértelmezetten vesszővel fűz össze, de aSEPARATOR
kulcsszóval könnyen megváltoztathatjuk. - A MySQL
QUOTE()
függvénye automatikusan idézőjelek közé teszi és escapeli a speciális karaktereket, ami nagyban leegyszerűsíti a munkát. - A
GROUP_CONCAT
kimenetének hossza alapértelmezetten 1024 karakterre van korlátozva, de ez agroup_concat_max_len
rendszerparaméterrel (pl.SET GLOBAL group_concat_max_len = 1000000;
) megnövelhető. Enélkül könnyen elveszhetnek adatok, ha a teljes string meghaladja az alapértelmezett értéket!
Parancssori varázslat: Amikor a SQL nem elég
Vannak helyzetek, amikor a natív SQL függvények elérik a korlátjukat (pl. string hossza, vagy túl bonyolult a beágyazott speciális karakterek kezelése). Ilyenkor jöhetnek szóba a parancssori eszközök, amelyekkel a kinyert adatokat utólag dolgozhatjuk fel. Ez a megközelítés kissé kilép a „pusztán SQL” kereteiből, de továbbra is PL/SQL-mentes marad. 💻
SQL kliens és shell scriptek kombinációja (pl. SQLPlus, psql, mysql cli + awk/sed)
A stratégia a következő:
- Futtassunk egy egyszerű SQL lekérdezést, amely minden ügyfelet külön sorban ad vissza, az oszlopokat vesszővel elválasztva és idézőjelezve. Itt még nem fűzzük össze az ügyfél-sorokat.
- A lekérdezés kimenetét csövezzük (
|
) egy parancssori eszközbe, mint azawk
vagy ased
, amelyek majd összefűzik ezeket a sorokat.
Példa (Oracle SQLPlus + awk
):
sqlplus -S user/pass@db << EOF
SET PAGESIZE 0 FEEDBACK OFF HEADING OFF VERIFY OFF
SELECT
-- Itt még ügyfélenként egy sor, idézőjelekkel és vesszőkkel
CHR(34) || ugyfel_id || CHR(34) || ',' ||
CHR(34) || REPLACE(nev, CHR(34), CHR(34) || CHR(34)) || CHR(34) || ',' ||
CHR(34) || REPLACE(email, CHR(34), CHR(34) || CHR(34)) || CHR(34) || ',' ||
CHR(34) || TO_CHAR(regisztracio_datuma, 'YYYY-MM-DD') || CHR(34)
FROM
ugyfel_tabla
ORDER BY ugyfel_id;
EOF
| awk 'BEGIN {ORS=""; first=1} {if (!first) print "|"; print; first=0}'
Magyarázat:
- Az
sqlplus -S ...
parancs elrejti a headereket és lábléceket, csak a tiszta adatot adja vissza. - Az
awk
script a kimeneti rekord elválasztót (ORS
) üresre állítja, ami azt jelenti, hogy nem tesz automatikusan sorvég karaktert a kimenet végére. - A
first
változóval kezeljük, hogy az első ügyfél elé ne tegyünk|
karaktert. Utána minden ügyfél elé betesszük a|
-t.
Ez a módszer rendkívül rugalmas és szinte bármilyen adatbázishoz adaptálható, feltéve, hogy a parancssori kliens telepítve van. A hátránya, hogy a teljes eredményt a shell-nek kell memóriában tartania a feldolgozáshoz, ami extrém nagy adatmennyiség esetén problémás lehet.
Tapasztalataink szerint a
LISTAGG
vagySTRING_AGG
funkciók hiába tűnnek elegánsnak, hatalmas adatmennyiségek (több tíz- vagy százmillió sor) esetén a generált string mérete könnyedén meghaladhatja az adatbázis által támogatott maximális VARCHAR2/STRING hosszt, memória- és CPU problémákat okozva. Ilyenkor érdemes átgondolni, valóban *egy* sorra van-e szükség, vagy elfogadható több sor, akár egy külső script által feldolgozva. Például, ha a célrendszer képes soronként feldolgozni az adatot, de egyedi sor-elválasztót vár, akkor a shell scriptes megoldás, ahol a newline karaktert cseréljük le, sokkal skálázhatóbb lehet. Ne feledjük, a "kényelem" néha a teljesítmény rovására mehet!
Fontos szempontok a sikeres exportáláshoz
Bármelyik megoldást is választjuk, van néhány kritikus tényező, amire oda kell figyelnünk, hogy a generált CSV valóban érvényes és felhasználható legyen.
- Elválasztó karakter (Delimiter): A vessző (
,
) a szabványos, de ha az adataink is tartalmazhatnak vesszőt, akkor vagy idézőjeleket kell használnunk (lásd alább), vagy egy másik, nem megjelenő karaktert kell választanunk (pl. pontosvessző;
, tabulátort
, vagy pipe|
). A cikkben a pipe karaktert használtuk a sorok elválasztására, hogy ne keveredjen a mezők közötti vesszővel. - Idézőjelek (Quoting): Ez a CSV export lelke. A mezőket idézőjelek (általában dupla idézőjelek
"
) közé kell tennünk, ha azok tartalmaznak:- az elválasztó karaktert (pl. vesszőt)
- sorvég karaktert (newline)
- vagy magát az idézőjelet.
Ha egy mező maga is tartalmaz idézőjelet, akkor azt meg kell duplázni (escapelni kell): pl.
"Kovács "János" Péter"
-ből"Kovács ""János"" Péter"
lesz. A fenti SQL példákban ezt aREPLACE
függvénnyel vagy aQUOTE()
funkcióval kezeltük. - Null értékek: A
NULL
értékek általában üres stringként (""
) jelennek meg CSV-ben. Ezt is kezelni kell a string összefűzés során (pl.NVL(oszlop, '')
Oracle-ben, vagyCOALESCE(oszlop, '')
más adatbázisokban). - Adattípusok konvertálása: Minden nem string típusú adatot (számok, dátumok) explicit módon stringgé kell alakítani, a kívánt formátumban (pl.
TO_CHAR(dátum, 'YYYY-MM-DD')
). - Oszlopok sorrendje: Győződjünk meg róla, hogy az oszlopok mindig ugyanabban a sorrendben szerepelnek a kimeneti stringben. Ezt az
ORDER BY
záradékkal tudjuk biztosítani.
Végszó: Nehéz, de megoldható
A PL/SQL nélküli, egyetlen soros CSV export egy olyan feladat, ami a fejlesztőket kreatív gondolkodásra ösztönzi. Bár elsőre ijesztőnek tűnhet a korlátok miatt, láthatjuk, hogy a modern adatbázisok beépített funkciói (LISTAGG
, STRING_AGG
, GROUP_CONCAT
) és a parancssori eszközök (awk
, sed
) segítségével is elérhető a kívánt eredmény. A kulcs a részletekre való odafigyelésben rejlik: a megfelelő elválasztó karakter, az idézőjelezés, a null értékek és az adattípusok konzisztens kezelése elengedhetetlen a működőképes és szabványos CSV fájlhoz.
Ne feledjük, minden eszköznek megvannak a maga korlátai. Míg az SQL aggregáló függvények rendkívül elegánsak, string hossz limitjük lehet. A parancssori scriptek rugalmasabbak lehetnek a hosszt illetően, de a teljesítmény és a memóriaigényük jelentős lehet hatalmas adatmennyiség esetén. A legjobb megoldás mindig az, amelyik a leginkább illeszkedik az adott projekt speciális igényeihez, a rendelkezésre álló erőforrásokhoz, és persze az elvárt teljesítményhez. Próbáljuk ki a különböző megközelítéseket, teszteljük őket alaposan, és válasszuk azt, amelyik a legmegbízhatóbbnak bizonyul a mi környezetünkben! Adatbázis varázslatra fel! ✨