SUMA de DATALENGTHs que no coinciden con el tamaño de la tabla de sys.allocation_units

11

Tenía la impresión de que si tuviera que sumar DATALENGTH()todos los campos para todos los registros en una tabla, obtendría el tamaño total de la tabla. ¿Estoy equivocado?

SELECT 
SUM(DATALENGTH(Field1)) + 
SUM(DATALENGTH(Field2)) + 
SUM(DATALENGTH(Field3)) TotalSizeInBytes
FROM SomeTable
WHERE X, Y, and Z are true

Utilicé esta consulta a continuación (que obtuve en línea para obtener tamaños de tabla, índices agrupados solo para que no incluya índices NC) para obtener el tamaño de una tabla en particular en mi base de datos. Para fines de facturación (cobramos a nuestros departamentos por la cantidad de espacio que usan) necesito calcular cuánto espacio usó cada departamento en esta tabla. Tengo una consulta que identifica cada grupo dentro de la tabla. Solo necesito calcular cuánto espacio ocupa cada grupo.

El espacio por fila puede oscilar enormemente debido a los VARCHAR(MAX)campos en la tabla, por lo que no puedo tomar un tamaño promedio * de la proporción de filas para un departamento. Cuando uso el DATALENGTH()enfoque descrito anteriormente, solo obtengo el 85% del espacio total utilizado en la consulta a continuación. Pensamientos?

SELECT 
s.Name AS SchemaName,
t.NAME AS TableName,
p.rows AS RowCounts,
(SUM(a.total_pages) * 8)/1024 AS TotalSpaceMB, 
(SUM(a.used_pages) * 8)/1024 AS UsedSpaceMB, 
((SUM(a.total_pages) - SUM(a.used_pages)) * 8)/1024 AS UnusedSpaceMB
FROM 
    sys.tables t with (nolock)
INNER JOIN 
    sys.schemas s with (nolock) ON s.schema_id = t.schema_id
INNER JOIN      
    sys.indexes i with (nolock) ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p with (nolock) ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a with (nolock) ON p.partition_id = a.container_id
WHERE 
    t.is_ms_shipped = 0
    AND i.OBJECT_ID > 255 
    AND i.type_desc = 'Clustered'
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    TotalSpaceMB desc

Se ha sugerido que cree un índice filtrado para cada departamento o partición de la tabla, de modo que pueda consultar directamente el espacio utilizado por índice. Los índices filtrados se pueden crear mediante programación (y se vuelven a colocar durante una ventana de mantenimiento o cuando necesito realizar la facturación periódica), en lugar de usar el espacio todo el tiempo (las particiones serían mejores a este respecto).

Me gusta esa sugerencia y normalmente haría eso. Pero para ser honesto, uso "cada departamento" como ejemplo para explicar por qué necesito esto, pero para ser honesto, ese no es realmente el motivo. Debido a razones de confidencialidad, no puedo explicar la razón exacta por la que necesito estos datos, pero es análogo a los diferentes departamentos.

Con respecto a los índices no agrupados en esta tabla: si puedo obtener los tamaños de los índices NC, sería genial. Sin embargo, los índices NC representan <1% del tamaño del índice agrupado, por lo que estamos bien sin incluirlos. Sin embargo, ¿cómo incluiríamos los índices NC de todos modos? Ni siquiera puedo obtener un tamaño exacto para el índice agrupado :)

Chris Woods
fuente
Entonces, en esencia, tiene dos preguntas: (1) ¿por qué la suma de las longitudes de fila no coincide con la contabilidad de metadatos del tamaño de toda la tabla? La respuesta a continuación aborda eso al menos en parte (y eso puede fluctuar por versión y por característica, por ejemplo, compresión, almacén de columnas, etc.). Y lo más importante: (2) ¿cómo puede determinar con precisión el espacio real utilizado por departamento? No sé si puede hacerlo con precisión, porque para algunos de los datos incluidos en la respuesta, no hay forma de saber a qué departamento pertenece.
Aaron Bertrand
No creo que el problema sea que no tiene un tamaño exacto para el índice agrupado; los metadatos definitivamente le indican con precisión cuánto espacio ocupa su índice. Lo que los metadatos no están diseñados para decirle, al menos dado su diseño / estructura actual, qué cantidad de datos se asocia con cada departamento.
Aaron Bertrand

