DB2 SQL

Esempi DB2 SQL per IBMi – parte 2

Sono passati oltre 10 anni da quando ho pubblicato alcuni esempi di SQL per IBMi (AS/400) che, ad oggi, è ancora l'articolo più visto di questo blog. Così, visto che ancora utilizzo quotidianamente SQL per la mia attività lavorativa (e non solo), ho deciso di scriverne un altro con nuovi trick, considerando anche che, nel frattempo, il DB2 è molto cresciuto e si è  arricchito di nuove funzionalità.

Giocare con date e tempi

Convertire data numerica in date

Le funzioni descritte in seguito prevedono di lavorare su colonne di tipo date o timestamp, ma tradizionalmente molte colonne "data" delle tabelle su IBMi sono definite come numeriche e non date, quindi, la prima cosa da capire è come convertirle nei formati sopra indicati.
Perchè dovrei farlo? I motivi possono essere diversi, ad esempio per interfacciarmi con altri DB o strutture esterne che richiedono veri formati date, oppure per poter utilizzare funzioni presenti nel DB2 che agiscono solo su campi date come quelle per reperire la settimana di una data.
Lo si può fare in vari modi ma quello che preferisco è il seguente:

date(to_date(char(mia_colonna_numerica), 'YYYYMMDD'))

Esempio:

WITH
test AS (
SELECT 
20211229 AS data_num
FROM sysibm.sysdummy1
)

SELECT 
data_num,
date(to_date(char(data_num), 'YYYYMMDD')) AS data_date
FROM test 

il risultato è il seguente:

DATA_NUM DATA_DATE
20211229 2021-12-29

Convertire colonna date in formato numerico

Può capitare di dover fare anche il contrario di quanto descritto precedentemente, ovvero convertire il valore di una colonna di tipo date in un dato numerico intero.
L'esempio seguente mostra come farlo con la data corrente:

select 
int(replace(char(current date, ISO), '-', ''))
from sysibm.sysdummy1

Convertire una data in formato testo con separatori

SELECT 
VARCHAR_FORMAT(timestamp_iso(CURRENT_DATE), 'DD/MM/YYYY') 
FROM sysibm.sysdummy1

se la data attuale è il '2021-12-28' il risultato sarà "28/12/2021".

ATTENZIONE: VARCHAR_FORMAT funziona con i timestamp e non i date, motivo per cui, nell'esempio sopra, uso la funzione timestamp_iso per trasformare la data in timestamp. Mentre nell'esempio sotto non serve perchè la funzione to_date trasforma già in timestamp:

WITH
test AS (
SELECT 
20211229 AS data_num
FROM SYSIBM.sysdummy1
)

SELECT 
data_num,
VARCHAR_FORMAT(to_date(char(data_num), 'YYYYMMDD'), 'DD/MM/YYYY')  as data_char
FROM test

il risultato è il seguente:

DATA_NUM DATA_CHAR
20211229 29/12/2021

Convertire due campi numerici data e ora in un campo timestamp

WITH
test AS (
SELECT 
20211229 AS data_num,
84532 AS ora_num
FROM SYSIBM.sysdummy1
)

SELECT 
data_num,
ora_num,
timestamp(lpad(data_num, 8, '0')||lpad(ora_num, 6, '0')) as mio_timestamp
FROM test 

il risultato è il seguente:

DATA_NUM ORA_NUM MIO_TIMESTAMP
20211229 84532 2021-12-29 08:45:32.000

Anno, mese, giorno

Per estrarre queste informazioni si usano rispettivamente le funzioni YEAR, MONTH e DAY. Inoltre esiste la funzione DAYNAME che restituisce il nome del giorno (lunedì, martedì, ecc.).

Esempio:

WITH
test (data_num)  
as 
( 
values 
(20211229), 
(20220101), 
(20220105), 
(20220106) 
) 

SELECT 
a.data_num,
year(to_date(char(data_num), 'YYYYMMDD')) AS anno,
month(to_date(char(data_num), 'YYYYMMDD')) AS mese,
day(to_date(char(data_num), 'YYYYMMDD')) AS giorno,
dayname(to_date(char(data_num), 'YYYYMMDD')) AS nome_giorno
FROM test a

il risultato è il seguente:

DATA_NUM ANNO MESE GIORNO NOME_GIORNO
20211229 2021 12 29 Mercoledi
20220101 2022 1 1 Sabato
20220105 2022 1 5 Mercoledi
20220106 2022 1 6 Giovedì

Funzioni relative alle Settimane

Ci sono varie funzioni che forniscono informazioni sulle settimane. Tra queste ne ho scelte alcune:

  • WEEK: restituisce il numero della settimana della data fornita. Considera la Domenica il primo giorno della settimana e la prima settimana dell'anno è quella che contiene l'1 Gennaio
  • WEEK_ISO: come WEEK restituisce il numero della settimana della data fornita. La differenza con WEEK è che il primo giorno della settimana è il Lunedì e la prima settimana dell'anno è quella che contiene il primo Giovedì dell'anno; quindi, ad esempio, se il 31 Dicembre dell'anno è giovedì, i primi 3 giorni dell'anno successivo appartengono all'ultima settimana dell'anno precedente (venerdì 1, sabato 2, domenica 3), poi lunedì 4 inizia una nuova settimana che contiene il giovedì e quindi diventa la settimana numero 1 dell'anno
  • VARCHAR_FORMAT: può fornire molte informazioni, ma, nell'esempio riportato sotto, restituisce la combinazione Anno-Settimana ISO (esempio 202205) grazie al parametro 'IYYYIW'
  • DAYOFWEEK e DAYOFWEEK_ISO: restituiscono entrambi il numero del giorno all'interno della settimana (da 1 a 7) della data fornita . La differenza consiste in quale giorno viene considerato il primo della settimana, ovvero rispettivamente la Domenica e il Lunedì.
  • DAYNAME: restituisce il nome descrittivo del giorno della data fornita: lunedì, martedì, mercoledì, ecc.
