Kontsulta paraleloak PostgreSQL-n

Kontsulta paraleloak PostgreSQL-n
CPU modernoek nukleo asko dituzte. Urteak daramatzate aplikazioek datu-baseetara kontsultak bidaltzen paraleloan. Taula bateko errenkada anitzetan txosten-kontsulta bat bada, azkarrago exekutatzen da CPU bat baino gehiago erabiltzean, eta PostgreSQL 9.6 bertsiotik aurrera egin ahal izan du.

3 urte behar izan dira kontsulta paraleloen funtzioa ezartzeko; kodea berridatzi behar izan dugu kontsultaren exekuzioaren fase desberdinetan. PostgreSQL 9.6-k azpiegitura sartu zuen kodea gehiago hobetzeko. Hurrengo bertsioetan, beste kontsulta mota batzuk exekutatzen dira paraleloan.

Murrizketak

  • Ez gaitu exekuzio paraleloa nukleo guztiak dagoeneko okupatuta badaude, bestela beste eskaerak motelduko dira.
  • Garrantzitsuena, WORK_MEM balio altuak dituen prozesatze paraleloak memoria asko erabiltzen du - hash-juntura edo ordena bakoitzak work_mem memoria hartzen du.
  • Latentzia baxuko OLTP kontsultak ezin dira bizkortu exekuzio paraleloarekin. Eta kontsultak errenkada bat itzultzen badu, prozesamendu paraleloak moteldu besterik ez du egingo.
  • Garatzaileei gustatzen zaie TPC-H erreferentzia erabiltzea. Agian antzeko kontsultak dituzu exekuzio paralelo perfekturako.
  • Predikatuen blokeorik gabeko SELECT kontsultak bakarrik exekutatzen dira paraleloan.
  • Batzuetan, indexazio egokia modu paraleloan taula sekuentziala eskaneatzea baino hobea da.
  • Ez dira onartzen kontsultak eta kurtsoreak pausatzea.
  • Leiho-funtzioak eta multzo ordenatutako funtzio agregatuak ez dira paraleloak.
  • I/O lan-kargan ez duzu ezer irabazten.
  • Ez dago ordenatzeko algoritmo paralelorik. Baina ordenak dituzten kontsultak paraleloan exekutatu daitezke alderdi batzuetan.
  • Ordeztu CTE (WITH ...) habiaratutako SELECT batekin prozesaketa paraleloa gaitzeko.
  • Hirugarrenen datu-bilgarriek oraindik ez dute prozesaketa paraleloa onartzen (baina liteke!)
  • FULL OUTER JOIN ez da onartzen.
  • max_rows-ek prozesaketa paraleloa desgaitzen du.
  • Kontsulta batek PARALLEL SAFE markatuta ez duen funtzio bat badu, hari bakarrekoa izango da.
  • SERIALIZABLE transakzio isolamendu mailak prozesamendu paraleloa desgaitzen du.

Proba ingurunea

PostgreSQL garatzaileak TPC-H erreferentziazko kontsulten erantzun-denbora murrizten saiatu ziren. Deskargatu erreferentzia eta egokitu PostgreSQLra. TPC-H erreferentzia-erabilera ez ofiziala da hau, ez datu-basea edo hardwarea alderatzeko.

  1. Deskargatu TPC-H_Tools_v2.17.3.zip (edo bertsio berriagoa) TPC-tik kanpo.
  2. Aldatu makefile.suite Makefile-ra eta aldatu hemen azaltzen den moduan: https://github.com/tvondra/pg_tpch . Konpilatu kodea make komandoarekin.
  3. Sortu datuak: ./dbgen -s 10 23 GB-ko datu-base bat sortzen du. Hau nahikoa da kontsulta paraleloen eta ez-paraleloen errendimenduan dagoen aldea ikusteko.
  4. Bihurtu fitxategiak tbl в csv с for и sed.
  5. Klonatu biltegia pg_tpch eta kopiatu fitxategiak csv в pg_tpch/dss/data.
  6. Sortu kontsultak komando batekin qgen.
  7. Kargatu datuak datu-basean komandoarekin ./tpch.sh.

