在我们的产品中,我们有一个通用的搜索引擎,并试图优化搜索性能。查询中使用的许多表都允许空值。我们是否应该重新设计我们的表以禁止空值进行优化?
我们的产品同时在Oracle
和上运行MS SQL Server
。
在我们的产品中,我们有一个通用的搜索引擎,并试图优化搜索性能。查询中使用的许多表都允许空值。我们是否应该重新设计我们的表以禁止空值进行优化?
我们的产品同时在Oracle
和上运行MS SQL Server
。
在Oracle
,NULL
值没有被索引,即这个查询:
SELECT *
FROM table
WHERE column IS NULL
将始终使用全表扫描,因为索引不涵盖您需要的值。
不仅如此,这个查询:
SELECT column
FROM table
ORDER BY
column
出于同样的原因,还将使用全表扫描和排序。
如果您的值本质上不允许NULL
',则将该列标记为NOT NULL
。
简短的回答:是的,有条件的!
空值和性能的主要问题与前向查找有关。
如果您将一行插入到表中,并且具有空值,它将被放置在它所属的自然页面中。任何查找该记录的查询都会在适当的位置找到它。到目前为止很容易......
...但是假设页面已填满,现在该行被拥抱在其他行之间。还是顺利...
...直到行被更新,并且空值现在包含一些东西。该行的大小已超出其可用空间,因此数据库引擎必须对其进行处理。
服务器做的最快的事情是将该行从该页面移到另一个,并用前向指针替换该行的条目。不幸的是,这需要在执行查询时进行额外的查找:查找行的自然位置,查找行的当前位置。
因此,您的问题的简短回答是肯定的,使这些字段不可为空将有助于搜索性能。如果您搜索的记录中的空字段经常更新为非空字段,则尤其如此。
当然,还有其他与较大数据集相关的惩罚(特别是 I/O,尽管索引深度很小),然后您会遇到应用程序问题,即在概念上需要它们的字段中不允许空值,但是,这是另一个问题 :)
一个额外的答案,以引起对 David Aldridge 对 Quassnoi 接受的答案的评论的额外关注。
该声明:
这个查询:
SELECT * FROM table WHERE column IS NULL
将始终使用全表扫描
不是真的。下面是使用带有文字值的索引的反例:
SQL> create table mytable (mycolumn)
2 as
3 select nullif(level,10000)
4 from dual
5 connect by level <= 10000
6 /
Table created.
SQL> create index i1 on mytable(mycolumn,1)
2 /
Index created.
SQL> exec dbms_stats.gather_table_stats(user,'mytable',cascade=>true)
PL/SQL procedure successfully completed.
SQL> set serveroutput off
SQL> select /*+ gather_plan_statistics */ *
2 from mytable
3 where mycolumn is null
4 /
MYCOLUMN
----------
1 row selected.
SQL> select * from table(dbms_xplan.display_cursor(null,null,'allstats last'))
2 /
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------
SQL_ID daxdqjwaww1gr, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ * from mytable where mycolumn
is null
Plan hash value: 1816312439
-----------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 |
|* 1 | INDEX RANGE SCAN| I1 | 1 | 1 | 1 |00:00:00.01 | 2 |
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("MYCOLUMN" IS NULL)
19 rows selected.
如您所见,该索引正在被使用。
问候,罗布。
我会说测试是必需的,但很高兴了解其他人的经验。根据我在 ms sql server 上的经验,空值可以而且确实会导致大量性能问题(差异)。现在在一个非常简单的测试中,我看到查询在 45 秒内返回,当在 table create 语句中的相关字段上设置了 not null 并且超过 25 分钟没有设置时(我放弃了等待,只是在估计的查询计划)。
测试数据为 100 万行 x 20 列,由 i5-3320 普通 HD 和 8GB RAM(SQL Server 使用 2GB)/Windows 8.1 上的 SQL Server 2012 Enterprise Edition 上的 62 个随机小写字母组成。使用随机数据/不规则数据使测试成为现实的“更糟糕”的情况非常重要。在这两种情况下,表都被重新创建并重新加载了随机数据,这些数据在已经有适当可用空间的数据库文件上花费了大约 30 秒。
select count(field0) from myTable where field0
not in (select field1 from myTable) 1000000
CREATE TABLE [dbo].[myTable]([Field0] [nvarchar](64) , ...
vs
CREATE TABLE [dbo].[myTable]([Field0] [nvarchar](64) not null,
出于性能原因,两者都有表选项 data_compression = page set,其他一切都是默认的。没有索引。
alter table myTable rebuild partition = all with (data_compression = page);
没有空值是我没有专门使用的内存优化表的要求,但是 sql server 显然会做最快的事情,在这种特定情况下,这似乎很大程度上支持在数据中没有空值并在表创建。
此表上相同形式的任何后续查询将在两秒内返回,因此我假设标准默认统计信息并且可能使 (1.3GB) 表适合内存运行良好。IE
select count(field19) from myTable where field19
not in (select field18 from myTable) 1000000
另一方面,没有 null 并且不必处理 null 情况也使查询更简单、更短、更不容易出错并且通常更快。如果可能的话,最好至少在 ms sql server 上避免空值,除非它们是明确要求的并且不能合理地从解决方案中解决。
从一个新表开始,并将其大小调整为 10m 行/13GB,相同的查询需要 12 分钟,考虑到硬件和没有使用的索引,这是非常可观的。对于信息查询完全是 IO 绑定的,IO 徘徊在 20MB/s 到 60MB/s 之间。重复相同的查询需要 9 分钟。
如果您的列不包含 NULL,最好声明此列NOT NULL
,优化器可能会采用更有效的路径。
但是,如果您的列中有 NULL,则您没有太多选择(非空默认值可能会产生比它解决的问题更多的问题)。
正如 Quassnoi 提到的,NULL 在 Oracle 中没有索引,或者更准确地说,如果所有索引列都是 NULL,则不会对行进行索引,这意味着:
以下脚本演示了一种索引 NULL 值的方法:
CREATE TABLE TEST AS
SELECT CASE
WHEN MOD(ROWNUM, 100) != 0 THEN
object_id
ELSE
NULL
END object_id
FROM all_objects;
CREATE INDEX idx_null ON test(object_id, 1);
SET AUTOTRACE ON EXPLAIN
SELECT COUNT(*) FROM TEST WHERE object_id IS NULL;
在执行“NOT IN”查询时,可为空的字段会对性能产生很大影响。因为所有索引字段都设置为 null 的行没有在 B-Tree 索引中编制索引,所以 Oracle 必须执行全表扫描以检查 null 整体,即使存在索引也是如此。
例如:
create table t1 as select rownum rn from all_objects;
create table t2 as select rownum rn from all_objects;
create unique index t1_idx on t1(rn);
create unique index t2_idx on t2(rn);
delete from t2 where rn = 3;
explain plan for
select *
from t1
where rn not in ( select rn
from t2 );
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 50173 | 636K| 3162 (1)| 00:00:38 |
|* 1 | FILTER | | | | | |
| 2 | TABLE ACCESS FULL| T1 | 50205 | 637K| 24 (5)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| T2 | 45404 | 576K| 2 (0)| 00:00:01 |
---------------------------------------------------------------------------
查询必须检查空值,因此它必须对 t1 中的每一行进行 t2 的全表扫描。
现在,如果我们使字段不可为空,它可以使用索引。
alter table t1 modify rn not null;
alter table t2 modify rn not null;
explain plan for
select *
from t1
where rn not in ( select rn
from t2 );
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2412 | 62712 | 24 (9)| 00:00:01 |
| 1 | NESTED LOOPS ANTI | | 2412 | 62712 | 24 (9)| 00:00:01 |
| 2 | INDEX FULL SCAN | T1_IDX | 50205 | 637K| 21 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN| T2_IDX | 45498 | 577K| 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------
是否因为 Null 影响性能而使用它们的问题是数据库设计的平衡行为之一。您必须平衡业务需求与性能。
如果需要,应使用空值。例如,您可能在表格中有开始日期和结束日期。您通常不会知道创建记录时的结束日期。因此,无论是否影响性能,您都必须允许空值,因为数据根本不存在。但是,如果根据业务规则,数据必须在创建记录时存在,那么您不应该允许空值。这将提高性能,使编码更简单,并确保保持数据完整性。
如果您想要更改现有数据以不再允许空值,那么您必须考虑该更改的影响。首先,您知道需要将什么值放入当前为空的记录中?其次,您是否有很多代码正在使用isnull
或coalesce
你需要更新哪些(这些东西会降低性能,所以如果你不再需要检查它们,你应该更改代码)?你需要一个默认值吗?你真的可以分配一个吗?如果不考虑该字段不能再为空,则某些插入或更新代码会中断。有时人们会输入不良信息以使他们摆脱空值。所以现在价格字段需要包含十进制值和“未知”之类的东西,因此不能正确地成为十进制数据类型,然后你必须去各种长度才能进行计算。这通常会产生与创建的 null 一样糟糕或更糟的性能问题。另外,您需要检查所有代码,以及您在哪里使用过引用该文件为 null 或不为 null 的文件,
我从客户端数据进行了大量数据导入,每次我们得到一个文件,其中一些应该允许空值的字段不允许,我们得到需要在导入系统之前清理的垃圾数据。电子邮件就是其中之一。通常在不知道该值的情况下输入数据,并且通常是某种类型的字符串数据,因此用户可以在此处键入任何内容。我们去导入电子邮件并找到“我不知道”的东西。很难尝试实际向“我不知道”发送电子邮件。如果系统需要一个有效的电子邮件地址并检查是否存在 @ 符号之类的东西,我们会得到“I@dont.know”这样的垃圾数据对数据的用户有什么用处?
一些与 null 相关的性能问题是由编写 nonsargable 查询造成的。有时只是重新排列 where 子句而不是消除必要的 null 可以提高性能。
根据我的经验,NULL 是一个有效值,通常意味着“不知道”。如果您不知道,那么为该列添加一些默认值或尝试强制执行一些 NOT NULL 约束确实是没有意义的。NULL 恰好是一种特殊情况。
NULL 的真正挑战是它使检索变得有点复杂。例如,您不能说 WHERE column_name IN (NULL,'value1','value2')。
就个人而言,如果您发现很多列,或者某些列包含很多 NULL,我认为您可能需要重新访问您的数据模型。也许那些空列可以放入子表中?例如:一个带有电话号码的表格,其中包括姓名、家庭电话、手机、传真号码、工作号码、紧急号码等……您可能只填充其中的一两个,这样会更好地对其进行规范化。
您需要做的是退后一步,看看如何访问数据。这是一个应该有值的列吗?这是仅在某些情况下具有值的列吗?这是一个会被大量查询的列吗?