Perché in questo caso specifico l'utilizzo di una variabile di tabella è due volte più veloce di una tabella #temp?


37

Stavo guardando l'articolo qui Tabelle temporanee contro variabili di tabella e il loro effetto sulle prestazioni di SQL Server e su SQL Server 2008 è stato in grado di riprodurre risultati simili a quelli mostrati lì per il 2005.

Quando si eseguono le stored procedure (definizioni di seguito) con solo 10 righe, la versione della variabile della tabella in uscita esegue la versione della tabella temporanea per più di due volte.

Ho cancellato la cache delle procedure ed eseguito entrambe le procedure memorizzate 10.000 volte, quindi ho ripetuto il processo per altre 4 esecuzioni. Risultati di seguito (tempo in ms per batch)

T2_Time     V2_Time
----------- -----------
8578        2718      
6641        2781    
6469        2813   
6766        2797
6156        2719

La mia domanda è: qual è la ragione per la migliore prestazione della versione variabile della tabella?

Ho fatto qualche indagine. ad es. guardando i contatori delle prestazioni con

SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';

conferma che in entrambi i casi gli oggetti temporanei vengono memorizzati nella cache dopo la prima esecuzione come previsto anziché essere nuovamente creati da zero per ogni chiamata.

Analogamente il rintracciamento Auto Stats, SP:Recompile, SQL:StmtRecompileeventi in Profiler (schermata qui sotto) mostra che tali eventi si verificano solo una volta (la prima chiamata della #tempprocedura memorizzata tabella) e gli altri 9.999 esecuzioni non sollevare uno di questi eventi. (La versione della variabile della tabella non ottiene nessuno di questi eventi)

Tracciare

L'overhead leggermente maggiore della prima esecuzione della procedura memorizzata non può in alcun modo tenere conto della grande differenza complessiva, tuttavia, poiché bastano ancora pochi ms per svuotare la cache delle procedure ed eseguire entrambe le procedure una volta, quindi non credo che le statistiche o la ricompilazione può essere la causa.

Creare oggetti di database richiesti

CREATE DATABASE TESTDB_18Feb2012;

GO

USE TESTDB_18Feb2012;

CREATE TABLE NUM 
  ( 
     n INT PRIMARY KEY, 
     s VARCHAR(128) 
  ); 

WITH NUMS(N) 
     AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0) 
         FROM   master..spt_values v1, 
                master..spt_values v2) 
INSERT INTO NUM 
SELECT N, 
       'Value: ' + CONVERT(VARCHAR, N) 
FROM   NUMS 

GO

CREATE PROCEDURE [dbo].[T2] @total INT 
AS 
  CREATE TABLE #T 
    ( 
       n INT PRIMARY KEY, 
       s VARCHAR(128) 
    ) 

  INSERT INTO #T 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   #T 
                        WHERE  #T.n = NUM.n) 
GO

CREATE PROCEDURE [dbo].[V2] @total INT 
AS 
  DECLARE @V TABLE ( 
    n INT PRIMARY KEY, 
    s VARCHAR(128)) 

  INSERT INTO @V 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   @V V 
                        WHERE  V.n = NUM.n) 


GO

Test script

SET NOCOUNT ON;

DECLARE @T1 DATETIME2,
        @T2 DATETIME2,
        @T3 DATETIME2,  
        @Counter INT = 0

SET @T1 = SYSDATETIME()

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END

SET @T2 = SYSDATETIME()
SET @Counter = 0

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END

SET @T3 = SYSDATETIME()

SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
       DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time

La traccia del profiler indica che le statistiche vengono create sulla #temptabella solo una volta nonostante vengano cancellate e ripopolate altre 9.999 volte successive.
Martin Smith,

Risposte:


31

L'output di SET STATISTICS IO ONper entrambi sembra simile

SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10

V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3