Respuestas:

19

                          Please note that the following info is not intended to be a comprehensive
description of how data pages are laid out, such that one can calculate
the number of bytes used per any set of rows, as that is very complicated.

Los datos no son lo único que ocupa espacio en una página de datos de 8k:

  • Hay espacio reservado. Solo puede usar 8060 de los 8192 bytes (eso es 132 bytes que nunca fueron suyos en primer lugar):

    • Encabezado de página: esto es exactamente 96 bytes.
    • Matriz de ranuras: esto es 2 bytes por fila e indica el desplazamiento de dónde comienza cada fila en la página. El tamaño de esta matriz no se limita a los 36 bytes restantes (132 - 96 = 36), de lo contrario, se limitaría efectivamente a poner solo 18 filas como máximo en una página de datos. Esto significa que cada fila es 2 bytes más grande de lo que crees. Este valor no se incluye en el "tamaño de registro" según lo informado por DBCC PAGE, por lo que se mantiene separado aquí en lugar de incluirse en la información por fila a continuación.
    • Metadatos por fila (incluidos, entre otros):
      • El tamaño varía según la definición de la tabla (es decir, número de columnas, longitud variable o longitud fija, etc.). Información tomada de los comentarios de @ PaulWhite y @ Aaron que se pueden encontrar en la discusión relacionada con esta respuesta y prueba.
      • Encabezado de fila: 4 bytes, 2 de ellos denotan el tipo de registro y los otros dos son un desplazamiento del mapa de bits NULL
      • Número de columnas: 2 bytes.
      • Mapa de bits NULL: qué columnas están actualmente NULL. 1 byte por cada conjunto de 8 columnas. Y para todas las columnas, incluso NOT NULLlas. Por lo tanto, mínimo 1 byte.
      • Matriz de desplazamiento de columna de longitud variable: 4 bytes como mínimo. 2 bytes para contener el número de columnas de longitud variable, y luego 2 bytes por cada columna de longitud variable para mantener el desplazamiento donde comienza.
      • Información de versiones: 14 bytes (esto estará presente si su base de datos está configurada en ALLOW_SNAPSHOT_ISOLATION ONo READ_COMMITTED_SNAPSHOT ON).
    • Consulte la siguiente pregunta y respuesta para obtener más detalles al respecto: matriz de ranuras y tamaño total de página
    • Consulte la siguiente publicación de blog de Paul Randall que tiene varios detalles interesantes sobre cómo se presentan las páginas de datos: hurgando con la página DBCC (Parte 1 de?)
  • Punteros LOB para datos que no están almacenados en fila. Entonces eso explicaría DATALENGTH+ pointer_size. Pero estos no son de un tamaño estándar. Consulte la siguiente publicación de blog para obtener detalles sobre este tema complejo: ¿Cuál es el tamaño del puntero LOB para tipos (MAX) como Varchar, Varbinary, Etc? . Entre esa publicación vinculada y algunas pruebas adicionales que he realizado , las reglas (predeterminadas) deberían ser las siguientes:

    • Legacy / desaprobado tipos LOB que nadie debería usar más como de SQL Server 2005 ( TEXT, NTEXTy IMAGE):
      • Por defecto, siempre almacene sus datos en páginas LOB y siempre use un puntero de 16 bytes para el almacenamiento LOB.
      • SI se usó sp_tableoption para establecer la text in rowopción, entonces:
        • si hay espacio en la página para almacenar el valor, y el valor no es mayor que el tamaño máximo de fila (rango configurable de 24 - 7000 bytes con un valor predeterminado de 256), entonces se almacenará en fila,
        • de lo contrario, será un puntero de 16 bytes.
    • Para los tipos LOB más recientes introducidas en SQL Server 2005 ( VARCHAR(MAX), NVARCHAR(MAX)y VARBINARY(MAX)):
      • Por defecto:
        • Si el valor no es superior a 8000 bytes y hay espacio en la página, se almacenará en fila.
        • Raíz en línea: para datos entre 8001 y 40,000 (realmente 42,000) bytes, si el espacio lo permite, habrá de 1 a 5 punteros (24 - 72 bytes) EN FILA que apuntan directamente a la (s) página (s) LOB. 24 bytes para la página inicial de 8k LOB, y 12 bytes por cada página adicional de 8k para hasta cuatro páginas de 8k más.
        • TEXT_TREE: para datos de más de 42,000 bytes, o si los punteros de 1 a 5 no caben en la fila, entonces solo habrá un puntero de 24 bytes a la página de inicio de una lista de punteros a las páginas LOB (es decir, "text_tree " página).
      • SI se usó sp_tableoption para establecer la large value types out of rowopción, siempre use un puntero de 16 bytes para almacenamiento LOB.
    • Dije reglas "por defecto", porque Yo no probar valores en fila contra el impacto de ciertas características tales como la compresión de datos, encriptación a nivel de columna, Transparente cifrado de datos, siempre cifrados, etc.
  • Páginas de desbordamiento de LOB: si un valor es 10k, entonces eso requerirá 1 página de desbordamiento de 8k completa, y luego parte de una segunda página. Si ningún otro dato puede ocupar el espacio restante (o incluso se permite, no estoy seguro de esa regla), entonces tiene aproximadamente 6 kb de espacio "desperdiciado" en esa segunda página de datos de desbordamiento de LOB.

  • Espacio no utilizado: una página de datos de 8k es solo eso: 8192 bytes. No varía en tamaño. Sin embargo, los datos y metadatos que se le asignan no siempre encajan bien en todos los 8192 bytes. Y las filas no se pueden dividir en varias páginas de datos. Por lo tanto, si tiene 100 bytes restantes pero ninguna fila (o ninguna fila que cabría en esa ubicación, dependiendo de varios factores) puede caber allí, la página de datos aún ocupa 8192 bytes, y su segunda consulta solo cuenta el número de páginas de datos Puede encontrar este valor en dos lugares (solo tenga en cuenta que una parte de este valor es una cantidad de ese espacio reservado):

    • DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;Busque ParentObject= "ENCABEZADO DE PÁGINA:" y Field= "m_freeCnt". El Valuecampo es el número de bytes no utilizados.
    • SELECT buff.free_space_in_bytes FROM sys.dm_os_buffer_descriptors buff WHERE buff.[database_id] = DB_ID(N'db_name') AND buff.[page_id] = page_id;Este es el mismo valor reportado por "m_freeCnt". Esto es más fácil que DBCC ya que puede obtener muchas páginas, pero también requiere que las páginas se hayan leído en el grupo de búferes en primer lugar.
  • Espacio reservado por FILLFACTOR<100. Las páginas recién creadas no respetan la FILLFACTORconfiguración, pero al realizar una RECONSTRUCCIÓN se reservará ese espacio en cada página de datos. La idea detrás del espacio reservado es que será utilizado por inserciones no secuenciales y / o actualizaciones que ya expanden el tamaño de las filas en la página, debido a que las columnas de longitud variable se actualizan con un poco más de datos (pero no lo suficiente como para causar un división de página). Pero podría reservar fácilmente espacio en páginas de datos que, naturalmente, nunca obtendrían nuevas filas y nunca actualizarían las filas existentes, o al menos no se actualizarían de una manera que aumentaría el tamaño de la fila.

  • División de página (fragmentación): la necesidad de agregar una fila a una ubicación que no tiene espacio para la fila provocará una división de página. En este caso, aproximadamente el 50% de los datos existentes se mueven a una nueva página y la nueva fila se agrega a una de las 2 páginas. Pero ahora tiene un poco más de espacio libre que no se tiene en cuenta en los DATALENGTHcálculos.

  • Filas marcadas para su eliminación. Cuando elimina filas, no siempre se eliminan inmediatamente de la página de datos. Si no pueden eliminarse de inmediato, están "marcados para la muerte" (referencia de Steven Segal) y serán eliminados físicamente más tarde por el proceso de limpieza de fantasmas (creo que ese es el nombre). Sin embargo, estos podrían no ser relevantes para esta pregunta en particular.

  • Páginas fantasma? No estoy seguro de si ese es el término apropiado, pero a veces las páginas de datos no se eliminan hasta que se realiza una RECONSTRUCCIÓN del índice agrupado. Eso también representaría más páginas de las DATALENGTHque sumaría. Esto generalmente no debería suceder, pero me he encontrado con él una vez, hace varios años.

  • Columnas SPARSE: las columnas dispersas ahorran espacio (principalmente para tipos de datos de longitud fija) en tablas donde un gran% de las filas son NULLpara una o más columnas. La SPARSEopción hace que el NULLtipo de valor aumente 0 bytes (en lugar de la cantidad normal de longitud fija, como 4 bytes para un INT), pero los valores no NULL ocupan 4 bytes adicionales para los tipos de longitud fija y una cantidad variable para tipos de longitud variable. El problema aquí es que DATALENGTHno incluye los 4 bytes adicionales para valores no NULL en una columna SPARSE, por lo que esos 4 bytes deben agregarse nuevamente. Puede verificar si hay SPARSEcolumnas a través de:

    SELECT OBJECT_SCHEMA_NAME(sc.[object_id]) AS [SchemaName],
           OBJECT_NAME(sc.[object_id]) AS [TableName],
           sc.name AS [ColumnName]
    FROM   sys.columns sc
    WHERE  sc.is_sparse = 1;

    Y luego, para cada SPARSEcolumna, actualice la consulta original para usar:

    SUM(DATALENGTH(FieldN) + 4)

    Tenga en cuenta que el cálculo anterior para agregar 4 bytes estándar es un poco simplista, ya que solo funciona para tipos de longitud fija. Y, hay metadatos adicionales por fila (de lo que puedo decir hasta ahora) que reduce el espacio disponible para los datos, simplemente al tener al menos una columna SPARSE. Para obtener más detalles, consulte la página de MSDN para Usar columnas dispersas .

  • Índice y otras páginas (por ejemplo, IAM, PFS, GAM, SGAM, etc.): estas no son páginas de "datos" en términos de datos del usuario. Estos inflarán el tamaño total de la tabla. Si usa SQL Server 2012 o posterior, puede usar la sys.dm_db_database_page_allocationsFunción de administración dinámica (DMF) para ver los tipos de página (pueden usar versiones anteriores de SQL Server DBCC IND(0, N'dbo.table_name', 0);):

    SELECT *
    FROM   sys.dm_db_database_page_allocations(
                   DB_ID(),
                   OBJECT_ID(N'dbo.table_name'),
                   1,
                   NULL,
                   N'DETAILED'
                  )
    WHERE  page_type = 1; -- DATA_PAGE

    Ni el DBCC INDni sys.dm_db_database_page_allocations(con esa cláusula WHERE) informará ninguna página de índice, y solo el DBCC INDinformará al menos una página IAM.

  • DATA_COMPRESSION: si tiene habilitado ROWo PAGECompresión en el índice agrupado o el montón, puede olvidarse de la mayoría de lo que se ha mencionado hasta ahora. El encabezado de página de 96 bytes, la matriz de ranuras de 2 bytes por fila y la información de versiones de 14 bytes por fila todavía están allí, pero la representación física de los datos se vuelve muy compleja (mucho más de lo que ya se ha mencionado cuando Compression no se está utilizando) Por ejemplo, con la compresión de filas, SQL Server intenta usar el contenedor más pequeño posible para ajustar cada columna, por cada fila. Entonces, si tiene una BIGINTcolumna que de lo contrario (suponiendo SPARSEque tampoco esté habilitada) siempre ocupará 8 bytes, si el valor está entre -128 y 127 (es decir, entero de 8 bits con signo), usará solo 1 byte, y si el valor podría caber en unSMALLINT, solo ocupará 2 bytes. Los tipos enteros que son NULLo 0no ocupan espacio y simplemente se indican como estar NULLo "vacíos" (es decir 0) en una matriz que asigna las columnas. Y hay muchas, muchas otras reglas. Los datos han Unicode ( NCHAR, NVARCHAR(1 - 4000)pero no NVARCHAR(MAX) , incluso si se almacena en fila)? La compresión Unicode se agregó en SQL Server 2008 R2, pero no hay forma de predecir el resultado del valor "comprimido" en todas las situaciones sin realizar la compresión real dada la complejidad de las reglas .

