통계정보
- SQL Server에서 **테이블/인덱스 통계정보**를 확인하려면 Oracle의 `DBA_TAB_STATISTICS`, `DBA_IND_STATISTICS`, `DBA_TAB_COL_STATISTICS`와 대응해서 보면 이해하기 좋습니다.
- 가장 중요한 것은 SQL Server의 **통계 객체(Statistics)** 자체를 조회하는 것입니다.
특정 테이블의 통계정보
예를 들어 `dbo.ORDERS` 테이블:
SELECT
s.name AS schema_name,
t.name AS table_name,
st.name AS statistics_name,
st.stats_id,
st.auto_created,
st.user_created,
st.no_recompute,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
sp.steps,
CAST(
sp.rows_sampled * 100.0
/ NULLIF(sp.rows, 0)
AS DECIMAL(10,2)
) AS sample_pct
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.stats st
ON t.object_id = st.object_id
OUTER APPLY sys.dm_db_stats_properties(
st.object_id,
st.stats_id
) sp
WHERE
s.name = 'dbo'
AND t.name = 'ORDERS'
ORDER BY
st.stats_id;
결과는 대략:
schema table statistics_name last_updated rows sampled mod_count dbo ORDERS _WA_Sys_00000001_... 2026-09-09 20:30 50,000,000 5,000,000 250,000 dbo ORDERS IX_ORDERS_CUSTOMER 2026-09-09 20:31 50,000,000 50,000,000 2,500 dbo ORDERS IX_ORDERS_DATE 2026-09-08 09:10 49,000,000 4,900,000 800,000
에서 특히 중요하게 볼 것은:
last_updated rows rows_sampled modification_counter
입니다.
- 2. Oracle `DBA_TAB_STATISTICS`와 비슷하게 보기
테이블별 통계 업데이트 상태를 보고 싶다면:
SELECT
s.name AS schema_name,
t.name AS table_name,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
CAST(
sp.modification_counter * 100.0
/ NULLIF(sp.rows, 0)
AS DECIMAL(10,2)
) AS modification_pct
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.stats st
ON t.object_id = st.object_id
OUTER APPLY sys.dm_db_stats_properties(
st.object_id,
st.stats_id
) sp
WHERE t.is_ms_shipped = 0
ORDER BY
sp.modification_counter DESC;
예를 들어:
TABLE ROWS MODIFICATION_COUNTER MODIFICATION_% ----------------- -------------- ---------------------------- -------------------- ORDERS 50,000,000 5,000,000 10.00 CUSTOMER 8,000,000 1,200,000 15.00 PRODUCT 2,000,000 5,000 0.25
이런 결과가 나온다면 `ORDERS`, `CUSTOMER`부터 통계를 점검하는 것이 좋습니다.
- 3. 인덱스별 통계정보
SQL Server에서는 **인덱스에 연결된 통계**와 일반 column statistics가 존재합니다.
인덱스 목록과 해당 통계를 같이 보고 싶다면:
SELECT
s.name AS schema_name,
t.name AS table_name,
i.name AS index_name,
i.index_id,
i.type_desc AS index_type,
st.name AS statistics_name,
st.auto_created,
st.user_created,
st.no_recompute,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.indexes i
ON t.object_id = i.object_id
LEFT JOIN sys.stats st
ON t.object_id = st.object_id
AND st.name = i.name
OUTER APPLY sys.dm_db_stats_properties(
st.object_id,
st.stats_id
) sp
WHERE
t.is_ms_shipped = 0
AND i.index_id > 0
ORDER BY
s.name,
t.name,
i.index_id;
그런데 여기서 중요한 점이 있습니다.
- 모든 인덱스 통계가 단순히 `statistics_name = index_name` 형태라고 가정하면 안 됩니다.**
따라서 실제 인덱스와 통계를 정확하게 연결하려면 `sys.stats`와 `sys.index_columns`까지 보는 것이 좋습니다.
- 4. 인덱스 + 통계의 선두 컬럼까지 확인
튜닝할 때는 이 쿼리를 추천합니다.
SELECT
s.name AS schema_name,
t.name AS table_name,
i.name AS index_name,
i.index_id,
i.type_desc,
st.name AS statistics_name,
STATS_DATE(
st.object_id,
st.stats_id
) AS stats_date,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
STRING_AGG(
CASE
WHEN ic.is_included_column = 0
THEN c.name
END,
', '
) WITHIN GROUP
(
ORDER BY ic.key_ordinal
) AS key_columns,
STRING_AGG(
CASE
WHEN ic.is_included_column = 1
THEN c.name
END,
', '
) AS include_columns
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.indexes i
ON t.object_id = i.object_id
JOIN sys.stats st
ON t.object_id = st.object_id
AND st.name = i.name
JOIN sys.index_columns ic
ON i.object_id = ic.object_id
AND i.index_id = ic.index_id
JOIN sys.columns c
ON ic.object_id = c.object_id
AND ic.column_id = c.column_id
OUTER APPLY sys.dm_db_stats_properties(
st.object_id,
st.stats_id
) sp
WHERE
t.is_ms_shipped = 0
GROUP BY
s.name,
t.name,
i.name,
i.index_id,
i.type_desc,
st.name,
st.object_id,
st.stats_id,
sp.rows,
sp.rows_sampled,
sp.modification_counter
ORDER BY
s.name,
t.name,
i.index_id;
이렇게 보면:
TABLE INDEX KEY_COLUMNS INCLUDE LAST_UPDATE MOD ------------- ------------------------- ------------------------------ ---------------- ------------------ ----- ORDERS PK_ORDERS ORDER_ID NULL 2026-09-09 100 ORDERS IX_ORDERS_CUSTOMER CUSTOMER_ID, ORDER_DATE AMOUNT 2026-09-09 120000 ORDERS IX_ORDERS_STATUS STATUS, ORDER_DATE AMOUNT 2026-08-20 900000
처럼 확인할 수 있습니다.
- 5. 가장 중요한 것: Histogram 확인
Oracle DBA가 SQL Server 통계를 튜닝하려면 **Histogram을 반드시 보시는 것을 추천**합니다.
SQL Server에서는:
DBCC SHOW_STATISTICS
를 사용합니다.
예를 들어:
DBCC SHOW_STATISTICS
(
'dbo.ORDERS',
'IX_ORDERS_CUSTOMER'
);
하면 크게 3개가 나옵니다.
1. STAT_HEADER 2. DENSITY_VECTOR 3. HISTOGRAM
이건 Oracle의:
DBA_TAB_HISTOGRAMS
를 보는 것과 가장 비슷한 부분입니다.
- 6. Histogram만 보고 싶다면
DBCC SHOW_STATISTICS
(
'dbo.ORDERS',
'IX_ORDERS_CUSTOMER'
)
WITH HISTOGRAM;
결과 예:
RANGE_HI_KEY EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS ----------------- ------------ ---------------------------- ------------------ 100 5000 0 1 200 12000 50 120 300 500000 1000 500
이 정보가 **Cardinality Estimation**을 볼 때 매우 중요합니다.
- 7. 통계가 오래되었는지 Top으로 확인
운영 DB에서 제가 특히 추천하는 쿼리입니다.
SELECT TOP (50)
s.name AS schema_name,
t.name AS table_name,
st.name AS statistics_name,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
CAST(
sp.modification_counter * 100.0
/ NULLIF(sp.rows, 0)
AS DECIMAL(10,2)
) AS modification_pct,
st.auto_created,
st.user_created,
st.no_recompute
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.stats st
ON t.object_id = st.object_id
OUTER APPLY sys.dm_db_stats_properties(
st.object_id,
st.stats_id
) sp
WHERE
t.is_ms_shipped = 0
ORDER BY
modification_pct DESC;
예:
TABLE STATISTICS ROWS MODIFICATION MOD% ------------- ----------------------------- ------------ ---------------- ------ ORDERS IX_ORDERS_CUSTOMER 50,000,000 5,000,000 10.00 CUSTOMER IX_CUSTOMER_REGION 8,000,000 700,000 8.75 SALES IX_SALES_DATE 100,000,000 4,000,000 4.00
이렇게 나오면 **통계가 stale일 가능성이 높은 객체**부터 조사할 수 있습니다.
- 8. 통계 업데이트 여부를 판단할 때 주의
`modification_counter`가 크다고 무조건 통계가 잘못된 것은 아닙니다.
예를 들어:
rows = 100,000,000 modification_counter = 100,000
이면:
변경률 = 0.1%
밖에 안 됩니다.
반면:
rows = 1,000,000 modification_counter = 300,000
이면:
변경률 = 30%
입니다.
따라서 저는:
last_updated + rows + modification_counter + Histogram + 실행계획의 Estimated Rows / Actual Rows
를 같이 봅니다.
- 9. 자동 통계인지 확인
`sys.stats`에서는 다음을 확인할 수 있습니다.
SELECT
s.name AS schema_name,
t.name AS table_name,
st.name AS statistics_name,
st.auto_created,
st.user_created,
st.no_recompute,
STATS_DATE(
st.object_id,
st.stats_id
) AS last_updated
FROM sys.tables t
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.stats st
ON t.object_id = st.object_id
WHERE t.is_ms_shipped = 0
ORDER BY
s.name,
t.name,
st.stats_id;
여기서:
auto_created = 1
이면 SQL Server가 자동 생성한 통계이고,
user_created = 1
이면 사용자가 만든 통계입니다.
- 10. 통계의 실제 컬럼 확인
어떤 통계가 **어떤 컬럼을 대상으로 만들어졌는지** 확인하려면:
SELECT
s.name AS schema_name,
t.name AS table_name,
st.name AS statistics_name,
sc.stats_column_id,
c.name AS column_name,
sc.stats_column_id
FROM sys.stats st
JOIN sys.tables t
ON st.object_id = t.object_id
JOIN sys.schemas s
ON t.schema_id = s.schema_id
JOIN sys.stats_columns sc
ON st.object_id = sc.object_id
AND st.stats_id = sc.stats_id
JOIN sys.columns c
ON sc.object_id = c.object_id
AND sc.column_id = c.column_id
WHERE
t.is_ms_shipped = 0
ORDER BY
s.name,
t.name,
st.name,
sc.stats_column_id;
예:
TABLE STATISTICS COLUMN --------- --------------------------------- ---------------- ORDERS IX_ORDERS_CUSTOMER CUSTOMER_ID ORDERS IX_ORDERS_CUSTOMER ORDER_DATE ORDERS _WA_Sys_... STATUS
이걸 보면 **인덱스 통계와 Auto Created Statistics를 구분**하기 좋습니다.
- 11. Oracle DBA 관점에서 대응시키면
대략 이렇게 생각하시면 됩니다.
| Oracle | SQL Server | | -------------------------------- | ------------------------------------------------------ | | `DBA_TAB_STATISTICS` | `sys.stats` + `dm_db_stats_properties()` | | `DBA_IND_STATISTICS` | `sys.stats` + `sys.indexes` | | `DBA_TAB_COL_STATISTICS` | `sys.stats_columns` | | `DBA_TAB_HISTOGRAMS` | `DBCC SHOW_STATISTICS ... WITH HISTOGRAM` | | `NUM_ROWS` | `rows` | | `SAMPLE_SIZE` | `rows_sampled` | | `LAST_ANALYZED` | `last_updated` | | 변경량 | `modification_counter` |
- 실전에서 특히 유용한 조합
지금까지 질문하신 **SQL 튜닝 + 파티션 + 테이블/인덱스 사이즈**까지 연결해서 보면, 저는 문제 SQL 하나를 잡은 다음 아래 순서로 확인하는 것을 추천합니다.
① Actual Execution Plan
↓
② Estimated Rows vs Actual Rows
↓
③ 해당 테이블/인덱스 Statistics
↓
④ last_updated
↓
⑤ modification_counter
↓
⑥ DBCC SHOW_STATISTICS → Histogram
↓
⑦ Logical Reads
↓
⑧ Index / Query 수정
특히 **`Estimated Rows`와 `Actual Rows`가 크게 차이 나는 SQL**이라면 통계정보가 문제의 원인인지 먼저 확인할 가치가 큽니다.
예를 들어 실행계획이:
Estimated Rows = 10 Actual Rows = 2,500,000
이면 단순히 인덱스가 없다는 문제만 볼 것이 아니라 **통계의 `last_updated`, `modification_counter`, histogram**을 같이 확인해야 합니다.
또 **파티션 테이블**이라면 일반 테이블보다 한 단계 더 들어가서 **파티션별 통계와 Incremental Statistics 여부**까지 보는 것이 중요합니다. SQL Server의 파티션 테이블을 튜닝 중이시라면 이 부분을 별도로 확인하는 쿼리까지 붙여서 보는 것이 좋습니다.