Sì, sembra un problema molto generico, ma non sono stato ancora in grado di restringerlo molto.
Quindi ho una dichiarazione UPDATE in un file batch sql:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
B ha 40k record, A ha 4M record e sono correlati da 1 a n tramite A.B_ID, sebbene non ci sia FK tra i due.
Quindi sostanzialmente sto pre-calcolando un campo per scopi di data mining. Anche se ho cambiato il nome delle tabelle per questa domanda, non ho cambiato la dichiarazione, è davvero così semplice.
Questa operazione richiede ore, quindi ho deciso di annullare tutto. Il DB è stato danneggiato, quindi l'ho eliminato, ripristinato un backup che ho fatto poco prima di eseguire l'istruzione e ho deciso di approfondire i dettagli con un cursore:
DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
RAISERROR(@Msg, 10, 1) WITH NOWAIT
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = @Id
FETCH NEXT FROM CursorB INTO @Id
END
Ora posso vederlo in esecuzione con un messaggio con l'id decrescente. Quello che succede è che ci vogliono circa 5 minuti per passare da id = 40k a id = 13
E poi a id 13, per qualche motivo, sembra bloccarsi. Il DB non ha alcuna connessione ad esso oltre a SSMS, ma in realtà non è impiccato:
- il disco rigido funziona continuamente, quindi sta sicuramente facendo qualcosa (ho verificato in Process Explorer che è effettivamente il processo sqlserver.exe che lo utilizza)
Ho eseguito sp_who2, ho trovato lo SPID (70) della sessione SUSPENDED, quindi ho eseguito il seguente script:
seleziona * da sys.dm_exec_requests r unisciti sys.dm_os_tasks t su r.session_id = t.session_id dove r.session_id = 70
Questo mi dà il wait_type, che è PAGEIOLATCH_SH il più delle volte ma in realtà cambia a WRITE_COMPLETION a volte, il che immagino accade quando sta svuotando il registro
- il file di registro, che era 1,6 GB quando ho ripristinato il DB (e quando è arrivato a id 13), ora è 3,5 GB
Altre informazioni forse utili:
- il numero di record nella tabella A per B_ID 13 non è elevato (14)
- La mia collega non ha lo stesso problema sulla sua macchina, con una copia di questo DB (di un paio di mesi fa) con la stessa struttura.
- la tabella A è di gran lunga la più grande tabella nel DB
- Ha diversi indici e diverse viste indicizzate lo utilizzano.
- Non ci sono altri utenti nel DB, è locale e nessuna applicazione lo sta usando.
- Il file LDF non ha dimensioni limitate.
- Il modello di recupero è SEMPLICE, il livello di compatibilità è 100
- Procmon non mi fornisce molte informazioni: sqlserver.exe legge e scrive molto dai file MDF e LDF.
Sto ancora aspettando che finisca (sono passate le 1h30) ma speravo che forse qualcuno mi avrebbe dato qualche altra azione per provare a risolvere questo problema.
Modificato: aggiunta di estratto dal registro di procmon
15:24:02.0506105 sqlservr.exe 1760 ReadFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal
Dall'uso di DBCC PAGE sembra che si stia leggendo e scrivendo in campi che assomigliano alla tabella A (o a uno dei suoi indici), ma per B_ID diversi che 13. Forse la ricostruzione degli indici?
Modificato 2: piano di esecuzione
Quindi ho annullato la query (effettivamente cancellato il DB e i suoi file e poi ripristinato), e ho controllato il piano di esecuzione per:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13
Il piano di esecuzione (stimato) è lo stesso di qualsiasi B.ID e sembra abbastanza semplice. La clausola WHERE utilizza una ricerca indice su un indice non cluster di B, JOIN utilizza una ricerca indice cluster su entrambi i PK delle tabelle. La ricerca dell'indice cluster su A utilizza il parallelismo (x7) e rappresenta il 90% del tempo della CPU.
Ancora più importante, l'esecuzione effettiva della query con ID 13 è immediata.
Modificato 3: frammentazione dell'indice
La struttura degli indici è la seguente:
B ha un PK cluster (non il campo ID) e un indice univoco non cluster, il cui primo campo è B.ID - questo secondo indice sembra essere usato sempre.
A ha un PK cluster (campo non correlato).
Ci sono anche 7 viste su A (tutte includono il campo AX), ognuna con il proprio PK cluster e un altro indice che include anche il campo AX
Le viste sono filtrate (con campi che non sono in questa equazione), quindi dubito che ci sia un modo in cui AGGIORNAMENTO A userebbe le viste stesse. Ma hanno un indice che include AX, quindi cambiare AX significa scrivere le 7 viste e i 7 indici che hanno quel campo.
Sebbene l'AGGIORNAMENTO dovrebbe essere più lento per questo, non c'è motivo per cui un ID specifico sia molto più lungo degli altri.
Ho verificato la frammentazione di tutti gli indici, tutti erano <0,1%, ad eccezione degli indici secondari delle viste , tutti compresi tra il 25% e il 50%. I fattori di riempimento per tutti gli indici sembrano ok, tra il 90% e il 95%.
Ho riorganizzato tutti gli indici secondari e riproposto la mia sceneggiatura.
È ancora impiccato, ma in un punto diverso:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Considerando che in precedenza, il registro messaggi appariva così:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Updating A for B_ID=13
Questo è strano, perché significa che non è nemmeno impiccato nello stesso punto del WHILE
loop. Il resto ha lo stesso aspetto: stessa riga UPDATE in attesa in sp_who2, stesso tipo di attesa PAGEIOLATCH_EX e stesso utilizzo intenso di HD da sqlserver.exe.
Il prossimo passo è eliminare tutti gli indici e le viste e ricrearli, credo.
Modificato 4: eliminazione quindi ricostruzione degli indici
Quindi, ho eliminato tutte le viste indicizzate che avevo sulla tabella (7 di esse, 2 indici per vista, inclusa quella raggruppata). Ho eseguito lo script iniziale (senza cursore) e in realtà è stato eseguito in 5 minuti.
Quindi il mio problema deriva dall'esistenza di questi indici.
Ho ricreato i miei indici dopo aver eseguito l'aggiornamento e ci sono voluti 16 minuti.
Ora capisco che gli indici richiedono tempo per essere ricostruiti, e in realtà sto bene con l'operazione completa che richiede 20 minuti.
Quello che ancora non capisco è, perché quando eseguo l'aggiornamento senza eliminare prima gli indici, ci vogliono diverse ore, ma quando li elimino per primi e poi li ricrea, ci vogliono 20 minuti. Non dovrebbe impiegare all'incirca la stessa ora in entrambi i modi?
DBCC PAGE
per vedere su cosa viene scritto.