Miaketa sekuentziala paraleloa

Azkarragoa izan daiteke irakurketa paraleloagatik ez, datuak CPU nukleo askotan zabaltzen direlako baizik. Sistema eragile modernoetan, PostgreSQL datu-fitxategiak ondo gordetzen dira cachean. Aurretik irakurrita, PG deabruak eskatzen duena baino bloke handiago bat lor daiteke biltegiratzetik. Hori dela eta, kontsulten errendimendua ez da diskoko I/O-k mugatzen. CPU zikloak kontsumitzen ditu:

  • irakurri taula-orrietako errenkadak banan-banan;
  • konparatu kateen balioak eta baldintzak WHERE.

Egin dezagun kontsulta sinple bat select:

tpch=# explain analyze select l_quantity as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Seq Scan on lineitem (cost=0.00..1964772.00 rows=58856235 width=5) (actual time=0.014..16951.669 rows=58839715 loops=1)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 1146337
Planning Time: 0.203 ms
Execution Time: 19035.100 ms

Eskaneatu sekuentzialak errenkada gehiegi sortzen ditu agregaziorik gabe, beraz, kontsulta PUZaren nukleo bakar batek exekutatzen du.

Gehitzen baduzu SUM(), ikus dezakezu bi lan-fluxuek kontsulta bizkortzen lagunduko dutela:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=1589702.14..1589702.15 rows=1 width=32) (actual time=8553.365..8553.365 rows=1 loops=1)
-> Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan on lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Agregazio paraleloa

Parallel Seq Scan nodoak agregazio partzialerako errenkadak sortzen ditu. "Agregazio partziala" nodoak lerro hauek mozten ditu erabiliz SUM(). Bukaeran, langile prozesu bakoitzeko SUM kontagailua "Bildu" nodoak biltzen du.

Azken emaitza "Finalizatu agregatua" nodoak kalkulatzen du. Zure agregazio-funtzioak badituzu, ez ahaztu "seguru paralelo" gisa markatzea.

Langile-prozesuen kopurua

Langile-prozesuen kopurua handitu daiteke zerbitzaria berrabiarazi gabe:

explain analyze select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=1589702.14..1589702.15 rows=1 width=32) (actual time=8553.365..8553.365 rows=1 loops=1)
-> Gather (cost=1589701.91..1589702.12 rows=2 width=32) (actual time=8553.241..8555.067 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=1588701.91..1588701.92 rows=1 width=32) (actual time=8547.546..8547.546 rows=1 loops=3)
-> Parallel Seq Scan on lineitem (cost=0.00..1527393.33 rows=24523431 width=5) (actual time=0.038..5998.417 rows=19613238 loops=3)
Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)
Rows Removed by Filter: 382112
Planning Time: 0.241 ms
Execution Time: 8555.131 ms

Zer gertatzen da hemen? 2 aldiz lan-prozesu gehiago izan ziren, eta eskaera 1,6599 aldiz azkarragoa izan zen. Kalkuluak interesgarriak dira. 2 langile prozesu eta lider 1 genituen. Aldaketaren ostean 4+1 bihurtu zen.

Prozesamendu paralelotik gure abiadura maximoa: 5/3 = 1,66 (6) aldiz.

Nola funtzionatzen du?

Prozesuak

Eskaeraren exekuzioa prozesu nagusiarekin hasten da beti. Liderrak paralelo ez den guztia eta prozesamendu paralelo batzuk egiten ditu. Eskaera berdinak egiten dituzten beste prozesu batzuei langile prozesu deitzen zaie. Prozesamendu paraleloak azpiegiturak erabiltzen ditu atzeko langile prozesu dinamikoak (9.4 bertsiotik). PostgreSQL-ren beste zati batzuek hariak baino prozesuak erabiltzen dituztenez, 3 langile-prozesu dituen kontsulta bat prozesamendu tradizionala baino 4 aldiz azkarragoa izan daiteke.

Elkarrekintza

Langile-prozesuak liderrarekin komunikatzen dira mezu-ilara baten bidez (memoria partekatuan oinarrituta). Prozesu bakoitzak 2 ilara ditu: erroreetarako eta tupleetarako.

Zenbat lan-fluxu behar dira?

Gutxieneko muga parametroak zehazten du max_parallel_workers_per_gather. Ondoren, eskaera-exekutatzaileak lan-prozesuak hartzen ditu parametroak mugatutako multzotik max_parallel_workers size. Azken muga da max_worker_processes, hau da, atzeko planoko prozesuen kopuru osoa.

Langile-prozesu bat esleitu ezinezkoa izan bada, izapidetzea prozesu bakarrekoa izango da.

Kontsulten planifikatzaileak lan-fluxuak murriztu ditzake taularen edo indizearen tamainaren arabera. Horretarako parametroak daude min_parallel_table_scan_size и min_parallel_index_scan_size.

set min_parallel_table_scan_size='8MB'
8MB table => 1 worker
24MB table => 2 workers
72MB table => 3 workers
x => log(x / min_parallel_table_scan_size) / log(3) + 1 worker

Mahaia baino 3 aldiz handiagoa den bakoitzean min_parallel_(index|table)_scan_size, Postgres-ek langile prozesu bat gehitzen du. Lan-fluxuen kopurua ez da kostuetan oinarritzen. Mendekotasun zirkularrak inplementazio konplexuak zailtzen ditu. Horren ordez, planifikatzaileak arau sinpleak erabiltzen ditu.

Praktikan, arau hauek ez dira beti egokiak ekoizpenerako, beraz, mahai jakin baterako langile-prozesuen kopurua alda dezakezu: ALTER TABLE ... SET (parallel_workers = N).

Zergatik ez da prozesaketa paraleloa erabiltzen?

Murrizketen zerrenda luzeaz gain, kostuen egiaztapenak ere badaude:

parallel_setup_cost - Eskaera laburren tratamendu paraleloa saihesteko. Parametro honek memoria prestatzeko, prozesua hasteko eta hasierako datu-trukea egiteko denbora kalkulatzen du.

parallel_tuple_cost: lider eta langileen arteko komunikazioa atzeratu daiteke lan-prozesuetako tupla kopuruaren proportzioan. Parametro honek datu-trukearen kostua kalkulatzen du.

Loop habiaratuak elkartzeak

PostgreSQL 9.6+ может выполнять вложенные циклы параллельно — это простая операция.

explain (costs off) select c_custkey, count(o_orderkey)
                from    customer left outer join orders on
                                c_custkey = o_custkey and o_comment not like '%special%deposits%'
                group by c_custkey;
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Finalize GroupAggregate
   Group Key: customer.c_custkey
   ->  Gather Merge
         Workers Planned: 4
         ->  Partial GroupAggregate
               Group Key: customer.c_custkey
               ->  Nested Loop Left Join
                     ->  Parallel Index Only Scan using customer_pkey on customer
                     ->  Index Scan using idx_orders_custkey on orders
                           Index Cond: (customer.c_custkey = o_custkey)
                           Filter: ((o_comment)::text !~~ '%special%deposits%'::text)

Bilketa azken fasean gertatzen da, beraz, Habiaratua Loop Left Join eragiketa paraleloa da. Parallel Index Only Scan 10. bertsioan bakarrik sartu zen. Serieko eskaneaketa paraleloaren antzera funtzionatzen du. Baldintza c_custkey = o_custkey bezero-kate bakoitzeko eskaera bat irakurtzen du. Beraz, ez da paraleloa.

Hash Batua

Langile prozesu bakoitzak bere hash taula sortzen du PostgreSQL 11 arte. Eta prozesu horietako lau baino gehiago badira, errendimendua ez da hobetuko. Bertsio berrian, hash taula partekatzen da. Langile prozesu bakoitzak WORK_MEM erabil dezake hash taula bat sortzeko.

select
        l_shipmode,
        sum(case
                when o_orderpriority = '1-URGENT'
                        or o_orderpriority = '2-HIGH'
                        then 1
                else 0
        end) as high_line_count,
        sum(case
                when o_orderpriority <> '1-URGENT'
                        and o_orderpriority <> '2-HIGH'
                        then 1
                else 0
        end) as low_line_count
from
        orders,
        lineitem
where
        o_orderkey = l_orderkey
        and l_shipmode in ('MAIL', 'AIR')
        and l_commitdate < l_receiptdate
        and l_shipdate < l_commitdate
        and l_receiptdate >= date '1996-01-01'
        and l_receiptdate < date '1996-01-01' + interval '1' year
group by
        l_shipmode
order by
        l_shipmode
LIMIT 1;
                                                                                                                                    QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1964755.66..1964961.44 rows=1 width=27) (actual time=7579.592..7922.997 rows=1 loops=1)
   ->  Finalize GroupAggregate  (cost=1964755.66..1966196.11 rows=7 width=27) (actual time=7579.590..7579.591 rows=1 loops=1)
         Group Key: lineitem.l_shipmode
         ->  Gather Merge  (cost=1964755.66..1966195.83 rows=28 width=27) (actual time=7559.593..7922.319 rows=6 loops=1)
               Workers Planned: 4
               Workers Launched: 4
               ->  Partial GroupAggregate  (cost=1963755.61..1965192.44 rows=7 width=27) (actual time=7548.103..7564.592 rows=2 loops=5)
                     Group Key: lineitem.l_shipmode
                     ->  Sort  (cost=1963755.61..1963935.20 rows=71838 width=27) (actual time=7530.280..7539.688 rows=62519 loops=5)
                           Sort Key: lineitem.l_shipmode
                           Sort Method: external merge  Disk: 2304kB
                           Worker 0:  Sort Method: external merge  Disk: 2064kB
                           Worker 1:  Sort Method: external merge  Disk: 2384kB
                           Worker 2:  Sort Method: external merge  Disk: 2264kB
                           Worker 3:  Sort Method: external merge  Disk: 2336kB
                           ->  Parallel Hash Join  (cost=382571.01..1957960.99 rows=71838 width=27) (actual time=7036.917..7499.692 rows=62519 loops=5)
                                 Hash Cond: (lineitem.l_orderkey = orders.o_orderkey)
                                 ->  Parallel Seq Scan on lineitem  (cost=0.00..1552386.40 rows=71838 width=19) (actual time=0.583..4901.063 rows=62519 loops=5)
                                       Filter: ((l_shipmode = ANY ('{MAIL,AIR}'::bpchar[])) AND (l_commitdate < l_receiptdate) AND (l_shipdate < l_commitdate) AND (l_receiptdate >= '1996-01-01'::date) AND (l_receiptdate < '1997-01-01 00:00:00'::timestamp without time zone))
                                       Rows Removed by Filter: 11934691
                                 ->  Parallel Hash  (cost=313722.45..313722.45 rows=3750045 width=20) (actual time=2011.518..2011.518 rows=3000000 loops=5)
                                       Buckets: 65536  Batches: 256  Memory Usage: 3840kB
                                       ->  Parallel Seq Scan on orders  (cost=0.00..313722.45 rows=3750045 width=20) (actual time=0.029..995.948 rows=3000000 loops=5)
 Planning Time: 0.977 ms
 Execution Time: 7923.770 ms

TPC-H-ko 12. kontsultak argi eta garbi erakusten du hash konexio paraleloa. Langile prozesu bakoitzak hash taula komun bat sortzen laguntzen du.

Batu bat egin

Fusion juntadurak izaera ez du paraleloa. Ez kezkatu hau kontsultaren azken urratsa bada - paraleloan exekutatu daiteke oraindik.

-- Query 2 from TPC-H
explain (costs off) select s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
from    part, supplier, partsupp, nation, region
where
        p_partkey = ps_partkey
        and s_suppkey = ps_suppkey
        and p_size = 36
        and p_type like '%BRASS'
        and s_nationkey = n_nationkey
        and n_regionkey = r_regionkey
        and r_name = 'AMERICA'
        and ps_supplycost = (
                select
                        min(ps_supplycost)
                from    partsupp, supplier, nation, region
                where
                        p_partkey = ps_partkey
                        and s_suppkey = ps_suppkey
                        and s_nationkey = n_nationkey
                        and n_regionkey = r_regionkey
                        and r_name = 'AMERICA'
        )
order by s_acctbal desc, n_name, s_name, p_partkey
LIMIT 100;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Limit
   ->  Sort
         Sort Key: supplier.s_acctbal DESC, nation.n_name, supplier.s_name, part.p_partkey
         ->  Merge Join
               Merge Cond: (part.p_partkey = partsupp.ps_partkey)
               Join Filter: (partsupp.ps_supplycost = (SubPlan 1))
               ->  Gather Merge
                     Workers Planned: 4
                     ->  Parallel Index Scan using <strong>part_pkey</strong> on part
                           Filter: (((p_type)::text ~~ '%BRASS'::text) AND (p_size = 36))
               ->  Materialize
                     ->  Sort
                           Sort Key: partsupp.ps_partkey
                           ->  Nested Loop
                                 ->  Nested Loop
                                       Join Filter: (nation.n_regionkey = region.r_regionkey)
                                       ->  Seq Scan on region
                                             Filter: (r_name = 'AMERICA'::bpchar)
                                       ->  Hash Join
                                             Hash Cond: (supplier.s_nationkey = nation.n_nationkey)
                                             ->  Seq Scan on supplier
                                             ->  Hash
                                                   ->  Seq Scan on nation
                                 ->  Index Scan using idx_partsupp_suppkey on partsupp
                                       Index Cond: (ps_suppkey = supplier.s_suppkey)
               SubPlan 1
                 ->  Aggregate
                       ->  Nested Loop
                             Join Filter: (nation_1.n_regionkey = region_1.r_regionkey)
                             ->  Seq Scan on region region_1
                                   Filter: (r_name = 'AMERICA'::bpchar)
                             ->  Nested Loop
                                   ->  Nested Loop
                                         ->  Index Scan using idx_partsupp_partkey on partsupp partsupp_1
                                               Index Cond: (part.p_partkey = ps_partkey)
                                         ->  Index Scan using supplier_pkey on supplier supplier_1
                                               Index Cond: (s_suppkey = partsupp_1.ps_suppkey)
                                   ->  Index Scan using nation_pkey on nation nation_1
                                         Index Cond: (n_nationkey = supplier_1.s_nationkey)

"Batu batu" nodoa "Bildu bateratzea"ren gainean dago. Beraz, bat egiteak ez du prozesamendu paraleloa erabiltzen. Baina "Indize paraleloen eskaneatzea" nodoak oraindik ere laguntzen du segmentuarekin part_pkey.

Atalen araberako konexioa

PostgreSQL 11n atalen araberako konexioa lehenespenez desgaituta: programazio oso garestia du. Partizio antzekoa duten taulak partizioz partizio elkartu daitezke. Modu honetan Postgres-ek hash taula txikiagoak erabiliko ditu. Atalen konexio bakoitza paraleloa izan daiteke.