WITH
test (data_num)  
as 
( 
values 
(20211229), 
(20220101), 
(20220105), 
(20220106) 
) 

SELECT 
a.data_num,
week(to_date(char(data_num), 'YYYYMMDD')) AS Sett,
week_iso(to_date(char(data_num), 'YYYYMMDD')) AS Sett_ISO,
varchar_format(TIMESTAMP_FORMAT(char(data_num), 'YYYYMMDD'), 'IYYYIW') AS Anno_Sett,
dayofweek(to_date(char(data_num), 'YYYYMMDD')) AS GG_Sett,
dayofweek_iso(to_date(char(data_num), 'YYYYMMDD')) AS GG_Sett_ISO,
dayname(to_date(char(data_num), 'YYYYMMDD')) AS Nome_GG
FROM test a

il risultato è il seguente:

DATA_NUM Sett Sett_ISO Anno_Sett GG_Sett GG_Sett_ISO Nome_GG
20211229 53 52 202152 4 3 Mercoledi
20220101 1 52 202152 7 6 Sabato
20220105 2 1 202201 4 3 Mercoledi
20220106 2 1 202201 5 4 Giovedì

Calcolo settimana precedente a quella fornita

SELECT 
week_iso(next_day(date(to_date(char(2027*1000 + (01-1)*7 + 1), 'YYYYDDD')), 'FRI') - 7 DAYS) AS sett_prec
FROM SYSIBM.SYSDUMMY1

Nell'istruzione precedente ho fornito l'anno 2027 e la settimana 01 e il risultato fornito è 53, perchè il 2026 ha 53 settimane.

Sottrarre/Sommare periodi ad una data numerica

Di seguito un esempio per sottrarre o sommare periodo (giorni, mesi o anni) ad una data numerica:

WITH
test AS (
SELECT 
20211229 AS data_num
FROM SYSIBM.sysdummy1
)

SELECT 
data_num,
date(to_date(char(data_num), 'YYYYMMDD')) - 41 days as data_date,
int(replace(char(date(to_date(char(data_num), 'YYYYMMDD')) - 41 days, ISO), '-', '')) as data_numerica,
date(to_date(char(data_num), 'YYYYMMDD')) + 3 months as add_mesi,
date(to_date(char(data_num), 'YYYYMMDD')) + 3 years as add_anni
FROM test 

il risultato è il seguente:

DATA_NUM Data_Date Data_Numerica Add_MESI Add_Anni
20211229 2021-11-18 20211118 2022-03-29 2024-12-29

Calcolare la differenza in giorni tra due date

WITH
test AS (
SELECT 
20211225 AS data_num1,
20220529 AS data_num2
FROM SYSIBM.sysdummy1
)

SELECT 
data_num1,
data_num2,
days(date(to_date(char(data_num2), 'YYYYMMDD'))) - days(date(to_date(char(data_num1), 'YYYYMMDD'))) as gg_diff
FROM test 

il risultato è il seguente:

DATA_NUM1 DATA_NUM2 GG_DIFF
20211225 20220529 155

Calcolare differenza in minuti (o altro) tra due timestamp

Supponendo di avere degli intervalli di tempo dati da data e ora iniziali e data e ora finali (espressi in formato numerico), per calcolare l'intervallo di tempo in minuti (o secondi, o ore, ecc.) si può usare la funzione TIMESTAMPDIFF come da esempio seguente:

WITH
test (dt_ini, h_ini, dt_fin, h_fin)  
as 
( 
values 
(20211229, 060000, 20211229, 194500), 
(20220101, 075000, 20220103, 090000)
) 

select
a.*,
TIMESTAMPDIFF(4, char(timestamp(lpad(dt_fin, 8, '0')||lpad(h_fin, 6, '0')) - timestamp(lpad(dt_ini, 8, '0')||lpad(h_ini, 6, '0')))) AS diff_min
FROM test a 

il risultato è il seguente:

DT_INI H_INI DT_FIN H_FIN DIFF_MIN
20211229 60000 20211229 194500 825
20220101 75000 20220103 90000 2950