Entonces, realmente, su segunda consulta, aunque es más precisa en términos del espacio físico total ocupado en el disco, solo es realmente precisa al hacer un REBUILDíndice agrupado. Y después de eso, aún debe tener en cuenta cualquier FILLFACTORconfiguración por debajo de 100. E incluso entonces siempre hay encabezados de página y, a menudo, una cantidad suficiente de espacio "desperdiciado" que simplemente no se puede llenar debido a que es demasiado pequeño para caber en cualquier fila en este tabla, o al menos la fila que lógicamente debería ir en esa ranura.

Con respecto a la precisión de la segunda consulta para determinar el "uso de datos", parece más justo anular los bytes del encabezado de página, ya que no son el uso de datos: son gastos generales del costo del negocio. Si hay 1 fila en una página de datos y esa fila es solo un TINYINT, entonces ese 1 byte aún requiere que la página de datos exista y, por lo tanto, los 96 bytes del encabezado. ¿Se debe cobrar a ese departamento por toda la página de datos? Si esa página de datos la llena el Departamento # 2, ¿dividirían equitativamente ese costo "general" o pagarían proporcionalmente? Parece más fácil simplemente retroceder. En cuyo caso, usar un valor de 8para multiplicar number of pageses demasiado alto. Qué tal si:

-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250

