日期列上带有where is null子句的optimize count(*)

9o685dep  于 2021-08-09  发布在  Java
关注(0)|答案(3)|浏览(235)

在我的数据库中,我们将审计信息存储在与当前记录相同的表中。因此,像product\u customer这样的表有4列product\u id、customer\u id、start\u date、end\u date,其中product\u id、customer\u id和start date是主键列。每当我们必须结束与客户的产品关联时,我们都会更新该记录的结束日期,如果购买了新产品,我们会插入一个新记录,其中包含相应的客户和产品id值,开始日期为购买日期,结束日期为空。
这张表格现在有近6000万条记录可以追溯到20年前,其中大约65%是审计信息。我建议将至少有15年历史的数据移动到存档表中,但这与某些保留策略有关,因此必须得到很多人的批准。我目前的oracledb版本是10g,但它将在几周内迁移到12c。
有一个新的要求,我们需要返回与特定产品相关联的客户计数,但问题是有些产品拥有数百万客户,因此我编写的查询需要花费大量时间(有时20分钟)来执行。

SELECT count(customer_id)
FROM product_customer
WHERE product_id = i_product_id AND end_date IS NULL;

既然我们正在迁移到oracle12c,有没有办法加快这个查询(我听说b-tree索引不索引空值,位图索引索引索引索引空值,但是由于这个表上经常运行dml语句,所以位图索引在这里不起作用)。
你能帮忙吗。

holgip5t

holgip5t1#

您的问题是在一个表中存储记录的历史和实际版本。
基本上,这个概念是好的,因为它可以很容易地制定涵盖实际数据和历史数据的查询。对于有限的数据,它也有很好的表现,但在某种程度上,您应该注意,实际数据的查询不会扫描历史数据
oracle开发了分区选项来解决这个问题。
您需要定义一个具有两个分区的表—一个用于实际数据,另一个用于历史记录。
使用 LIST 分区如下:

create table prod_hist
(product_id int not null, 
customer_id int not null, 
start_date date, 
end_date date)
partition by list (end_date)
(PARTITION p_active values(NULL),
 PARTITION p_history values(default)
);

重要的是,每次客户产品更改后,您都需要更新 end_date (来自 null 所以你必须允许行移动

alter table prod_hist enable row movement;

TechNicAll该行将从活动分区中删除并插入到历史分区中。
现在您的查询将 full table scan 但仅在活动分区上。
例子

EXPLAIN PLAN  SET STATEMENT_ID = 'jara1' into   plan_table  FOR
SELECT count(customer_id)
FROM prod_hist
WHERE product_id = 1 AND end_date IS NULL;

SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'jara1','ALL'));

----------------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |     1 |    22 |  1058  (23)| 00:00:13 |       |       |
|   1 |  SORT AGGREGATE        |           |     1 |    22 |            |          |       |       |
|   2 |   PARTITION LIST SINGLE|           |   672 | 14784 |  1058  (23)| 00:00:13 |   KEY |   KEY |
|*  3 |    TABLE ACCESS FULL   | PROD_HIST |   672 | 14784 |  1058  (23)| 00:00:13 |     1 |     1 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter("PRODUCT_ID"=TO_NUMBER(:I_PRODUCT_ID))

活动分区将只有21m行(60m的35%),因此该查询将在几秒钟内返回。
如果你认为完全扫描很慢,我需要一个索引,这很可能在这里不起作用。
请记住,索引访问是您当前的问题(至少我猜是1/6),使用相同的索引访问可以将运行时间缩短到7分钟(从20分钟减少35%)。
为什么?在大多数情况下,你有数以百万计的客户,但只有成千上万的产品。这意味着您需要访问活动分区的很大一部分才能获得所有客户,而完全扫描比索引访问更好。
不管怎样,如果你有一个不同的设置,有少量的客户和大量的(个别的)产品,你可能会从索引中获利

create index prod_hist_idx1 on prod_hist(product_id,customer_id) LOCAL;

请注意 LOCAL 意味着您将只使用活动分区上的索引。另外,product\u id和customer\u id都在索引中,因此您根本不需要访问该表。
以下是预期计划:

---------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name           | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                |     1 |    22 |   741   (0)| 00:00:09 |       |       |
|   1 |  SORT AGGREGATE        |                |     1 |    22 |            |          |       |       |
|   2 |   PARTITION LIST SINGLE|                |   672 | 14784 |    58   (0)| 00:00:01 |   KEY |   KEY |
|*  3 |    INDEX RANGE SCAN    | PROD_HIST_IDX1 |   672 | 14784 |    58   (0)| 00:00:01 |     1 |     1 |
---------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("PRODUCT_ID"=TO_NUMBER(:I_PRODUCT_ID))
yshpjwxd

yshpjwxd2#

您的查询应该能够利用上的索引 product_customer(product_id, end_date) .
但是,即使扫描6000万行的表也不需要20分钟。其他事情可能会干扰,例如:
系统上的其他进程锁定记录/表。
使用视图而不是表。
我还将指出,我所看到的许多系统——特别是oracle——使用的是“无限”结束日期,而不是 NULL 在这种情况下。 DATE '4195-01-01' 似乎很受欢迎;我不知道这是地方主义还是更普遍的真理。

oprakyz7

oprakyz73#

您可以尝试以下索引:

create index product_customer_ai on product_customer(product_id, end_date) compress 1;

product_id 如果不为空,则每个表记录都将始终有一个索引项,即使对于结束日期为空的记录也是如此。
压缩使索引更紧凑,占用磁盘空间更少,读取速度更快。
别忘了收集新索引的统计数据

begin
  dbms_stats.gather_index_stats(ownname => user,indname => 'PRODUCT_CUSTOMER_AI');
end;
/

相关问题