Nella funzione TIMESTAMPDIFF il primo argomento (il valore 4 nell'esempio precedente), espresso con un intero, indica in quale unità di misura si desidera l'intervallo:

Codice Significato
1 Microseconds
2 Seconds
4 Minutes
8 Hours
16 Days
32 Weeks
64 Months
128 Quarters
256 Years

Calcolare data di inizio e fine settimana di una certa data

Nell'esempio si considera lo standard ISO quindi la data di inizio settimana si riferisce ad un Lunedì mentre quella di fine settimana alla Domenica.
Inoltre calcolo anche le date di inizio e fine della stessa settimana dell'anno precedente.

WITH
test AS (
SELECT 
20211229 AS data_num
FROM SYSIBM.sysdummy1
)

SELECT 
data_num,
date(to_date(char(data_num), 'YYYYMMDD')) AS dt_date,
date((to_date(char(data_num), 'YYYYMMDD')) - dayofweek_iso((to_date(char(data_num), 'YYYYMMDD'))) days + 1 days ) as inizio_sett,
date((to_date(char(data_num), 'YYYYMMDD')) - dayofweek_iso((to_date(char(data_num), 'YYYYMMDD'))) DAYS + 7 days ) as fine_sett,
date(next_day(lpad(year(to_date(char(data_num), 'YYYYMMDD'))-2, 4) concat '-12-31', 'THU') + ((week_iso(to_date(char(data_num), 'YYYYMMDD'))-1)*7) days) - 3 days as inizio_sett_aa_prec,
date(next_day(lpad(year(to_date(char(data_num), 'YYYYMMDD'))-2, 4) concat '-12-31', 'THU') + ((week_iso(to_date(char(data_num), 'YYYYMMDD'))-1)*7) days) + 3 days as fine_sett_aa_prec
FROM test 

il risultato è il seguente:

DATA_NUM DATA_DATE Inizio_Sett Fine_Sett Inizio_Sett_AA_Prec Fine_Sett_AA_Prec
20211229 2021-12-29 2021-12-27 2022-01-02 2020-12-21 2020-12-27

Per il calcolo delle date relative alla settimana dell'anno precedente uso anche la funzione NEXT_DAY che restituisce la data del prossimo giorno della settimana relativo alla data fornita. In pratica lo uso per determinare il giovedì (parametro "THU") successivo al fine anno di due anni prima.

Calcolare data di inizio e fine mese di una data

WITH
test AS (
SELECT 
20240219 AS data_num
FROM SYSIBM.sysdummy1
)

SELECT 
data_num,
date(to_date(char(data_num), 'YYYYMMDD') - (day(to_date(char(data_num), 'YYYYMMDD'))-1) DAYS) as inizio_mese,
int(replace(char(date(to_date(char(data_num), 'YYYYMMDD') - (day(to_date(char(data_num), 'YYYYMMDD'))-1) DAYS), ISO), '-', '')) as inizio_mese_int,
last_day(date(to_date(char(data_num), 'YYYYMMDD'))) as fine_mese,
int(replace(char(date(last_day((to_date(char(data_num), 'YYYYMMDD')))), ISO), '-', '')) as fine_mese_int
FROM test 

il risultato è il seguente:

DATA_NUM Inizio_Mese Inizio_Mese_Int Fine_Mese Fine_Mese_Int
20240219 2024-02-01 20240201 2024-02-29 20240229

Tra le funzioni utilizzato per questo scopo c'è LAST_DAY per restituisce l'ultimo giorno del mese della data di riferimento.

Generare una sequenza di date in formato intero partendo da un range di date

Se per qualche motivo servisse generare una sequenza di date entro un certo range, si può utilizzare la seguente SQL ricorsiva:

with 
Periodo as (
select
'2022-01-01' as dt_ini,     -- data iniziale periodo
'2022-05-29' as dt_fin      -- data fine periodo
from sysibm.sysdummy1
),

calendario (dt_cal) as (
select 
date(dt_ini)
from periodo

union all   
-- ...poi leggo i successivi livelli ricorsivamente
select
dt_cal + 1 days 
from calendario
where dt_cal < (select date(dt_fin) from Periodo)
)

-- infine converto la data in formato numerico
select
int(replace(char(dt_cal, ISO), '-', '')) as dt_calen
from calendario

il risultato è il seguente:

DT_CALEN
20220101
20220102
20220103
....
20220529

Giocare con le stringhe

Dividere una stringa in più sottostringhe

Se si ha una stringa molto lunga e la si vuole dividere/splittare in più sotto stringhe, ad esempio da 20 caratteri, si può usare il seguente metodo:

WITH
testo (stringa_lunga) AS (
VALUES
('La donzelletta vien dalla campagna, in sul calar del sole, col suo fascio dell''erba; e reca in mano un mazzolin di rose e di viole, onde, siccome suole, ornare ella si appresta dimani, al dì di festa, il petto e il crine')
),

splittiamo_tutto (str, ref, stringa) as (
select substr(stringa_lunga, 1, 20), 1, stringa_lunga
from testo

union all

select substr(stringa, ref + 20, 20), ref + 20, stringa
from splittiamo_tutto
where substr(stringa, ref + 20, 20)<>''
)

select str from splittiamo_tutto

Il risultato sarà il seguente:

STR
La donzelletta vien
dalla campagna, in s
ul calar del sole, c
ol suo fascio dell'e
rba; e reca in mano
un mazzolin di rose
e di viole, onde, si
ccome suole, ornare
ella si appresta dim
ani, al dì di festa,
il petto e il crine

Convertire un numero in CHAR senza perdere zeri

Capita spesso di dover convertire un numero in carattere (CHAR) senza perdere gli zeri NON signifiativi perchè, ad esempio, dev'essere scritto su file TXT o CSV utilizzato per l'interscambio di dati con altri applicativi.
Per questo scopo esitono varie funzioni. Di seguito qualche esempio:

WITH
dati (numero) AS (
values
cast(125.7106 AS decimal(11, 5))
)

SELECT
numero,
digits(numero) AS digits,
right(REPEAT('0', 15) concat trim(char(numero)), 15) AS RIGHT,
lpad(numero, 15, '0') AS lpad,
replace(lpad(numero, 15, '0'), '.', ',') AS lpad2
FROM dati

Il risultato sarà il seguente:

NUMERO DIGITS RIGHT LPAD LPAD2
125,71060 00012571060 000000125.71060 000000125.71060 000000125,71060

Con la funzione DIGITS (colonna 2) il numero viene convertito ma perde l'eventuale separatore dei decimali. Inoltre la lunghezza finale della stringa è fissa, ovvero è la dimensione della colonna convertita se è una colonna DECIMAL, oppure ha dimensioni fisse 5, 10 o 19 nel caso sia uno SMALLINT, INTEGER o BIGINT.

Per convertire il numero senza perdere il separatore dei decimali e indicando una lunghezza fissa (ad esempio perchè richiesta nelle specifiche) si usa la funzione LPAD o la funzione RIGHT (colonne 3 e 4). LPAD è decisamente più comoda ma è stata introdotta dalla V5R4.

In entrambi i casi, il separatore dei decimali è il punto ("."), se serve che sia la virgola (",") è sufficiente utilizzare la funzione REPLACE (colonna 5).

Determinare se una colonna "carattere" contiene solo numeri

Per questo obbiettivo, nel SQL di altri database esiste la funzione ISNUMERIC. Nel DB2 ci si deve arrangiare con la fantasia.

In sostanza lo si può determinare sostituendo tutti i caratteri numerici della colonna con lo "spazio", quindi trimmare il risultato ed infine determinarne la lunghezza. Se è ZERO significa che conteneva solo caratteri numerici, viceversa significa che contiene almeno un carattere NON numerico.

Ipotizzando che la colonna di tipo "CHAR" (o VARCHAR) da testare si chiami "my_str", l'istruzione da inserire all'interno della nostra SQL è la seguente:

CASE
  WHEN LENGTH(RTRIM(TRANSLATE(my_str, ' ', '0123456789'))) = 0 
  THEN 'All digits'
  ELSE 'No'
END

se si vuole fare qualche test si può usare la seguente SQL modicando a piacimento il valore della colonna "str":

WITH
param (str) AS (
values
('189A45   '),
('84586    '),
('187/7584 '),
('5262846  ')
)

SELECT
str,
TRANSLATE(str, ' ', '0123456789') AS str_trimmata,
CASE
  WHEN LENGTH(RTRIM(TRANSLATE(str, ' ', '0123456789'))) = 0 
  THEN 'Yes'
  ELSE 'No'
END AS numerico
FROM param

Nell'esempio precedente, il risultatà sarà:

STR STR_TRIMMATA NUMERICO
189A45 A No
84586 Yes
187/7584 / No
5262846 Yes

Estrarre cifre da una colonna alfanumerica

Per raggiungere lo scopo si usa la funzione per le espressioni regolari (regexp_replace)

Esempio:

WITH
dati (str) AS (
values
('AB189A45'),
('845CDEF86'),
('18-95-67-85')
)

SELECT
str,
regexp_replace(str, '([^0-9])', '') AS cifre
FROM dati

Il risultato sarà il seguente:

STR CIFRE
AB189A45 18945
845CDEF86 84586
18-95-67-85 18956785

ATTENZIONE: se l'esecuzione vi da l'errore SQL0204 QQQSVREG in QSYS di tipo SRVPGM non trovato significa che non avete installato l'opzione 39 del sistema operativo (International Components for Unicode)

Escape di un carattere jolly

In SQL i caratteri jolly vengono utilizzati con l'operatore SQL LIKE e sono usati per sostituire qualsiasi altro carattere in una ricerca all'interno di una stringa. I più classici sono:

  • %: sostituisce zero o più caratteri
  • _: sostituisce un singolo carattere

Capita a volte che la stringa contenga uno dei caratteri jolly e si debba filtrare proprio quello, ad esempio il carattere underscore ("_").

In questo caso bisogna usare un carattere di escape per far capire al SQL che il carattere successivo è un carattere da ricercare nella stringa e NON un carattere jolly.

Per farlo si usa l'istruzione escape insieme all'operatore LIKE per indicare qual'è il carattere di escape che viene utilizzato. Ad esempio:

SELECT 
*
FROM mia_tabella
WHERE TRIM(mia_colonna_stringa) LIKE '%@_V' ESCAPE '@'

dove viene usato il carattere di escape "@" per cercare all'interno della colonna mia_colonna_stringa tutto quello che termina con "_V".

Concatenare stringhe da più righe in un'unica colonnna

Per aggregare più stringhe di righe diverse all'interno di un'unica colonna si usa la funzione LISTAGG.

Esempio: all'interno di un'ipotetica tabella di note dei DDT, in cui per ogni DDT di possono avere più righe di note, si vuole ottenere, per ogni DDT, la nota aggregata data dalla concatenazione di tutte le sue note separate da uno spazio. Il tutto si ottiene con

WITH
note (oggetto, tipo_nota, riga, nota) AS (
VALUES
('DDT00565', 'NOT', 1, 'Prima riga della nota'),
('DDT00565', 'NOT', 3, 'Terza ed ultima riga della nota'),
('DDT00565', 'NOT', 2, 'Seconda riga della nota'),
('DDT00568', 'NOT', 1, 'Decisamente'),
('DDT00568', 'NOT', 2, 'molto'),
('DDT00568', 'NOT', 3, 'semplice')
)

SELECT
oggetto, tipo_nota,
LISTAGG(TRIM(nota), ' ') WITHIN GROUP(ORDER BY riga) AS NOTA_AGGREGATA
FROM note 
GROUP BY oggetto, tipo_nota

Il risultato ottenuto sarà il seguente:

OGGETTO TIPO_NOTA NOTA_AGGREGATA
DDT00565 NOT Prima riga della nota Seconda riga della nota Terza ed ultima riga della nota
DDT00568 NOT Decisamente molto semplice

NOTE:

  • all'interno della funzione LISTAGG viene indicato l'eventuale stringa separatrice (nel nostro caso lo spazio)
  • WITHIN GROUP è facoltativo, ma se non specificato, l'ordine con cui vengono concatenate le righe all'interno del gruppo è quello sequenziale con il quale le righe si presentano, quindi, nell'esempio precedente, per il DDT 00565 la nota aggregata sarebbe diventata "Prima riga della nota Terza ed ultima riga della nota Seconda riga della nota"

Leggere un file di testo dall'IFS

Ebbene sì, con SQL è possibile leggere un file di testo direttamente dall'IFS.
Per farlo si usa la funzione IFS_READ o IFS_READ_UTF8.

Di seguito un esempio:

SELECT
* 
FROM TABLE(QSYS2.IFS_READ_UTF8(PATH_NAME => '/SMEDOC/EDI/01/test_paolo.edi'))

Il risultato mostrerà due colonne:

  • LINE_NUMBER: numero di riga
  • LINE: contenuto di ogni riga del file di testo

Contare il numero di occorrenze di una stringa all'interno di un altra stringa

Per contare quante volte è presente una stringa all'interno di un altra stringa posso usare la seguente instruzione SQL:

select REGEXP_COUNT (trim('codice articolo;descrizione;qt ordinata;UM;prezzo;'),trim(';'))
from SYSIBM.SYSDUMMY1
with NC 

con l'istruzione precedente conto quante volte è presente il carattere punto e virgola all'interno della stringa. Il risultato è 5.


Giocare con JSON

Dalla versione V7R3 del sistema operativo sono state introdotte nel DB2 dell'IBMi comode funzioni per gestire i JSON

generare una colonna con un array JSON

Esempio:

WITH
citta (nome_citta, provincia, abitanti) AS (
VALUES
('Bergamo', 'BG', 135000),
('Treviglio', 'BG', 30500),
('Brescia', 'BS', 197000),
('Caravaggio', 'BG', 16500)
)

SELECT 
json_object ('citta' value json_arrayagg ( json_object ('nome' value trim(nome_citta) , 'abitanti' value abitanti))) 
FROM citta 
WHERE provincia='BG'

il risultato sarà:

{"citta":[{"nome":"Bergamo","abitanti":135000},{"nome":"Treviglio","abitanti":30500},{"nome":"Caravaggio","abitanti":16500}]}

Parsing di un JSON

Nell'esempio seguente si farà il parsing di una stringa JSON che contiene anche un array:

WITH 
testjson AS (
SELECT  
CAST('{"team": "Atalanta", "citta": "Bergamo", "giocatori":[{"nome": "Musso", "ruolo": "P", "numero": 1}, {"nome": "Okoli", "ruolo": "D", "numero": 5},{"nome": "Pasalic", "ruolo": "C", "numero": 88},{"nome": "Zapata", "ruolo": "A", "numero": 91}] }' 
     AS varchar(32700) ) AS json_info
FROM sysibm.sysdummy1
),

righe (team, citta, sequenza, nome_giocatore, ruolo, numero_maglia) AS (
SELECT * FROM JSON_TABLE((SELECT json_info FROM testjson),
              '$'
                 COLUMNS(
                       team VARCHAR(30) PATH 'lax $.team' , 
                       citta VARCHAR(30) PATH 'lax $.citta' , 
                       NESTED PATH '$.giocatori[*]' COLUMNS ( 
                            ord FOR ORDINALITY,
                            "nome" CHAR(35), 
                            "ruolo" CHAR(1),
                            "numero" int
                       )
                 ) 
             ) as t
)

SELECT 
*
FROM righe

il risultato sarà il seguente:

TEAM CITTA SEQUENZA NOME_GIOCATORE RUOLO NUMERO_MAGLIA
Atalanta Bergamo 1 Musso P 1
Atalanta Bergamo 2 Okoli D 5
Atalanta Bergamo 3 Pasalic C 88
Atalanta Bergamo 4 Zapata A 91

SQL da programmi CL

Di seguito un esempio per eseguire istruzioni SQL all'interno di un programma CL (CLP o CLLE):

PGM

QSH CMD('DB2 "update my_table set my_col01=15 +
              where my_col02=20181225"')

ENDPGM

SQL all'interno di programmi ILE RPG

Select Into

Evitare l'istruzione FETCH di lettura con SELECT ... INTO ... se la SELECT restituisce una sola riga.
Esempio:

Exec Sql Select replace(:$_STRIN, '/', '-')
         into :$_STROUT
         from SYSIBM.SYSDUMMY1;

in questo esempio viene valorrizzata la variabile $_STROUT con il contenuto della variabile $_STRIN sostituendo eventuali caratteri "/" con "-".

Ovviamente possono essere restituite anche più colonne contemporaneamente, ad esempio:

Exec Sql Select * into :MIA_DS
         from MIA_TABELLA
         where ID=:$_ID;

dove ovviamente MIA_DS è una data structure con la struttura di tutte le colonne restituite.


FULL OUTER JOIN

La FULL OUTER JOIN serve a mettere in JOIN due o più tabelle avendo come risultato anche le righe che NON hanno corrispondenze.
Esempio:

WITH
tab1 (id1, citta, id_pr) AS (
VALUES
(1, 'Caravaggio', 1),
(2, 'Gorgonzola', 15),
(3, 'Crema', 2)
),

tab2 (id2, provincia) AS (
VALUES
(1, 'Bergamo'),
(2, 'Cremona'),
(3, 'Mantova')
)

select
a.citta,
b.provincia
from tab1 a
full outer join tab2 b ON a.id_pr=b.id2

Il risultato sarà il seguente:

CITTA PROVINCIA
Caravaggio Bergamo
Gorgonzola
Crema Cremona
Mantova

Nomi lunghi e brevi per tabelle e colonne

A volte può essere comodo gestire tabelle con nomi lunghi e chiari sia per il nome tabella che per i nomi delle colonne. Questo però causa qualche problema se si devono usare all'interno di un programma RPG perchè i nomi più lunghi di 10 caratteri vengono tagliati e resi univoci con dei progressivi incomprensibili.

In questo caso conviene creare la tabella come nell'esempio seguente usando FOR COLUMN per indicare un nome breve (alias) della colonna in modo da poterlo utilizzare nei programmi RPG:

create table my_lib.mia_tabella_lunga (
  codice_articolo FOR COLUMN cdpar char(15),
  codice_cliente FOR COLUMN cdcli char(10),
  PRIMARY KEY (codice_articolo)
) RCDFMT MyTabR

in questo modo la mia tabella si chiamerà mia_tabella_lunga in ambito SQL o linguaggi di programmazione quali Java, PHP, ecc. Ma per il sistema e per l'RPG avrà il brutto nome MIA_T00001.

Però abbiamo già ottenuto due risultati:

  • colonne con nomi lunghi e corti: il nome colonna è quello lungo ma nei programmi RPG vedo il nome corto indicato tramite "FOR COLUMN", quindi "codice_articolo" è il nome colonna che vedo in ambito SQL e numerosi linguaggi di programmazione mentre nel programma RPG (o comunque nel sistema con un semplice DSPFFD) lo vedo come "CDPAR"
  • il nome del formato record è "MYTABR" grazie all'istruzione RECFMT senza la quale il formato record assumerebbe lo stesso nome della tabella e darebbe problemi con l'RPG (in quel caso è sufficiente l'istruzione "RENAME" nelle spedifiche "F" per aggirare il problema)

Per risolvere il "problema" del nome tabella corto che è decisamente poco intuitivo, si usa il comando SQL rename table:

rename table my_lib.mia_tabella_lunga   
to system name mytab00f

in questo modo in ambito SQL la tabella resta mia_tabella_lunga mentre per il sistema e quindi nei programmi RPG viene vista come MYTAB00F.

Trovare la prima riga di un gruppo

Può capitare di avere la necessità di identificare la prima riga di un gruppo, ovvero a rottura di una o più colonne. Esempio la prima riga di ogni DDT o di ogni fattura di un certo periodo, la prima riga di un gruppo di note, ecc.

Nell'esempio seguente di identifica il primo componente in sequenza di una ipotetica distinta base:

WITH
distinta (assieme, componente, sequenza, coefficiente_impiego) AS (
VALUES
('A', 'B', '040', 1),
('A', 'C', '020', 2),
('A', 'D', '030', 1),
('C', 'E', '015', 2),
('C', 'F', '020', 1),
('F', 'G', '005', 3.5),
('F', 'H', '010', 1),
('F', 'I', '020', 1),
('F', 'J', '040', 6),
('H', 'K', '010', 3),
('H', 'L', '020', 2),
('H', 'M', '030', 1)
),

righe_numerate as (
      select assieme, componente,
      row_number() over (partition by assieme order by assieme, sequenza) as Riga
      from distinta
      )
select
      a.assieme, a.componente, a.coefficiente_impiego
      from distinta a
      join righe_numerate b on a.assieme=b.assieme 
                           and a.componente=b.componente 
                           and b.riga=1

Il risultato sarebbe il seguente:

ASSIEME COMPONENTE COEFFICIENTE_IMPIEGO
A C 2,0
C E 2,0
F G 3,5
H K 3,0

PARTITION BY: lavorare su gruppi di dati

La funzione PARTITION BY divide l'intero set di dati in più gruppi basati sui valori delle colonne desiderate permettendo di eseguire funzioni su questi gruppi invece che sull'intero set di dati.

Come sempre, gli esempi aiutano meglio a capire il significato e l'utilità di quanto descritto sopra.

Esempio:

WITH
citta (nome_citta, provincia, abitanti) AS (
VALUES
('Bergamo', 'BG', 135000),
('Treviglio', 'BG', 30500),
('Brescia', 'BS', 220000),
('Caravaggio', 'BG', 16500),
('Bedizzole', 'BS', 12300)
)

SELECT 
row_number() over (partition by provincia ORDER BY provincia, nome_citta) AS riga,
a.*,
sum(abitanti) over (partition by provincia ORDER BY provincia, nome_citta) AS progressivo
FROM citta a

Il risultato è il seguente:

RIGA NOME_CITTA PROVINCIA ABITANTI PROGRESSIVO
1 Bergamo BG 135.000 135.000
2 Caravaggio BG 16.500 151.500
3 Treviglio BG 30.500 182.000
1 Bedizzole BS 12.300 12.300
2 Brescia BS 197.000 209.300

Nell'esempio sopra, l'intero set di dati viene diviso per provincia e ordinato per provincia e nome_citta. Quindi, all'interno di questi gruppi vengono utilizzate le seguenti funzioni:

  • ROW_NUMBER per ottenere la numerazione delle righe all'interno del gruppo
  • SUM per ottenere la sommatoria progressiva all'interno del gruppo

SQL per controllare il server

A volte abbiamo la necessità di fare verifiche riguardandi lo stato dell'occupazione disco del server IBMi (oggetti, librerie, IFS) o relativamente all'allocazione di alcuni oggetti. Anche in questo caso, SQL ci viene in aiuto.
Di seguito alcuni esempi.

Dimensioni librerie ed oggetti

Un modo molto semplice per verificare la dimensione degli oggetti (*FILE, *PGM, *DTAARA, ecc.) e di conseguenza delle librerie tramite SQL è utilizzare le statistiche di sistema come fossero una tabella di database.

Esempio:

SELECT 
SUM(OBJSIZE) 
FROM TABLE (QSYS2.OBJECT_STATISTICS('mia_libreria', 'ALL')) AS A 

in questo modo ottengo l'occupazione totale della libreria "mia_libreria".

Ovviamente posso anche vedere la dimensione (e non solo) dei singoli oggetti:

SELECT 
objname,
objsize
FROM TABLE (QSYS2.OBJECT_STATISTICS('mia_libreria', 'ALL')) AS A 
ORDER BY objsize DESC 

Dimensioni IFS

Con SQL possiamo rilevare persino del dimensioni degli oggetti presenti nell'IFS (Integrated File System).
Per questo però, prima di eseguire l'SQL, abbiamo bisogno di un comando di sistema che recupera le informazioni dell'IFS e le registra in una tabella di database:

RTVDIRINF DIR(/) INFFILEPFX(IFS002) INFLIB(QGPL) OMIT('/qsys.lib' '/qntc' '/qfilesvr.400' '/qopt')

Con questo comando vengono esclusi dall'analisi alcuni percorsi (ad esempio "/qsys.lib") e vengono generate due tabelle:

  • IFS002D: riferimeni ai percorsi delle directory
  • IFS002O: dettaglio informazioni (tra cui la dimensione) per ogni oggetto. Contiene anche l'ID della directory che si lega all'ID della tabella precedente per sapere dove si trova l'oggetto

A questo punto posso usare SQL, ad esempio, per avere l'elenco degli oggetti in ordine discendente per occupazione:

Select
   Case D.QEZDIRNAM1
      When '/' Then D.QEZDIRNAM1 || O.QEZOBJNAM
      Else D.QEZDIRNAM1 || '/' || O.QEZOBJNAM
   End As "Object",
   QEZOBJTYPE As "Object Type",
   QEZALCSIZE As "Allocated Size"
 From
   QGPL.IFS002O O,
   QGPL.IFS002D D
 Where
   O.QEZDIRIDX = D.QEZDIRIDX
 Order By
   QEZALCSIZE DESC

oppure l'elenco delle directory sempre in ordine discendente per occupazione

Select
(Sum(O.QEZALCSIZE)/1048576) As Directory_Size_In_MB,
D.QEZDIRNAM1 As Directory_Path
FROM QGPL.IFS002D D 
Join QGPL.IFS002O O On D.QEZDIRIDX = O.QEZDIRIDX
Group BY D.QEZDIRIDX, QEZDIRNAM1
Order By   1 Desc, 2

ATTENZIONE: alcune colonne delle tabelle create dal comando RTVDIRINF hanno un CCSID diverso dai soliti che si usano sui server IBMi Power System, quindi se si eseguono le SQL precedenti da ambiente 5250 (ad esempio da STRSQL) si potrebbe ricevere l'errore SQL0332 - Conversione carattere tra CCSID 1200 e CCSID 65535 non valida.
In questi casti è sufficiente "castizzare" queste colonne con la funzione CAST. In quel caso, la prima istruzione SQL diventa la seguente:

Select
Case cast(D.QEZDIRNAM1 as varchar(1024) ccsid 280 )
  When '/' Then cast(D.QEZDIRNAM1 as varchar(1024) ccsid 280 ) !!
                cast(QEZOBJNAM as varchar(512) ccsid 280  )
  Else cast(D.QEZDIRNAM1 as varchar(1024) ccsid 280 ) !! 
                '/' !!
                cast(QEZOBJNAM as varchar(512) ccsid 280  )
End As "Object",
cast(QEZOBJTYPE as varchar(10) ccsid 280) As "Object Type",
QEZALCSIZE As "Allocated Size"
From QGPL.IFS002O O, QGPL.IFS002D D
Where O.QEZDIRIDX = D.QEZDIRIDX
Order By QEZALCSIZE DESC

Elenco lavori che allocano un oggetto

Con SQL si può persino avere l'elenco dei lavori che allocano un oggetto. Quello che normalmente si ottiene con il comando WRKOBJLCK. Farlo con SQL può essere utile per utilizzarlo in qualche programma particolare o per fare un controllo su più oggetti contemporaneamente.

Esempio:

SELECT 
OBJECT_NAME,
JOB_NAME
FROM qsys2.OBJECT_LOCK_INFO
WHERE OBJECT_SCHEMA='mia_libreria'
ORDER BY OBJECT_NAME 

In questo caso ottengo tutti i job che allocano gli oggetti della libreria "mia_libreria".

Interrogazione Index Advisor

Il database del nostro server IBMi, in base all'utilizzo quotidiano delle tabelle, produce continuamente statistiche con suggerimenti su indici che bisognerebbe creare per ottimizzare le performance (access plan).
Con SQL possiamo interrogare questi suggerimenti e decidere cosa farne:

select
table_name,
key_columns_advised,
index_type,
last_advised,
times_advised,
estimated_creation_time,
reason_advised
from qsys2.SYSIXADV
WHERE TABLE_SCHEMA NOT IN ('QSYS','QUSRSYS')
  and LAST_ADVISED > '2022-07-01 00:01:00.000000'
  and table_schema = 'mia_libreria'
order by times_advised desc  

con questa SQL otteniamo i suggerimenti successivi al 1 Luglio 2022 per le tabelle presenti nella libreria "mia_libreria" ordinati in modo discendente per il numero di volte che è stato generato il suggerimento.

La colonna key_columns_advised contiene l'elenco delle colonne che dovrebbero diventare la chiave dell'indice da creare.

Elenco utenti di sistema (USRPRF)

Anche per questa esigenza esiste una tabella di database con tutte le informazioni.
Esempio:

SELECT 
AUTHORIZATION_NAME AS utente,
STATUS AS stato,
PREVIOUS_SIGNON AS ultimo_login,
SIGN_ON_ATTEMPTS_NOT_VALID AS nr_login_non_validi,
TEXT_DESCRIPTION AS descrizione
FROM qsys2.user_info a
WHERE JOB_DESCRIPTION_NAME='mia_jobd'
ORDER BY AUTHORIZATION_NAME

Nell'esempio precedente vengono elencati tutti gli utenti associati ad una determinata job description indicando per ognuno lo stato, il timestamp dell'ultimo login valido, il numero dei login NON validi e la sua descrizione.

Elenco tabelle di una libreria

Per elencare le tabelle e relative informazioni di una specifica libreria usare la seguente istruzione SQL:

SELECT
TABLE_SCHEMA ,
TABLE_NAME ,
NUMBER_ROWS ,
DATA_SIZE 
from sysibm.SYSTABLESTAT 
WHERE TABLE_SCHEMA ='mia_libreria'
  AND NUMBER_ROWS >0
ORDER BY TABLE_SCHEMA , TABLE_NAME

Elenco Stack dei programmi di un lavoro

Con SQL è persino possibile leggere lo stack dei programmi di un job.

Di seguito un esempio in cui vengono filtrati solo i programmi ILE la cui libreria inizia con "SME", evitanto quindi tutti i programmi di sistema:

SELECT 
a.ORDINAL_POSITION,
a.PROGRAM_NAME,
a.statement_identifiers,
a.*
FROM TABLE(QSYS2.STACK_INFO('246408/SMEUPADM/A_MRP2','ALL')) a
WHERE ENTRY_TYPE='ILE'
  AND program_library_name LIKE 'SME%'
ORDER BY ordinal_position

Variabili di sistema

Le normali variabili di sistema che si possono gestire con il comando WRKENVVAR sono consultabili anche tramite SQL.

Ad esempio, se voglio reperire il valore della variabile "LANG" a livello del mio lavoro, posso farlo con la seguente istruzione SQL:

SELECT
ENVIRONMENT_VARIABLE_VALUE
FROM QSYS2.ENVIRONMENT_VARIABLE_INFO a
WHERE ENVIRONMENT_VARIABLE_TYPE ='JOB'
  AND ENVIRONMENT_VARIABLE_NAME ='LANG'

Il risultato sarà simile al seguente:

ENVIRONMENT_VARIABLE_VALUE
\/QSYS.LIB\/IT_IT.LOCALE

Questo è comodo anche quando nella nostra applicazione si utilizzano variabili di sistema dedicate aggiunte tramite il comando: ADDANVVAR.

Elenco comandi con default modificati (CHGCMDDFT)

Tramite la seguente SQL è possibile visualizzare l'elenco dei comandi di cui sono stati modificati i default di uno o più parametri:

SELECT OBJNAME,APAR_ID,USER_CHANGED
FROM TABLE(QSYS2.OBJECT_STATISTICS('QSYS','CMD')) 
WHERE APAR_ID = 'CHGDFT'

Molte altre funzioni che permettono di indagare ed interagire col sistema si possono trovare sul sito di IBM.

(Letto 607 volte di cui 9 negli ultimi 30gg)
twitterlinkedinmailby feather

2 thoughts on “Esempi DB2 SQL per IBMi – parte 2

  1. Che dire…. complimenti !, ottimo articolo e con funzioni più che interessanti per chi lavora con iSeries/As400.
    🙂

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *