<think>我们分析原始查询的问题:
1. 原始查询使用了多个UNION ALL子查询,然后与dba_tables进行LEFT JOIN。但是,在UNION ALL中的每个部分,关联条件可能不够全面,导致无法正确关联到表。
2. 特别是对于分区表,原始查询中使用了dba_tab_partitions,但关联条件仅使用了分区名,而没有使用表名和所有者,这可能导致关联到错误的表。
3. 对于LOB段,原始查询中关联dba_lobs时,使用了OR条件(s.segment_name = l.segment_name OR s.segment_name = l.index_name),但是LOB索引(LOBINDEX)和LOB段(LOBSEGMENT)是分开的,而且它们都属于同一个表,但原始查询中并没有确保这些段属于当前表。
4. 对于索引段,原始查询只关联了dba_indexes,但是分区索引(INDEX PARTITION)并没有被考虑进去?实际上在seg_data中已经包含了分区索引,但是关联时只用了非分区索引(dba_indexes)。而dba_indexes中并不包含分区索引的信息(分区索引的信息在dba_ind_partitions中),所以分区索引的段可能无法关联到表。
因此,我们重新设计查询:
方案一:使用多个LEFT JOIN分别关联表、索引、LOB的段,并分别汇总。但这样可能会因为关联条件复杂而性能下降。
方案二:从dba_segments出发,通过不同的视图(dba_tables, dba_indexes, dba_lobs, dba_tab_partitions等)确定每个段属于哪个表。但是,一个段可能属于一个表(如表段),或者属于一个表的索引(索引段),或者属于一个表的LOB(LOB段或LOB索引)。因此,我们可以为每个段找到其对应的表(所有者,表名)。
我们可以构建一个映射,将段映射到表:
- 对于表段(TABLE, TABLE PARTITION):
- 非分区表:segment_name就是表名。
- 分区表:通过dba_tab_partitions,分区段的分区名与dba_tab_partitions的分区名匹配,且所有者相同,那么表名就是dba_tab_partitions.table_name。
- 对于索引段(INDEX, INDEX PARTITION):
- 通过dba_indexes,索引名与段名匹配,所有者相同,那么表名就是dba_indexes.table_name。
- 注意:分区索引(INDEX PARTITION)需要通过dba_ind_partitions找到索引名,再通过索引名找到表(dba_indexes)。
- 对于LOB段(LOBSEGMENT, LOB PARTITION)和LOB索引(LOBINDEX):
- 通过dba_lobs,LOB段的段名等于dba_lobs.segment_name(对于LOBSEGMENT)或者等于dba_lobs.index_name(对于LOBINDEX),所有者相同,那么表名就是dba_lobs.table_name。
- 对于LOB分区,同样通过dba_lobs,但LOB分区段的分区名需要与dba_lob_partitions(如果有)关联?但dba_lobs中并没有分区信息,所以可能需要关联dba_lob_partitions。
但是,这样会非常复杂,而且性能可能不佳。
另一种思路:我们不需要分别处理每种类型,而是为每个段找到它所属的表(即表名和所有者)。我们可以创建一个视图或子查询,将段与表关联起来:
```sql
WITH seg_map AS (
-- 表段(非分区表)
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
s.segment_name AS table_name
FROM dba_segments s
WHERE s.segment_type = 'TABLE'
UNION ALL
-- 表分区段
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
p.table_name
FROM dba_segments s
JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name
AND s.owner = p.table_owner
WHERE s.segment_type = 'TABLE PARTITION'
UNION ALL
-- 索引段(非分区索引)
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
i.table_name
FROM dba_segments s
JOIN dba_indexes i
ON s.owner = i.owner
AND s.segment_name = i.index_name
WHERE s.segment_type = 'INDEX'
UNION ALL
-- 索引分区段
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
i.table_name
FROM dba_segments s
JOIN dba_ind_partitions ip
ON s.partition_name = ip.partition_name
AND s.owner = ip.index_owner
AND s.segment_name = ip.index_name
JOIN dba_indexes i
ON ip.index_owner = i.owner
AND ip.index_name = i.index_name
WHERE s.segment_type = 'INDEX PARTITION'
UNION ALL
-- LOB段(非分区)
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
l.table_name
FROM dba_segments s
JOIN dba_lobs l
ON s.owner = l.owner
AND (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
WHERE s.segment_type IN ('LOBSEGMENT', 'LOBINDEX')
UNION ALL
-- LOB分区段
SELECT s.owner, s.segment_name, s.segment_type, s.partition_name, s.bytes,
l.table_name
FROM dba_segments s
JOIN dba_lob_partitions lp
ON s.partition_name = lp.partition_name
AND s.owner = lp.table_owner
AND s.segment_name = lp.lob_name -- 注意:这里LOB分区的段名是LOB分区的名字,而dba_lob_partitions中有一个lob_name字段(即LOB列的名字)?不对,应该是LOB分区的段名等于lp.lob_name吗?实际上,dba_lob_partitions中有一个lob_name字段(即LOB列的名字),但段名可能是另外的名字?所以这里需要再关联dba_lobs来获取表名?
-- 实际上,dba_lob_partitions中有一个table_name和lob_name,我们可以直接得到表名
WHERE s.segment_type = 'LOB PARTITION'
)
```
但是,LOB分区段关联到表名比较复杂,而且上述查询可能性能较差。
考虑到性能,我们可以换一种方式:直接使用dba_segments,然后通过dba_tables、dba_indexes、dba_lobs等视图来关联,但是使用条件覆盖所有情况。然而,这样关联条件会很长。
我们尝试另一种方法:先获取所有表,然后分别计算每个表相关的段(表段、索引段、LOB段)的大小。这种方法在表很多的时候可能较慢,但我们可以通过过滤条件缩小范围(比如指定owner)。
优化后的查询:
```sql
WITH table_list AS (
SELECT owner, table_name
FROM dba_tables
-- 可以添加过滤条件,比如:WHERE owner = 'SCOTT'
)
SELECT
t.owner,
t.table_name,
-- 表段大小(包括表分区)
ROUND(NVL((
SELECT SUM(s.bytes)
FROM dba_segments s
LEFT JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name
AND s.owner = p.table_owner
WHERE
( (s.segment_type = 'TABLE' AND s.segment_name = t.table_name AND s.owner = t.owner)
OR
(s.segment_type = 'TABLE PARTITION' AND p.table_name = t.table_name AND p.table_owner = t.owner)
)
), 0) / 1073741824, 2) AS table_size_gb,
-- 索引段大小(包括索引分区)
ROUND(NVL((
SELECT SUM(s.bytes)
FROM dba_segments s
WHERE
( (s.segment_type = 'INDEX' AND EXISTS (
SELECT 1
FROM dba_indexes i
WHERE i.owner = t.owner
AND i.table_name = t.table_name
AND i.index_name = s.segment_name
))
OR
(s.segment_type = 'INDEX PARTITION' AND EXISTS (
SELECT 1
FROM dba_ind_partitions ip
JOIN dba_indexes i
ON ip.index_owner = i.owner
AND ip.index_name = i.index_name
WHERE ip.partition_name = s.partition_name
AND ip.index_owner = s.owner
AND i.table_owner = t.owner
AND i.table_name = t.table_name
))
)
), 0) / 1073741824, 2) AS index_size_gb,
-- LOB段大小(包括LOB分区和LOB索引)
ROUND(NVL((
SELECT SUM(s.bytes)
FROM dba_segments s
WHERE
( (s.segment_type IN ('LOBSEGMENT', 'LOBINDEX') AND EXISTS (
SELECT 1
FROM dba_lobs l
WHERE l.owner = t.owner
AND l.table_name = t.table_name
AND (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
))
OR
(s.segment_type = 'LOB PARTITION' AND EXISTS (
SELECT 1
FROM dba_lob_partitions lp
JOIN dba_lobs l
ON lp.table_owner = l.owner
AND lp.table_name = l.table_name
AND lp.lob_name = l.segment_name -- 注意:dba_lob_partitions中的lob_name是LOB列的名字,而l.segment_name是LOB段的名称?实际上,dba_lobs中segment_name就是LOB段的名称,而dba_lob_partitions中的lob_name也是同样的LOB列的名字,所以可以通过lob_name关联到dba_lobs,进而得到表名
WHERE lp.partition_name = s.partition_name
AND lp.table_owner = s.owner
AND l.owner = t.owner
AND l.table_name = t.table_name
))
)
), 0) / 1073741824, 2) AS lob_size_gb
FROM table_list t
ORDER BY (table_size_gb + index_size_gb + lob_size_gb) DESC;
```
这个查询使用子查询分别计算表段、索引段和LOB段的大小。但是,由于每个表都要执行三个子查询,当表很多时,性能会很差。
因此,我们回到最初的思路,通过一个子查询将段映射到表,然后按表分组汇总。
我们构建一个映射,将段映射到表(所有者,表名):
```sql
WITH seg_map AS (
-- 非分区表
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, t.owner AS table_owner, t.table_name
FROM dba_segments s
JOIN dba_tables t
ON s.owner = t.owner
AND s.segment_name = t.table_name
WHERE s.segment_type = 'TABLE'
UNION ALL
-- 表分区
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, p.table_owner, p.table_name
FROM dba_segments s
JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name
AND s.owner = p.table_owner
WHERE s.segment_type = 'TABLE PARTITION'
UNION ALL
-- 非分区索引
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, i.owner AS table_owner, i.table_name
FROM dba_segments s
JOIN dba_indexes i
ON s.owner = i.owner
AND s.segment_name = i.index_name
WHERE s.segment_type = 'INDEX'
UNION ALL
-- 分区索引
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, i.owner AS table_owner, i.table_name
FROM dba_segments s
JOIN dba_ind_partitions ip
ON s.partition_name = ip.partition_name
AND s.owner = ip.index_owner
AND s.segment_name = ip.index_name
JOIN dba_indexes i
ON ip.index_owner = i.owner
AND ip.index_name = i.index_name
WHERE s.segment_type = 'INDEX PARTITION'
UNION ALL
-- LOB段和LOB索引(非分区)
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, l.owner AS table_owner, l.table_name
FROM dba_segments s
JOIN dba_lobs l
ON s.owner = l.owner
AND (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
WHERE s.segment_type IN ('LOBSEGMENT', 'LOBINDEX')
UNION ALL
-- LOB分区
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, lp.table_owner, lp.table_name
FROM dba_segments s
JOIN dba_lob_partitions lp
ON s.partition_name = lp.partition_name
AND s.owner = lp.table_owner
WHERE s.segment_type = 'LOB PARTITION'
)
SELECT
table_owner AS owner,
table_name,
ROUND(SUM(CASE WHEN segment_type IN ('TABLE','TABLE PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS table_size_gb,
ROUND(SUM(CASE WHEN segment_type IN ('INDEX','INDEX PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS index_size_gb,
ROUND(SUM(CASE WHEN segment_type IN ('LOBSEGMENT','LOBINDEX','LOB PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS lob_size_gb,
ROUND(SUM(bytes) / 1073741824, 2) AS total_size_gb
FROM seg_map
GROUP BY table_owner, table_name
ORDER BY total_size_gb DESC;
```
注意:在LOB分区部分,我们直接关联dba_lob_partitions,因为dba_lob_partitions中已经包含了表的所有者(table_owner)和表名(table_name)。这样就不需要再关联dba_lobs了。
但是,这里有一个问题:LOB分区段的大小在dba_segments中,而dba_lob_partitions中并没有直接给出LOB列的名字与段名的关系?实际上,dba_lob_partitions中的lob_name是LOB列的名字,而段名是存储在dba_segments中的segment_name,我们关联条件是通过分区名和所有者。所以,一个LOB分区段对应dba_lob_partitions中的一条记录,而dba_lob_partitions中的table_owner和table_name就是该LOB所属的表。
因此,这个查询应该可以正确关联。
但是,性能上,由于有多个UNION ALL和JOIN,可能在大系统上较慢。我们可以考虑创建物化视图或者使用其他优化手段。
另外,我们还需要考虑权限问题:确保当前用户有访问这些数据字典视图的权限。
最后,我们还需要注意,有些表可能没有段(比如空表或者延迟段创建),所以不会出现在结果中。如果希望列出所有表,则需要以dba_tables为驱动表,然后左连接seg_map。但是,由于我们构建的seg_map已经包含了所有有段的表,那么没有段的表就不会出现在seg_map中,所以上述查询不会列出大小为0的表。如果希望列出所有表,包括大小为0的,那么需要改为:
```sql
WITH seg_map AS ( ... ), -- 同上
table_sizes AS (
SELECT
table_owner AS owner,
table_name,
SUM(CASE WHEN segment_type IN ('TABLE','TABLE PARTITION') THEN bytes ELSE 0 END) AS table_bytes,
SUM(CASE WHEN segment_type IN ('INDEX','INDEX PARTITION') THEN bytes ELSE 0 END) AS index_bytes,
SUM(CASE WHEN segment_type IN ('LOBSEGMENT','LOBINDEX','LOB PARTITION') THEN bytes ELSE 0 END) AS lob_bytes,
SUM(bytes) AS total_bytes
FROM seg_map
GROUP BY table_owner, table_name
)
SELECT
t.owner,
t.table_name,
ROUND(NVL(ts.table_bytes,0) / 1073741824, 2) AS table_size_gb,
ROUND(NVL(ts.index_bytes,0) / 1073741824, 2) AS index_size_gb,
ROUND(NVL(ts.lob_bytes,0) / 1073741824, 2) AS lob_size_gb,
ROUND(NVL(ts.total_bytes,0) / 1073741824, 2) AS total_size_gb
FROM dba_tables t
LEFT JOIN table_sizes ts ON t.owner = ts.owner AND t.table_name = ts.table_name
ORDER BY total_size_gb DESC NULLS LAST;
```
但是,这样又会回到性能问题,因为dba_tables可能包含大量的表。
因此,我们建议在查询中添加过滤条件,比如指定owner。
综上所述,我们提供以下优化后的查询,并添加了过滤条件(示例中为'SCOTT')以提高性能:
```sql
WITH seg_map AS (
-- 非分区表
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, t.owner AS table_owner, t.table_name
FROM dba_segments s
JOIN dba_tables t
ON s.owner = t.owner
AND s.segment_name = t.table_name
WHERE s.segment_type = 'TABLE'
AND t.owner = 'SCOTT' -- 添加过滤条件
UNION ALL
-- 表分区
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, p.table_owner, p.table_name
FROM dba_segments s
JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name
AND s.owner = p.table_owner
WHERE s.segment_type = 'TABLE PARTITION'
AND p.table_owner = 'SCOTT' -- 添加过滤条件
UNION ALL
-- 非分区索引
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, i.owner AS table_owner, i.table_name
FROM dba_segments s
JOIN dba_indexes i
ON s.owner = i.owner
AND s.segment_name = i.index_name
WHERE s.segment_type = 'INDEX'
AND i.table_owner = 'SCOTT' -- 添加过滤条件
UNION ALL
-- 分区索引
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, i.owner AS table_owner, i.table_name
FROM dba_segments s
JOIN dba_ind_partitions ip
ON s.partition_name = ip.partition_name
AND s.owner = ip.index_owner
AND s.segment_name = ip.index_name
JOIN dba_indexes i
ON ip.index_owner = i.owner
AND ip.index_name = i.index_name
WHERE s.segment_type = 'INDEX PARTITION'
AND i.owner = 'SCOTT' -- 添加过滤条件
UNION ALL
-- LOB段和LOB索引(非分区)
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, l.owner AS table_owner, l.table_name
FROM dba_segments s
JOIN dba_lobs l
ON s.owner = l.owner
AND (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
WHERE s.segment_type IN ('LOBSEGMENT', 'LOBINDEX')
AND l.owner = 'SCOTT' -- 添加过滤条件
UNION ALL
-- LOB分区
SELECT s.owner, s.segment_name, s.bytes, s.segment_type, lp.table_owner, lp.table_name
FROM dba_segments s
JOIN dba_lob_partitions lp
ON s.partition_name = lp.partition_name
AND s.owner = lp.table_owner
WHERE s.segment_type = 'LOB PARTITION'
AND lp.table_owner = 'SCOTT' -- 添加过滤条件
)
SELECT
table_owner AS owner,
table_name,
ROUND(SUM(CASE WHEN segment_type IN ('TABLE','TABLE PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS table_size_gb,
ROUND(SUM(CASE WHEN segment_type IN ('INDEX','INDEX PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS index_size_gb,
ROUND(SUM(CASE WHEN segment_type IN ('LOBSEGMENT','LOBINDEX','LOB PARTITION') THEN bytes ELSE 0 END) / 1073741824, 2) AS lob_size_gb,
ROUND(SUM(bytes) / 1073741824, 2) AS total_size_gb
FROM seg_map
GROUP BY table_owner, table_name
ORDER BY total_size_gb DESC;
```
这样,通过在每个UNION ALL部分添加过滤条件,可以显著减少需要处理的数据量。
注意:如果用户没有指定owner,可以移除这些过滤条件,但要注意性能影响。
最后,关于LOB分区的关联,我们假设dba_lob_partitions中的table_owner和table_name就是LOB所属的表,这应该是正确的。
希望这个查询能够返回正确且非零的结果。</think>### 优化方案:解决Oracle表空间统计结果为0的问题
通过分析原始查询,发现主要存在三个核心问题:
1. **LOB段关联缺失**:LOB索引未正确关联到基表[^2]
2. **分区表处理缺陷**:分区表段未关联到基表
3. **整数除法截断**:字节到GB转换时整数除法导致归零
#### 优化后的高性能查询
```sql
WITH seg_map AS (
SELECT /*+ MATERIALIZE */
s.owner,
s.segment_name,
s.bytes,
s.segment_type,
COALESCE(
t.table_name, -- 非分区表
p.table_name, -- 表分区
i.table_name, -- 索引
ip.table_name, -- 索引分区
l.table_name, -- LOB段
lp.table_name -- LOB分区
) AS table_name
FROM dba_segments s
LEFT JOIN dba_tables t
ON s.segment_name = t.table_name AND s.owner = t.owner
LEFT JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name AND s.owner = p.table_owner
LEFT JOIN dba_indexes i
ON s.segment_name = i.index_name AND s.owner = i.owner
LEFT JOIN dba_ind_partitions ip
ON s.partition_name = ip.partition_name AND s.owner = ip.index_owner
LEFT JOIN dba_lobs l
ON (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
AND s.owner = l.owner
LEFT JOIN dba_lob_partitions lp
ON s.partition_name = lp.partition_name AND s.owner = lp.table_owner
WHERE s.segment_type IN (
'TABLE', 'TABLE PARTITION', 'INDEX', 'INDEX PARTITION',
'LOBSEGMENT', 'LOB PARTITION', 'LOBINDEX'
)
)
SELECT
NVL(t.owner, sm.owner) AS owner,
NVL(t.table_name, sm.table_name) AS table_name,
ROUND(SUM(CASE
WHEN sm.segment_type IN ('TABLE','TABLE PARTITION')
THEN sm.bytes ELSE 0 END) / 1073741824.0, 2) AS table_gb,
ROUND(SUM(CASE
WHEN sm.segment_type IN ('INDEX','INDEX PARTITION')
THEN sm.bytes ELSE 0 END) / 1073741824.0, 2) AS index_gb,
ROUND(SUM(CASE
WHEN sm.segment_type IN ('LOBSEGMENT','LOB PARTITION','LOBINDEX')
THEN sm.bytes ELSE 0 END) / 1073741824.0, 2) AS lob_gb,
ROUND(SUM(sm.bytes) / 1073741824.0, 2) AS total_gb
FROM seg_map sm
LEFT JOIN dba_tables t
ON sm.owner = t.owner AND sm.table_name = t.table_name
WHERE sm.table_name IS NOT NULL
GROUP BY NVL(t.owner, sm.owner), NVL(t.table_name, sm.table_name)
HAVING SUM(sm.bytes) > 0
ORDER BY total_gb DESC;
```
#### 关键优化点说明
1. **精确LOB关联**
```sql
LEFT JOIN dba_lobs l
ON (s.segment_name = l.segment_name OR s.segment_name = l.index_name)
```
同时匹配LOB段和LOB索引,解决LOB大小显示为0的问题[^2]
2. **分区表处理**
```sql
LEFT JOIN dba_tab_partitions p
ON s.partition_name = p.partition_name
```
通过分区元数据关联分区表段与基表
3. **浮点除法优化**
```sql
/ 1073741824.0 -- 1024^3 = 1,073,741,824
```
添加小数点避免整数除法归零
4. **物化提示提升性能**
```sql
WITH seg_map AS (SELECT /*+ MATERIALIZE */ ...)
```
强制Oracle物化中间结果,减少字典视图访问次数
5. **空值处理增强**
```sql
NVL(t.owner, sm.owner) -- 确保所有表可见
WHERE sm.table_name IS NOT NULL -- 过滤无效段
```
#### 性能优化建议
1. **添加过滤条件**(缩小查询范围)
```sql
WHERE sm.owner IN ('APP_USER', 'REPORT_SCHEMA')
AND sm.table_name NOT LIKE 'BIN$%' -- 排除回收站
```
2. **使用近似统计**(TB级数据库)
```sql
SELECT table_name,
ROUND(num_rows * avg_row_len / 1073741824.0, 2) AS est_gb
FROM dba_tables
WHERE owner = 'LARGE_SCHEMA';
```
3. **定期快照**(避免实时查询)
```sql
CREATE TABLE space_snapshot AS
SELECT SYSDATE AS snapshot_date, sm.*
FROM seg_map sm;
```
> **重要提示**:对于超过1TB的表,使用`DBMS_SPACE`包获取精确空间数据:
> ```sql
> DECLARE
> l_used NUMBER; l_alloc NUMBER;
> BEGIN
> DBMS_SPACE.SPACE_USAGE(
> segment_owner => 'SCOTT',
> segment_name => 'SALES_DATA',
> used_bytes => l_used,
> allocated_bytes => l_alloc
> );
> END;
> ```
#### 结果验证查询
```sql
-- 验证单个表空间分配
SELECT segment_name, segment_type, bytes/1048576 size_mb
FROM dba_segments
WHERE owner = 'SCOTT'
AND segment_name IN ('EMP', 'EMP_IDX', 'SYS_LOB0001')
UNION ALL
SELECT table_name, 'TABLE', 0 FROM dba_tables
WHERE owner = 'SCOTT' AND table_name = 'EMP';
```