我们的系统面临从3800万行表中选择行的性能问题。
这个包含3800万行的表存储来自客户/供应商等的信息。这些信息出现在许多其他表格中,例如发票。
主要问题是我们的数据库远未标准化。 Clients_Suppliers表有一个由3列组成的复合键,Code - varchar2(16),Category - char(2),最后一个是up_date,一个日期。一个客户端地址中的每个更改都存储在具有新日期的同一个表中。所以我们可以有这样的记录:
code ca up_date
---------------- -- --------
1234567890123456 CL 01/01/09
1234567890123456 CL 01/01/10
1234567890123456 CL 01/01/11
1234567890123456 CL 01/01/12
6543210987654321 SU 01/01/10
6543210987654321 SU 08/03/11
最糟糕的是,在每个使用客户端信息的表中,而不是完整的复合键,只存储代码和类别。例如,发票有自己的密钥,包括发票日期。所以我们可以这样:
invoice_no serial_no emission code ca
---------- --------- -------- ---------------- --
1234567890 12345 05/02/12 1234567890123456 CL
我的具体问题是我必须生成一个客户列表,其中包含在给定时间段内创建的发票。由于我必须从客户端获取最新信息,因此我必须使用max(up_date)。
所以这是我的查询(在Oracle中):
SELECT
CL.CODE,
CL.CATEGORY,
-- other address fields
FROM
CLIENTS_SUPPLIERS CL
INVOICES I
WHERE
CL.CODE = I.CODE AND
CL.CATEGORY = I.CATEGORY AND
CL.UP_DATE =
(SELECT
MAX(CL2.UP_DATE)
FROM
CLIENTS_SUPPLIERS CL2
WHERE
CL2.CODE = I.CODE AND
CL2.CATEGORY = I.CATEGORY AND
CL2.UP_DATE <= I.EMISSION
) AND
I.EMISSION BETWEEN DATE1 AND DATE2
选择178,000行需要长达7个小时。发票在DATE1和DATE2之间有300,000行。
这是一个(非常,非常,非常)糟糕的设计,我提出了一个事实,我们应该通过规范化表来改进它。这将涉及为客户端创建一个表,每个代码/类别具有一个新的int主键,另一个用于Adresses(使用客户端主键作为外键),然后在每个表中使用Adresses的主键给客户。
但这意味着改变整个系统,所以我的建议已被避开。我需要找到一种不同的提高性能的方法(显然只使用SQL)。
我已经尝试了索引,视图和临时表,但没有一个在性能方面有任何显着改进。我没有想法,有人有解决方案吗?
提前致谢!
答案 0 :(得分:1)
SELECT
CL2.CODE,
CL2.CATEGORY,
... other fields
FROM
CLIENTS_SUPPLIERS CL2 INNER JOIN (
SELECT DISTINCT
CL.CODE,
CL.CATEGORY,
I.EMISSION
FROM
CLIENTS_SUPPLIERS CL INNER JOIN INVOICES I ON CL.CODE = I.CODE AND CL.CATEGORY = I.CATEGORY
WHERE
I.EMISSION BETWEEN DATE1 AND DATE2) CL3 ON CL2.CODE = CL3.CODE AND CL2.CATEGORY = CL3.CATEGORY
WHERE
CL2.UP_DATE <= CL3.EMISSION
GROUP BY
CL2.CODE,
CL2.CATEGORY
HAVING
CL2.UP_DATE = MAX(CL2.UP_DATE)
我们的想法是将流程分开:首先我们告诉oracle给我们提供您想要的期间发票的客户列表,然后我们得到它们的最后一个版本。在你的版本中有一个针对MAX 38000000次的检查,我真的认为这是在查询中花费的大部分时间。
但是,假设它们设置正确,我不会要求索引......
答案 1 :(得分:1)
DBA有什么要说的?
他/她是否尝试过:
我不是说SQL是完美的,但如果性能随着时间的推移而降低,那么DBA真的需要看看它。
答案 2 :(得分:0)
您可以尝试重写查询以使用分析函数而不是相关子查询:
select *
from (SELECT CL.CODE, CL.CATEGORY, -- other address fields
max(up_date) over (partition by cl.code, cl.category) as max_up_date
FROM CLIENTS_SUPPLIERS CL join
INVOICES I
on CL.CODE = I.CODE AND
CL.CATEGORY = I.CATEGORY and
I.EMISSION BETWEEN DATE1 AND DATE2 and
up_date <= i.emission
) t
where t.up_date = max_up_date
您可能希望删除外部选择中的max_up_date列。
有些人已经注意到,这个查询与原始查询略有不同,因为它在所有日期都占用了up_date的最大值。原始查询具有以下条件:
CL2.UP_DATE <= I.EMISSION
然而,通过及物性,这意味着:
CL2.UP_DATE <= DATE2
因此,唯一的区别是更新日期的最大值小于原始查询中的DATE1。但是,这些行将通过与UP_DATE的比较过滤掉。
虽然这个查询的措辞略有不同,但我认为它也是一样的。我必须承认不是100%肯定,因为这是我不熟悉的数据的微妙情况。
答案 3 :(得分:0)
假设(代码,ca)的行数很小,我会尝试使用内联视图强制每张发票进行索引扫描,例如:
SELECT invoice_id,
(SELECT MAX(rowid) KEEP (DENSE_RANK FIRST ORDER BY up_date DESC
FROM clients_suppliers c
WHERE c.code = i.code
AND c.category = i.category
AND c.up_date < i.invoice_date)
FROM invoices i
WHERE i.invoice_date BETWEEN :p1 AND :p2
然后,您可以将此查询加入CLIENTS_SUPPLIERS
,希望通过rowid触发连接(300k rowid读取可忽略不计)。
您可以使用SQL对象改进上述查询:
CREATE TYPE client_obj AS OBJECT (
name VARCHAR2(50),
add1 VARCHAR2(50),
/*address2, city...*/
);
SELECT i.o.name, i.o.add1 /*...*/
FROM (SELECT DISTINCT
(SELECT client_obj(
max(name) KEEP (DENSE_RANK FIRST ORDER BY up_date DESC),
max(add1) KEEP (DENSE_RANK FIRST ORDER BY up_date DESC)
/*city...*/
) o
FROM clients_suppliers c
WHERE c.code = i.code
AND c.category = i.category
AND c.up_date < i.invoice_date)
FROM invoices i
WHERE i.invoice_date BETWEEN :p1 AND :p2) i
答案 4 :(得分:0)
相关的子查询可能会导致问题,但对我而言,真正的问题在于您的主要客户端表,您无法轻松获取最新数据而不会执行max(up_date)混乱。它实际上是历史和当前数据的混合,正如您所描述的那样设计不佳。
无论如何,它将帮助您在此和其他长时间运行的联接中拥有一个表/视图,其中只包含客户端的最新数据。因此,首先为此构建一个mat视图(未经测试):
create or replace materialized view recent_clients_view
tablespace my_tablespace
nologging
build deferred
refresh complete on demand
as
select * from
(
select c.*, rownumber() over (partition by code, category order by up_date desc, rowid desc) rnum
from clients c
)
where rnum = 1;
在代码,类别上添加唯一索引。假设这将在一些非工作时间表上定期刷新,并且您使用此查询将可以显示上次刷新日期的AS AS数据。在DW env或报告中,这通常是常态。
此视图的快照表应比具有所有历史记录的完整客户端表小。
现在,您正在为这个较小的视图执行加入发票,并在代码,类别(date1和date2之间的排放)上进行等值连接。类似的东西:
select cv.*
from
recent_clients_view cv,
invoices i
where cv.code = i.code
and cv.category = i.category
and i.emission between :date1 and :date2;
希望有所帮助。