tpch=# set enable_partitionwise_join=t;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                    QUERY PLAN
---------------------------------------------------
 Append
   ->  Hash Join
         Hash Cond: (t2.b = t1.a)
         ->  Seq Scan on prt2_p1 t2
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p1 t1
                     Filter: (b = 0)
   ->  Hash Join
         Hash Cond: (t2_1.b = t1_1.a)
         ->  Seq Scan on prt2_p2 t2_1
               Filter: ((b >= 0) AND (b <= 10000))
         ->  Hash
               ->  Seq Scan on prt1_p2 t1_1
                     Filter: (b = 0)
tpch=# set parallel_setup_cost = 1;
tpch=# set parallel_tuple_cost = 0.01;
tpch=# explain (costs off) select * from prt1 t1, prt2 t2
where t1.a = t2.b and t1.b = 0 and t2.b between 0 and 10000;
                        QUERY PLAN
-----------------------------------------------------------
 Gather
   Workers Planned: 4
   ->  Parallel Append
         ->  Parallel Hash Join
               Hash Cond: (t2_1.b = t1_1.a)
               ->  Parallel Seq Scan on prt2_p2 t2_1
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p2 t1_1
                           Filter: (b = 0)
         ->  Parallel Hash Join
               Hash Cond: (t2.b = t1.a)
               ->  Parallel Seq Scan on prt2_p1 t2
                     Filter: ((b >= 0) AND (b <= 10000))
               ->  Parallel Hash
                     ->  Parallel Seq Scan on prt1_p1 t1
                           Filter: (b = 0)

Gauza nagusia da atalen konexioa paraleloa dela atal horiek nahikoa handiak badira.

Eranskin Paraleloa

Eranskin Paraleloa bloke ezberdinen ordez erabil daiteke lan-fluxu desberdinetan. Hau normalean UNION ALL kontsultekin gertatzen da. Desabantaila paralelismo txikiagoa da, langile prozesu bakoitzak eskaera bakarra prozesatzen duelako.

2 lan-prozesu daude martxan hemen, 4 gaituta dauden arren.

tpch=# explain (costs off) select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '1998-12-01' - interval '105' day union all select sum(l_quantity) as sum_qty from lineitem where l_shipdate <= date '2000-12-01' - interval '105' day;
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Gather
   Workers Planned: 2
   ->  Parallel Append
         ->  Aggregate
               ->  Seq Scan on lineitem
                     Filter: (l_shipdate <= '2000-08-18 00:00:00'::timestamp without time zone)
         ->  Aggregate
               ->  Seq Scan on lineitem lineitem_1
                     Filter: (l_shipdate <= '1998-08-18 00:00:00'::timestamp without time zone)

Aldagai garrantzitsuenak

  • WORK_MEMek memoria mugatzen du prozesu bakoitzeko, ez soilik kontsultak: work_mem prozesuak konexioak = memoria asko.
  • max_parallel_workers_per_gather — Programa exekutatzaileak planaren prozesamendu paralelorako zenbat langile-prozesu erabiliko dituen.
  • max_worker_processes — Langile-prozesuen guztizko kopurua zerbitzariko CPU-nukleoen kopurura doitzen du.
  • max_parallel_workers - berdin, baina lan prozesu paraleloetarako.

Emaitzak

9.6 bertsiotik aurrera, prozesatze paraleloak errenkada edo indize asko eskaneatzen dituzten kontsulta konplexuen errendimendua asko hobe dezake. PostgreSQL 10-n, prozesaketa paraleloa gaituta dago lehenespenez. Gogoratu OLTP lan karga handia duten zerbitzarietan desgaitzen duzula. Miaketa sekuentzialak edo indizeen azterketak baliabide asko kontsumitzen ditu. Datu-multzo osoari buruzko txostenik ez baduzu exekutatzen, kontsultaren errendimendua hobe dezakezu falta diren indizeak gehituz edo partizio egokia erabiliz.

Erreferentziak

Iturria: www.habr.com

Gehitu iruzkin berria