Por lo tanto, use algo como:

(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]

para todos los cálculos contra columnas "número_de_páginas".

Y , teniendo en cuenta que el uso DATALENGTHpor cada campo no puede devolver los metadatos por fila, eso debe agregarse a su consulta por tabla donde obtiene el DATALENGTHpor cada campo, filtrando en cada "departamento":

  • Tipo de registro y desplazamiento a mapa de bits NULL: 4 bytes
  • Recuento de columnas: 2 bytes
  • Matriz de ranuras: 2 bytes (no incluido en el "tamaño de registro", pero aún debe tener en cuenta)
  • Mapa de bits NULL: 1 byte por cada 8 columnas (para todas las columnas)
  • Control de versiones de fila: 14 bytes (si la base de datos tiene ALLOW_SNAPSHOT_ISOLATIONo está READ_COMMITTED_SNAPSHOTestablecida en ON)
  • Matriz de desplazamiento de columna de longitud variable: 0 bytes si todas las columnas son de longitud fija. Si alguna columna es de longitud variable, entonces 2 bytes, más 2 bytes por cada una de las columnas de longitud variable.
  • Punteros LOB: esta parte es muy imprecisa ya que no habrá un puntero si el valor es NULL, y si el valor cabe en la fila, entonces puede ser mucho más pequeño o mucho más grande que el puntero, y si el valor se almacena fuera de fila, entonces el tamaño del puntero puede depender de la cantidad de datos que haya. Sin embargo, dado que solo queremos una estimación (es decir, "swag"), parece que 24 bytes es un buen valor para usar (bueno, tan bueno como cualquier otro ;-). Esto es por cada MAXcampo.

