La presenza del campo XML fa sì che la maggior parte dei dati della tabella si trovi nelle pagine LOB_DATA (in effetti ~ 90% delle pagine della tabella sono LOB_DATA).
Avere semplicemente la colonna XML nella tabella non ha questo effetto. È la presenza di dati XML che, in determinate condizioni , fa sì che una parte dei dati di una riga venga archiviata su una riga, nelle pagine LOB_DATA. E mentre uno (o forse più ;-) potrebbe sostenere che duh, la XML
colonna implica che ci saranno davvero dati XML, non è garantito che i dati XML debbano essere archiviati off line: a meno che la riga sia praticamente già piena al di fuori del loro essere qualsiasi dato XML, piccoli documenti (fino a 8000 byte) potrebbero adattarsi in fila e non andare mai a una pagina LOB_DATA.
Sono corretto nel pensare che le pagine LOB_DATA possono causare scansioni lente non solo a causa delle loro dimensioni, ma anche perché SQL Server non può scansionare efficacemente l'indice cluster quando ci sono molte pagine LOB_DATA nella tabella?
La scansione si riferisce all'osservazione di tutte le righe. Naturalmente, quando viene letta una pagina di dati, vengono letti tutti i dati in riga , anche se è stato selezionato un sottoinsieme delle colonne. La differenza con i dati LOB è che se non si seleziona quella colonna, i dati fuori riga non verranno letti. Quindi non è proprio corretto trarre una conclusione sull'efficienza con cui SQL Server può eseguire la scansione di questo indice cluster poiché non lo si è esattamente testato (o se ne è stato verificato la metà). Hai selezionato tutte le colonne, che include la colonna XML, e come hai già detto, è lì che si trova la maggior parte dei dati.
Quindi sappiamo già che il SELECT TOP 1000 *
test non stava semplicemente leggendo una serie di pagine di dati 8k, tutte in fila, ma saltando invece in altre posizioni per ogni riga . L'esatta struttura di tali dati LOB può variare in base alla loro dimensione. Sulla base della ricerca mostrata qui ( Qual è la dimensione del puntatore LOB per tipi (MAX) come Varchar, Varbinary, Etc? ), Ci sono due tipi di allocazioni LOB off-row:
- Root in linea: per dati tra 8001 e 40.000 (in realtà 42.000) byte, spazio consentito, ci saranno da 1 a 5 puntatori (24 - 72 byte) IN RIGA che puntano direttamente alle pagine LOB.
- TEXT_TREE: per i dati superiori a 42.000 byte o se i puntatori da 1 a 5 non possono adattarsi in fila, ci sarà solo un puntatore a 24 byte sulla pagina iniziale di un elenco di puntatori alle pagine LOB (ovvero il " pagina "text_tree").
Una di queste due situazioni si verifica ogni volta che si recuperano dati LOB che superano gli 8000 byte o che non si adattano perfettamente alla riga. Ho pubblicato uno script di test su PasteBin.com (script T-SQL per testare le allocazioni e le letture LOB ) che mostra i 3 tipi di allocazioni LOB (in base alla dimensione dei dati) e l'effetto che ciascuno di essi ha su logici e letture fisiche. Nel tuo caso, se i dati XML sono in realtà inferiori a 42.000 byte per riga, nessuno di essi (o molto poco) dovrebbe essere nella struttura TEXT_TREE meno efficiente.
Se si desidera verificare la velocità con cui SQL Server può eseguire la scansione di tale indice cluster, eseguire SELECT TOP 1000
ma specificare una o più colonne che non includono quella colonna XML. In che modo ciò influisce sui risultati? Dovrebbe essere un po 'più veloce.
si ritiene ragionevole disporre di una tale struttura di tabella / modello di dati?
Dato che abbiamo una descrizione incompleta dell'attuale struttura della tabella e del modello di dati, qualsiasi risposta potrebbe non essere ottimale a seconda di quali siano quei dettagli mancanti. Con questo in mente, direi che non c'è nulla di ovviamente irragionevole nella struttura della tabella o nel modello di dati.
Posso (in ac # app) comprimere XML da 20 KB a ~ 2,5 KB e archiviarlo nella colonna VARBINARY, impedendo l'utilizzo delle pagine di dati LOB. Questo accelera SELEZIONA 20 volte nei miei test.
Ciò ha reso VARBINARY
più veloce la selezione di tutte le colonne, o anche solo dei dati XML (ora in ), ma in realtà danneggia le query che non selezionano i dati "XML". Supponendo di avere circa 50 byte nelle altre colonne e di avere un FILLFACTOR
100, quindi:
Nessuna compressione: 15k di XML
dati dovrebbero richiedere 2 pagine LOB_DATA, che quindi richiedono 2 puntatori per il root inline. Il primo puntatore è di 24 byte e il secondo è 12, per un totale di 36 byte memorizzati in fila per i dati XML. La dimensione totale della riga è di 86 byte e puoi adattare circa 93 di quelle righe a una pagina di dati di 8060 byte. Pertanto, 1 milione di righe richiede 10.753 pagine di dati.
Compressione personalizzata: 2,5k di VARBINARY
dati si adatteranno in fila. La dimensione totale della riga è di 2610 (2,5 * 1024 = 2560) byte e puoi adattare solo 3 di quelle righe su una pagina di dati da 8060 byte. Pertanto, 1 milione di righe richiede 333.334 pagine di dati.
Ergo, l'implementazione della compressione personalizzata comporta un aumento di 30 volte delle pagine di dati per l'indice cluster. Ciò significa che tutte le query che utilizzano una scansione dell'indice cluster hanno ora circa 322.500 pagine di dati in più da leggere. Si prega di consultare la sezione dettagliata di seguito per ulteriori ramificazioni su come eseguire questo tipo di compressione.
Vorrei mettere in guardia dal fare qualsiasi refactoring basato sulle prestazioni di SELECT TOP 1000 *
. Non è probabile che si tratti di una query che l'applicazione emetterà, e non dovrebbe essere utilizzata come unica base per l'ottimizzazione / i potenzialmente inutile.
Per informazioni più dettagliate e altri test da provare, consultare la sezione seguente.
A questa domanda non è possibile dare una risposta definitiva, ma possiamo almeno fare qualche progresso e suggerire ulteriori ricerche per aiutarci ad avvicinarci a capire il problema esatto (idealmente basato su prove).
Quello che sappiamo:
- La tabella ha circa 1 milione di righe
- La dimensione del tavolo è di circa 15 GB
- Tabella contiene una
XML
colonna e diversi altri tipi di colonne: INT
, BIGINT
, UNIQUEIDENTIFIER
, "etc"
XML
la colonna "dimensione" è, in media, circa 15k
- Dopo l'esecuzione
DBCC DROPCLEANBUFFERS
, sono necessari 20-25 secondi per completare la query seguente:SELECT TOP 1000 * FROM TABLE
- L'indice cluster è in fase di scansione
- La frammentazione sull'indice cluster è vicina allo 0%
Cosa pensiamo di sapere:
- Nessun'altra attività su disco al di fuori di queste query. Sei sicuro? Anche se non ci sono altre query degli utenti, ci sono operazioni in background in corso? Esistono processi esterni a SQL Server in esecuzione sullo stesso computer che potrebbero occupare parte dell'IO? Potrebbe non esserci, ma non è chiaro in base esclusivamente alle informazioni fornite.
- Vengono restituiti 15 MB di dati XML. Su cosa si basa questo numero? Una stima derivata dalle 1000 righe per la media di 15k di dati XML per riga? O un'aggregazione programmatica di ciò che è stato ricevuto per quella query? Se è solo una stima, non farei affidamento su di essa poiché la distribuzione dei dati XML potrebbe non essere nemmeno nel modo implicito da una media semplice.
La compressione XML potrebbe aiutare. Come faresti esattamente la compressione in .NET? Tramite le classi GZipStream o DeflateStream ? Questa non è un'opzione a costo zero. Comprimerà sicuramente alcuni dei dati di una grande percentuale, ma richiederà anche più CPU in quanto sarà necessario un processo aggiuntivo per comprimere / decomprimere i dati ogni volta. Questo piano eliminerebbe inoltre completamente la tua capacità di:
- interrogare i dati XML attraverso il
.nodes
, .value
, .query
, e .modify
funzioni XML.
indicizzare i dati XML.
Tieni presente (dal momento che hai menzionato che XML è "altamente ridondante") che il XML
tipo di dati è già ottimizzato in quanto memorizza i nomi degli elementi e degli attributi in un dizionario, assegnando un ID indice intero a ciascun elemento e quindi utilizzando tale ID intero in tutto il documento (quindi non ripete il nome completo per ogni utilizzo, né lo ripete come tag di chiusura per gli elementi). I dati effettivi hanno anche rimosso uno spazio bianco estraneo. Questo è il motivo per cui i documenti XML estratti non mantengono la loro struttura originale e perché gli elementi vuoti vengono estratti <element />
anche se inseriti come<element></element>
. Pertanto, qualsiasi guadagno derivante dalla compressione tramite GZip (o qualsiasi altra cosa) verrà trovato solo comprimendo i valori di elemento e / o attributo, che è una superficie molto più piccola che potrebbe essere migliorata di quanto molti si aspetterebbero, e molto probabilmente non vale la perdita di capacità come indicato direttamente sopra.
Tieni inoltre presente che la compressione dei dati XML e la memorizzazione del VARBINARY(MAX)
risultato non elimineranno l'accesso a LOB, ma semplicemente lo ridurranno. A seconda della dimensione del resto dei dati nella riga, il valore compresso potrebbe adattarsi nella riga o potrebbe richiedere ancora pagine LOB.
Tali informazioni, sebbene utili, non sono quasi sufficienti. Ci sono molti fattori che influenzano le prestazioni delle query, quindi abbiamo bisogno di un quadro molto più dettagliato di ciò che sta succedendo.
Quello che non sappiamo, ma dobbiamo:
- Perché le prestazioni della
SELECT *
materia? È questo uno schema che usi nel codice. Se è così, perché?
- Qual è la prestazione di selezionare solo la colonna XML? Quali sono le statistiche e i tempi se fai solo
SELECT TOP 1000 XmlColumn FROM TABLE;
:?
Quanto dei 20 - 25 secondi necessari per restituire queste 1000 righe è correlato a fattori di rete (trasmissione dei dati attraverso il cavo) e quanto è correlato a fattori client (il che rende circa 15 MB più il resto dei non- Dati XML nella griglia in SSMS o eventualmente salvando su disco)?
Il factoring di questi due aspetti dell'operazione a volte può essere fatto semplicemente non restituendo i dati. Ora, si potrebbe pensare di selezionare una tabella temporanea o una variabile di tabella, ma ciò introdurrebbe solo alcune nuove variabili (ad es. I / O del disco per tempdb
, scritture del registro delle transazioni, possibile crescita automatica dei dati tempdb e / o file di registro, necessità spazio nel pool di buffer, ecc.). Tutti questi nuovi fattori possono effettivamente aumentare il tempo di query. Invece, in genere memorizzo le colonne in variabili (del tipo di dati appropriato; non SQL_VARIANT
) che vengono sovrascritte con ogni nuova riga (cioè SELECT @Column1 = tab.Column1,...
).
TUTTAVIA , come sottolineato da @PaulWhite in questo D & D di DBA.StackExchange, Logical legge diversamente quando si accede agli stessi dati LOB , con ulteriori ricerche del mio postato su PasteBin ( script T-SQL per testare vari scenari per letture LOB ) , LOB non sono accessibili in modo coerente tra SELECT
, SELECT INTO
, SELECT @XmlVariable = XmlColumn
, SELECT @XmlVariable = XmlColumn.query(N'/')
, e SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
. Quindi le nostre opzioni sono un po 'più limitate qui, ma ecco cosa si può fare:
- Escludere i problemi di rete eseguendo la query sul server che esegue SQL Server, in SSMS o SQLCMD.EXE.
- Escludere i problemi dei client in SSMS andando su Opzioni query -> Risultati -> Griglia e selezionando l'opzione "Elimina risultati dopo l'esecuzione". Si noti che questa opzione impedisce TUTTO l'output, inclusi i messaggi, ma può comunque essere utile per escludere il tempo impiegato da SSMS per allocare la memoria per ogni riga e quindi disegnarla nella griglia.
In alternativa, è possibile eseguire la query tramite sqlcmd.exe e indirizzare l'output di andare da nessuna parte via: -o NUL:
.
- Esiste un tipo di attesa associato a questa query? Se sì, cos'è quel tipo di attesa?
Qual è la dimensione effettiva dei dati per le XML
colonne restituite ? La dimensione media di quella colonna sull'intera tabella non ha importanza se le righe "TOP 1000" contengono una porzione sproporzionatamente grande dei XML
dati totali . Se vuoi conoscere le TOP 1000 righe, guarda quelle righe. Si prega di eseguire quanto segue:
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
- Lo schema esatto della tabella. Fornisci la dichiarazione completa
CREATE TABLE
, inclusi tutti gli indici.
- Piano di query? È qualcosa che puoi pubblicare? Quelle informazioni probabilmente non cambieranno nulla, ma è meglio sapere che non lo farà piuttosto che indovinare che non lo farà e che si sbagliano ;-)
- Esiste una frammentazione fisica / esterna sul file di dati? Anche se questo potrebbe non essere un grande fattore qui, dal momento che stai usando "SATA di livello consumer" e non SSD o anche SATA super-costoso, l'effetto dei settori ordinati in modo subottimale sarà più evidente, soprattutto perché il numero di quei settori che deve essere letto aumenta.
Quali sono i risultati esatti della seguente query:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
AGGIORNARE
Mi è venuto in mente che avrei dovuto provare a riprodurre questo scenario per vedere se ho un comportamento simile. Quindi, ho creato una tabella con diverse colonne (simile alla vaga descrizione nella Domanda), e poi l'ho popolata con 1 milione di righe e la colonna XML ha circa 15k di dati per riga (vedi codice sotto).
Quello che ho scoperto è che fare un SELECT TOP 1000 * FROM TABLE
completamento completato in 8 secondi la prima volta e 2 - 4 secondi ogni volta in seguito (sì, eseguendo DBCC DROPCLEANBUFFERS
prima di ogni esecuzione della SELECT *
query). E il mio laptop di diversi anni non è veloce: SQL Server 2012 SP2 Developer Edition, 64 bit, 6 GB RAM, dual 2.5 Ghz Core i5 e un'unità SATA da 5400 RPM. Sto anche eseguendo SSMS 2014, SQL Server Express 2014, Chrome e molte altre cose.
In base al tempo di risposta del mio sistema, ripeterò che abbiamo bisogno di maggiori informazioni (ovvero specifiche sulla tabella e sui dati, risultati dei test suggeriti, ecc.) Al fine di aiutare a restringere la causa del tempo di risposta di 20-25 secondi che stai vedendo.
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
E, poiché vogliamo calcolare il tempo impiegato per leggere le pagine non LOB, ho eseguito la seguente query per selezionare tutto tranne la colonna XML (uno dei test che ho suggerito sopra). Questo ritorna in 1,5 secondi abbastanza coerentemente.
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Conclusione (per il momento)
In base al mio tentativo di ricreare il tuo scenario, non credo che possiamo indicare l'unità SATA o l'I / O non sequenziale come la causa principale dei 20-25 secondi, soprattutto perché ancora non so quanto velocemente la query ritorna quando non si include la colonna XML. E non sono stato in grado di riprodurre il gran numero di Letture logiche (non LOB) che stai mostrando, ma ho la sensazione che ho bisogno di aggiungere più dati a ciascuna riga alla luce di ciò e la dichiarazione di:
~ 90% delle pagine della tabella sono LOB_DATA
La mia tabella ha 1 milione di righe, ognuna con poco più di 15k di dati XML e sys.dm_db_index_physical_stats
mostra che ci sono 2 milioni di pagine LOB_DATA. Il restante 10% sarebbe quindi 222k pagine di dati IN_ROW, ma ne ho solo 11.630. Quindi, ancora una volta, abbiamo bisogno di maggiori informazioni sullo schema della tabella attuale e sui dati effettivi.