E come sottolinea Aaron nei commenti, il piano per la versione della variabile della tabella è in realtà meno efficiente poiché mentre entrambi hanno un piano di cicli nidificati guidato da una ricerca dell'indice sulla versione dbo.NUMdella #temptabella esegue una ricerca nell'indice [#T].n = [dbo].[NUM].[n]con predicato residuo [#T].[n]<=[@total]mentre la variabile della tabella la versione esegue una ricerca dell'indice @V.n <= [@total]con predicato residuo @V.[n]=[dbo].[NUM].[n]e quindi elabora più righe (motivo per cui questo piano ha prestazioni così scarse per un numero maggiore di righe)

L'utilizzo di Eventi estesi per esaminare i tipi di attesa per lo spid specifico fornisce questi risultati per 10.000 esecuzioni diEXEC dbo.T2 10

+---------------------+------------+----------------+----------------+----------------+
|                     |            |     Total      | Total Resource |  Total Signal  |
| Wait Type           | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16         | 19             | 19             | 0              |
| PAGELATCH_SH        | 39998      | 14             | 0              | 14             |
| PAGELATCH_EX        | 1          | 0              | 0              | 0              |
+---------------------+------------+----------------+----------------+----------------+

e questi risultati per 10.000 esecuzioni di EXEC dbo.V2 10

+---------------------+------------+----------------+----------------+----------------+
|                     |            |     Total      | Total Resource |  Total Signal  |
| Wait Type           | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX        | 2          | 0              | 0              | 0              |
| PAGELATCH_SH        | 1          | 0              | 0              | 0              |
| SOS_SCHEDULER_YIELD | 676        | 0              | 0              | 0              |
+---------------------+------------+----------------+----------------+----------------+

Quindi è chiaro che il numero di PAGELATCH_SHattese è molto più elevato nel #tempcaso della tabella. Non sono a conoscenza di alcun modo per aggiungere la risorsa wait alla traccia degli eventi estesi, quindi per approfondire ulteriormente ho eseguito

WHILE 1=1
EXEC dbo.T2 10

Mentre in un'altra connessione polling sys.dm_os_waiting_tasks

CREATE TABLE #T(resource_description NVARCHAR(2048))

WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'

Dopo aver lasciato la corsa per circa 15 secondi, aveva raccolto i seguenti risultati

+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
|  1098 | 2:1:150              |
|  1689 | 2:1:146              |
+-------+----------------------+

Entrambe queste pagine bloccate appartengono a (diversi) indici non cluster sulla tempdb.sys.sysschobjstabella di base denominata 'nc1'e 'nc2'.

Le query tempdb.sys.fn_dblogdurante le esecuzioni indicano che il numero di record di registro aggiunti dalla prima esecuzione di ciascuna procedura memorizzata era in qualche modo variabile ma per le esecuzioni successive il numero aggiunto da ciascuna iterazione era molto coerente e prevedibile. Una volta memorizzati nella cache i piani delle procedure, il numero di voci del registro è circa la metà di quelle necessarie per la #tempversione.

+-----------------+----------------+------------+
|                 | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run       |            126 | 72 or 136  |
| Subsequent Runs |             17 | 32         |
+-----------------+----------------+------------+

Esaminando le voci del registro delle transazioni in modo più dettagliato per la #tempversione della tabella dell'SP ogni successiva chiamata della procedura memorizzata crea tre transazioni e la variabile della tabella solo due.

+---------------------------------+----+---------------------------------+----+
|           #Temp Table                |         @Table Variable              |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE                    |  9 |                                 |    |
| INSERT                          | 12 | TVQuery                         | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable |  5 |
+---------------------------------+----+---------------------------------+----+

Le transazioni INSERT/ TVQUERYsono identiche ad eccezione del nome. Questo contiene i record di registro per ciascuna delle 10 righe inserite nella tabella temporanea o variabile della tabella più le voci LOP_BEGIN_XACT/ LOP_COMMIT_XACT.

La CREATE TABLEtransazione appare solo nella #Tempversione e ha il seguente aspetto.

+-----------------+-------------------+---------------------+
|    Operation    |      Context      |    AllocUnitName    |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT  | LCX_NULL          |                     |
| LOP_SHRINK_NOOP | LCX_NULL          |                     |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1  |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc1  |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2  |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc2  |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL          |                     |
+-----------------+-------------------+---------------------+

La FCheckAndCleanupCachedTempTabletransazione appare in entrambi ma ha 6 voci aggiuntive nella #tempversione. Queste sono le 6 righe che fanno riferimento sys.sysschobjse hanno esattamente lo stesso modello di cui sopra.

+-----------------+-------------------+----------------------------------------------+
|    Operation    |      Context      |                AllocUnitName                 |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT  | LCX_NULL          |                                              |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT  | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA  | LCX_NULL          |                                              |
| LOP_HOBT_DELTA  | LCX_NULL          |                                              |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst                          |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1                           |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc1                           |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2                           |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF    | sys.sysschobjs.nc2                           |
| LOP_MODIFY_ROW  | LCX_CLUSTERED     | sys.sysschobjs.clst                          |
| LOP_COMMIT_XACT | LCX_NULL          |                                              |
+-----------------+-------------------+----------------------------------------------+

Guardando queste 6 righe in entrambe le transazioni corrispondono alle stesse operazioni. Il primo LOP_MODIFY_ROW, LCX_CLUSTEREDè un aggiornamento della modify_datecolonna in sys.objects. Le restanti cinque righe riguardano tutte la ridenominazione degli oggetti. Poiché nameè una colonna chiave di entrambi gli NCI interessati ( nc1e nc2), questo viene eseguito come eliminazione / inserimento per quelli, quindi torna all'indice cluster e lo aggiorna.

Sembra che per la #tempversione della tabella quando la procedura memorizzata termina parte del cleanup effettuato dalla FCheckAndCleanupCachedTempTabletransazione sia rinominare la tabella temporanea da qualcosa di simile #T__________________________________________________________________________________________________________________00000000E316a un nome interno diverso come #2F4A0079e quando viene inserita la CREATE TABLEtransazione la rinomina nuovamente. Questo nome di flip flop può essere visto da una connessione in esecuzione dbo.T2in un ciclo mentre in un'altra

WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects 
WHERE name LIKE '#%'

Risultati di esempio

Immagine dello schermo

Quindi una potenziale spiegazione per il differenziale prestazionale osservato come accennato da Alex è che è questo lavoro aggiuntivo a mantenere tempdbresponsabili le tabelle di sistema .


Eseguendo entrambe le procedure in un ciclo il profiler di Visual Studio Code rivela quanto segue

+-------------------------------+--------------------+-------+-----------+
|           Function            |    Explanation     | Temp  | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute        | Insert ... Select  | 16.93 | 37.31     |
| CXStmtQuery::ErsqExecuteQuery | Select Max         | 8.77  | 23.19     |
+-------------------------------+--------------------+-------+-----------+
| Total                         |                    | 25.7  | 60.5      |
+-------------------------------+--------------------+-------+-----------+

La versione della variabile della tabella impiega circa il 60% del tempo in cui viene eseguita l'istruzione insert e la selezione successiva, mentre la tabella temporanea è inferiore alla metà. Ciò è in linea con i tempi mostrati nel PO e con la conclusione sopra che la differenza nelle prestazioni è dovuta al tempo impiegato nell'esecuzione di lavori accessori non dovuto al tempo impiegato nell'esecuzione della query stessa.

Le funzioni più importanti che contribuiscono al "mancante" 75% nella versione della tabella temporanea sono

+------------------------------------+-------------------+
|              Function              | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute  | 26.26%            |
| CXStmtDDL::FinishNormalImp         | 4.17%             |
| TmpObject::Release                 | 27.77%            |
+------------------------------------+-------------------+
| Total                              | 58.20%            |
+------------------------------------+-------------------+

In entrambe le funzioni di creazione e rilascio, la funzione CMEDProxyObject::SetNameviene mostrata con un valore di esempio inclusivo di 19.6%. Da cui deduco che il 39,2% delle volte nel caso della tabella temporanea è occupato con la ridenominazione descritta in precedenza.

E quelli più grandi nella versione variabile della tabella che contribuiscono all'altro 40% lo sono

+-----------------------------------+-------------------+
|             Function              | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate             | 7.41%             |
| TmpObject::Release                | 12.87%            |
+-----------------------------------+-------------------+
| Total                             | 20.28%            |
+-----------------------------------+-------------------+

Profilo della tabella temporanea

inserisci qui la descrizione dell'immagine

Profilo variabile tabella

inserisci qui la descrizione dell'immagine


10

Disco Inferno

Poiché questa è una domanda più vecchia, ho deciso di rivisitare il problema sulle versioni più recenti di SQL Server per vedere se esiste ancora lo stesso profilo delle prestazioni o se le caratteristiche sono cambiate del tutto.

In particolare, l'aggiunta di tabelle di sistema in memoria per SQL Server 2019 sembra un'occasione utile per riprovare.

Sto usando un cablaggio di prova leggermente diverso, poiché ho riscontrato questo problema mentre lavoravo su qualcos'altro.

Test, test

Utilizzando la versione 2013 di Stack Overflow , ho questo indice e queste due procedure:

Indice:

CREATE INDEX ix_whatever 
    ON dbo.Posts(OwnerUserId) INCLUDE(Score);
GO

Tabella temporanea:

    CREATE OR ALTER PROCEDURE dbo.TempTableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        CREATE TABLE #t(i INT NOT NULL);
        DECLARE @i INT;

        INSERT #t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM #t AS t;

    END;
    GO 

Variabile di tabella:

    CREATE OR ALTER PROCEDURE dbo.TableVariableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        DECLARE @t TABLE (i INT NOT NULL);
        DECLARE @i INT;

        INSERT @t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM @t AS t;

    END;
    GO 

Per evitare qualsiasi potenziale attesa di ASYNC_NETWORK_IO , sto usando le procedure wrapper.

CREATE PROCEDURE #TT AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TempTableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

CREATE PROCEDURE #TV AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TableVariableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

SQL Server 2017

Dal 2014 e 2016 sono fondamentalmente RELICS a questo punto, sto iniziando i miei test con il 2017. Inoltre, per brevità, sto saltando a destra per profilare il codice con Perfview . Nella vita reale, ho guardato ad attese, chiavistelli, spinlock, pazze bandiere traccia e altre cose.

La profilazione del codice è l'unica cosa che ha rivelato qualcosa di interessante.

Differenza di tempo:

  • Tabella delle temp: 17891 ms
  • Variabile di tabella: 5891 ms

C'è ancora una differenza molto chiara, eh? Ma cosa sta colpendo SQL Server ora?

NOCCIOLINE

Guardando i primi due aumenti nei campioni diffusi, vediamo sqlmine sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketsiamo i due più grandi trasgressori.

NOCCIOLINE

A giudicare dai nomi nelle pile di chiamate, ripulire e rinominare internamente le tabelle temporanee sembra essere il tempo più grande che risucchia nella chiamata della tabella temporanea rispetto alla chiamata della variabile della tabella.

Anche se le variabili di tabella sono supportate internamente da tabelle temporanee, questo non sembra essere un problema.

SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;

Tabella '# B98CE339'. Conteggio scansioni 1

Esaminare gli stack di chiamate per il test delle variabili della tabella non mostra affatto nessuno dei principali autori del reato:

NOCCIOLINE

SQL Server 2019 (Vanilla)

Bene, quindi questo è ancora un problema in SQL Server 2017, c'è qualcosa di diverso nel 2019 pronto all'uso?

Innanzitutto, per mostrare che non c'è niente nella manica:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

NOCCIOLINE

Differenza di tempo:

  • Tabella delle temp: 15765 ms
  • Variabile di tabella: 7250 ms

Entrambe le procedure erano diverse. La chiamata della tabella temporanea era un paio di secondi più veloce e la chiamata della variabile della tabella era più lenta di circa 1,5 secondi. Il rallentamento della variabile di tabella può essere parzialmente spiegato dalla compilazione differita della variabile di tabella , una nuova scelta di ottimizzatore nel 2019.

Guardando il diff in Perfview, è cambiato un po '- sqlmin non è più lì - ma lo sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketè.

NOCCIOLINE

SQL Server 2019 (tabelle di sistema Tempdb in memoria)

Che dire di questa novità nella tabella dei sistemi di memoria? Hm? Sup con quello?

Accendiamolo!

EXEC sys.sp_configure @configname = 'advanced', 
                      @configvalue = 1  
RECONFIGURE;

EXEC sys.sp_configure @configname = 'tempdb metadata memory-optimized', 
                      @configvalue = 1 
RECONFIGURE;

Nota che questo richiede un riavvio di SQL Server per iniziare, quindi scusami mentre riavvio SQL in questo delizioso venerdì pomeriggio.

Ora le cose sembrano diverse:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

SELECT *, 
       OBJECT_NAME(object_id) AS object_name, 
       @@VERSION AS sql_server_version
FROM tempdb.sys.memory_optimized_tables_internal_attributes;

NOCCIOLINE

Differenza di tempo:

  • Tabella delle temp: 11638 ms
  • Variabile di tabella: 7403 ms

Le tabelle temporanee hanno fatto circa 4 secondi meglio! È qualcosa.

Mi piace qualcosa

Questa volta, la differenza Perfview non è molto interessante. Fianco a fianco, è interessante notare quanto i tempi siano vicini su tutta la linea:

NOCCIOLINE

Un punto interessante nel diff sono le chiamate a hkengine!, che possono sembrare ovvie poiché le funzionalità di hekaton sono ora in uso.

NOCCIOLINE

Per quanto riguarda i primi due elementi del diff, non posso fare molto ntoskrnl!?:

NOCCIOLINE

Oppure sqltses!CSqlSortManager_80::GetSortKey, ma sono qui per Smrtr Ppl ™ a guardare:

NOCCIOLINE

Nota che esiste un documento non documentato e sicuramente non sicuro per la produzione, quindi per favore non usarlo flag di traccia di avvio che puoi usare per avere oggetti di sistema di tabelle temporanee aggiuntive (sysrowset, sysallocunits e sysseobjvalues) inclusi nella funzione in-memory, ma in questo caso non ha fatto alcuna differenza nei tempi di esecuzione.

Arrotondare

Anche nelle versioni più recenti di SQL Server, le chiamate ad alta frequenza alle variabili della tabella sono molto più veloci delle chiamate ad alta frequenza alle tabelle temporanee.

Anche se è allettante dare la colpa a compilazioni, ricompilazioni, statistiche automatiche, latch, spinlock, memorizzazione nella cache o altri problemi, il problema è chiaramente ancora legato alla gestione della pulizia della tabella temporanea.

È una chiamata più vicina in SQL Server 2019 con le tabelle di sistema in memoria abilitate, ma le variabili di tabella continuano a funzionare meglio quando la frequenza delle chiamate è elevata.

Certo, come rifletteva una volta un saggio di svapo: "usa le variabili di tabella quando la scelta del piano non è un problema".


Bene - mi dispiace che mi sia sfuggito che hai aggiunto una risposta a questa fino a quando non hai seguito il link nel tuo post sul blog "debugging"
Martin Smith
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.