Por lo tanto, use algo como:

  • En general (encabezado de fila + número de columnas + matriz de ranuras + mapa de bits NULL):

    ([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
  • En general (detección automática si hay "información de versión"):

    + (SELECT CASE WHEN snapshot_isolation_state = 1 OR is_read_committed_snapshot_on = 1
                     THEN 14 ELSE 0 END FROM sys.databases WHERE [database_id] = DB_ID())
  • SI hay columnas de longitud variable, agregue:

    + 2 + (2 * {NumVariableLengthColumns})
  • SI hay MAXcolumnas / LOB, luego agregue:

    + (24 * {NumLobColumns})
  • En general:

    )) AS [MetaDataBytes]

Esto no es exacto, y nuevamente no funcionará si tiene habilitada la compresión de filas o páginas en el montón o el índice agrupado, pero definitivamente debería acercarlo.


ACTUALIZACIÓN sobre el misterio del 15% de diferencia

Nosotros (incluido yo mismo) estábamos tan centrados en pensar en cómo se distribuyen las páginas de datos y cómo DATALENGTHpodrían explicar las cosas que no pasamos mucho tiempo revisando la segunda consulta. Ejecuté esa consulta en una sola tabla y luego comparé esos valores con lo que informaba sys.dm_db_database_page_allocationsy no eran los mismos valores para el número de páginas. En una corazonada, eliminé las funciones agregadas GROUP BYy reemplacé la SELECTlista con a.*, '---' AS [---], p.*. Y luego quedó claro: las personas deben tener cuidado de dónde obtienen información y guiones de estas interwebs turbias ;-). La segunda consulta publicada en la pregunta no es exactamente correcta, especialmente para esta pregunta en particular.

  • Problema menor: fuera de él no tiene mucho sentido GROUP BY rows(y no tener esa columna en una función agregada), la UNIÓN entre sys.allocation_unitsy sys.partitionsno es técnicamente correcta. Hay 3 tipos de unidades de asignación, y una de ellas debería UNIRSE a un campo diferente. Muy a menudo partition_idy hobt_idson lo mismo, por lo que puede que nunca haya un problema, pero a veces esos dos campos tienen valores diferentes.

  • Problema principal: la consulta usa el used_pagescampo. Ese campo cubre todos los tipos de páginas: Datos, Índice, IAM, etc., tc. Hay otro, el campo más adecuado para su uso cuando se trate sólo con los datos reales: data_pages.

Adapte la segunda consulta en la Pregunta con los elementos anteriores en mente, y usando el tamaño de página de datos que retrocede el encabezado de la página. También he eliminado dos combinaciones que eran innecesarios: sys.schemas(reemplazado con llamada a SCHEMA_NAME()) y sys.indexes(el índice agrupado es siempre index_id = 1y tenemos index_iden sys.partitions).

SELECT  SCHEMA_NAME(st.[schema_id]) AS [SchemaName],
        st.[name] AS [TableName],
        SUM(sp.[rows]) AS [RowCount],
        (SUM(sau.[total_pages]) * 8.0) / 1024 AS [TotalSpaceMB],
        (SUM(CASE sau.[type]
           WHEN 1 THEN sau.[data_pages]
           ELSE (sau.[used_pages] - 1) -- back out the IAM page
         END) * 7.91) / 1024 AS [TotalActualDataMB]
FROM        sys.tables st
INNER JOIN  sys.partitions sp
        ON  sp.[object_id] = st.[object_id]
INNER JOIN  sys.allocation_units sau
        ON  (   sau.[type] = 1
            AND sau.[container_id] = sp.[partition_id]) -- IN_ROW_DATA
        OR  (   sau.[type] = 2
            AND sau.[container_id] = sp.[hobt_id]) -- LOB_DATA
        OR  (   sau.[type] = 3
            AND sau.[container_id] = sp.[partition_id]) -- ROW_OVERFLOW_DATA
WHERE       st.is_ms_shipped = 0
--AND         sp.[object_id] = OBJECT_ID(N'dbo.table_name')
AND         sp.[index_id] < 2 -- 1 = Clustered Index; 0 = Heap
GROUP BY    SCHEMA_NAME(st.[schema_id]), st.[name]
ORDER BY    [TotalSpaceMB] DESC;
Solomon Rutzky
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9
Aunque la consulta actualizada que proporcionó para la segunda consulta está aún más lejos (en la otra dirección ahora :)), estoy de acuerdo con esta respuesta. Aparentemente, es una nuez muy difícil de descifrar y, por lo que vale, me alegra que incluso con los expertos que me ayudaron, aún no pude descubrir la razón exacta por la que los dos métodos no coinciden. Voy a usar la metodología en la otra respuesta para extrapolar. Desearía poder votar sí por ambas respuestas, pero @srutzky me ayudó con todas las razones por las cuales las dos estarían apagadas.
Chris Woods
6

Tal vez esta sea una respuesta grunge, pero esto es lo que haría.

Entonces DATALENGTH solo representa el 86% del total. Todavía es una división muy representativa. La sobrecarga en la excelente respuesta de srutzky debería tener una división bastante pareja.

Usaría su segunda consulta (páginas) para el total. Y use el primero (longitud de datos) para asignar la división. Muchos costos se asignan utilizando una normalización.

Y debe tener en cuenta que una respuesta más cercana aumentará los costos, por lo que incluso el departamento que perdió en una división aún puede pagar más.

paparazzo
fuente