使用多个布尔运算符的 ORACLE 上的 SQL 查询
评论:我与CRYSTAL REPORTS,一位数据库报表设计师合作了很多年。它是最早的基于 GUI 的拖放工具之一,它使没有太多数据库背景的开发人员可以更轻松地构建具有多个表和过滤条件的查询。权衡是该工具在后台写入SQL;很多时候,这是一个严重的性能消耗,因为运行报告文件的工作站必须吸收被查询的数据库表的整个内容,只是为了在客户端系统上本地运行过滤过程。那是十多年前的事了,但我看到其他下一代工具也能自动生成真正糟糕的 SQL 代码。
任何软件都无法弥补糟糕的数据库设计。您不会在第一时间就做好一切(正如其他人已经注意到的那样),但是当产品在现实世界中使用PERFORMANCE 和SCALABILITY 的需求时,稍加规划可以提供一些喘息的空间。
演示架构和测试数据
以下解决方案是在ORACLE 11g Release 2 RDBMS 系统上设计的。第一个表可以由数据库VIEW、INLINE QUERY、SUB QUERY、MATERIALIZED VIEW 甚至CURSOR 输出表示,因此本示例中讨论的“属性”可能来自多个表源并加入标准。
CREATE TABLE "ZZ_DATA_ATTRIBUTES"
( "DATA_ID" NUMBER(10,0) NOT NULL ENABLE,
"NAME" VARCHAR2(50),
"AGE" NUMBER(5,0),
"HH_SIZE" NUMBER(5,0),
"SURVEY_SCORE" NUMBER(5,0),
"DMA_REGION" VARCHAR2(100),
"LAST_CONTACT" DATE,
CONSTRAINT "ZZ_DATA_ATTRIBUTES_PK" PRIMARY KEY ("DATA_ID") ENABLE
)
/
CREATE SEQUENCE "ZZ_DATA_ATTRIBUTES_SEQ" MINVALUE 1 MAXVALUE
9999999999999999999999999999 INCREMENT BY 1 START WITH 41 CACHE 20 NOORDER NOCYCLE
/
CREATE OR REPLACE TRIGGER "BI_ZZ_DATA_ATTRIBUTES"
before insert on "ZZ_DATA_ATTRIBUTES"
for each row
begin
if :NEW."DATA_ID" is null then
select "ZZ_DATA_ATTRIBUTES_SEQ".nextval into :NEW."DATA_ID" from sys.dual;
end if;
end;
/
ALTER TRIGGER "BI_ZZ_DATA_ATTRIBUTES" ENABLE
/
SEQUENCE 和 TRIGGER 对象仅用于每个表上主键的唯一、自动递增值。
CREATE TABLE "ZZ_CONDITION_RESULTS"
( "RESULT_ID" NUMBER(10,0) NOT NULL ENABLE,
"DATA_ID" NUMBER(10,0) NOT NULL ENABLE,
"COND_ONE" NUMBER(10,0),
"COND_TWO" NUMBER(10,0),
"COND_THREE" NUMBER(10,0),
"COND_FOUR" NUMBER(10,0),
"COND_FIVE" NUMBER(10,0),
CONSTRAINT "ZZ_CONDITION_RESULTS_PK" PRIMARY KEY ("RESULT_ID") ENABLE
)
/
ALTER TABLE "ZZ_CONDITION_RESULTS" ADD CONSTRAINT "ZZ_CONDITION_RESULTS_FK"
FOREIGN KEY ("DATA_ID") REFERENCES "ZZ_DATA_ATTRIBUTES" ("DATA_ID") ENABLE
/
CREATE SEQUENCE "ZZ_CONDITION_RESULTS_SEQ" MINVALUE 1 MAXVALUE
9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE
/
CREATE OR REPLACE TRIGGER "BI_ZZ_CONDITION_RESULTS"
before insert on "ZZ_CONDITION_RESULTS"
for each row
begin
if :NEW."RESULT_ID" is null then
select "ZZ_CONDITION_RESULTS_SEQ".nextval into :NEW."RESULT_ID" from sys.dual;
end if;
end;
/
ALTER TRIGGER "BI_ZZ_CONDITION_RESULTS" ENABLE
/
表ZZ_CONDITION_RESULTS 应该是TABLE 类型。它将包含每个单独的布尔 OR 标准的结果。虽然 1000 列实际上可能不可行,但最初的方法将展示如何排列大量布尔输出并能够快速识别和隔离感兴趣的组合和模式。
样本数据
您可以选择自己的数据值,但创建这些值是为了让示例正常工作。我选择了MARKETING 的主题,其中汇总的数据是我们虚构的公司收集的关于他们客户的不同属性:客户姓名、年龄、hh_size(家庭规模)、一些基准调查的评分结果、DMA(人口营销地区)地区和上次联系客户的日期。
使用 Oracle 包结构定义的布尔参数
最初的设计是通过Oracle PL/SQL Package Object来计算业务逻辑。例如,在 OP 中:
select * from some_table
where .. and .. or .. or.. or..
or.. or.. or.. or.. or.. or.. (1000 times)
每个空白都是包中的一个单独的 Oracle 函数调用。结果表示为每个被评估属性记录的列值。
create or replace package ZZ_PKG_MARKETING_DEMO as
c_result_true constant pls_integer:= 1;
c_result_false constant pls_integer:= 0;
cursor attrib_cur is
select data_id, name, age, hh_size, survey_score, dma_region,
last_contact
from zz_data_attributes;
TYPE attrib_record_type IS RECORD (
data_id zz_data_attributes.data_id%TYPE,
name zz_data_attributes.name%TYPE,
age zz_data_attributes.age%TYPE,
hh_size zz_data_attributes.hh_size%TYPE,
survey_score zz_data_attributes.survey_score%TYPE,
dma_region zz_data_attributes.dma_region%TYPE,
last_contact zz_data_attributes.last_contact%TYPE
);
function evaluate_cond_one (
p_attrib_rec attrib_record_type) return pls_integer;
function evaluate_cond_two (
p_attrib_rec attrib_record_type) return pls_integer;
function evaluate_cond_three (
p_attrib_rec attrib_record_type) return pls_integer;
function evaluate_cond_four (
p_attrib_rec attrib_record_type) return pls_integer;
function evaluate_cond_five (
p_attrib_rec attrib_record_type) return pls_integer;
procedure main_driver;
end;
create or replace package body "ZZ_PKG_MARKETING_DEMO" is
function evaluate_cond_one (
p_attrib_rec attrib_record_type) return pls_integer
as
begin
-- Checks if person is from a DMA Region in California.
IF p_attrib_rec.dma_region like 'CA%'
THEN return c_result_true;
ELSE return c_result_false;
END IF;
end EVALUATE_COND_ONE;
function evaluate_cond_two (
p_attrib_rec attrib_record_type) return pls_integer
as
c_begin_age_range constant zz_data_attributes.age%TYPE:= 20;
c_end_age_range constant zz_data_attributes.age%TYPE:= 35;
begin
-- Part 1 of 2 Checks if person belongs to the 20 to 35 years age bracket
IF p_attrib_rec.age between c_begin_age_range and c_end_age_range
THEN return c_result_true;
ELSE return c_result_false;
END IF;
end EVALUATE_COND_TWO;
function evaluate_cond_three (
p_attrib_rec attrib_record_type) return pls_integer
as
c_lowest_age constant zz_data_attributes.age%TYPE:= 45;
begin
-- Part 2 of 2 Checks if person is from age 45 and up demographic.
IF p_attrib_rec.age >= c_lowest_age
THEN return c_result_true;
ELSE return c_result_false;
END IF;
end EVALUATE_COND_THREE;
function evaluate_cond_four (
p_attrib_rec attrib_record_type) return pls_integer
as
c_cutoff_score CONSTANT zz_data_attributes.survey_score%TYPE:= 1200;
begin
-- Checks if person's survey score is higher than c_cutoff_score
IF p_attrib_rec.survey_score >= c_cutoff_score
THEN return c_result_true;
ELSE return c_result_false;
END IF;
end EVALUATE_COND_FOUR;
function evaluate_cond_five (
p_attrib_rec attrib_record_type) return pls_integer
as
c_last_contact_period CONSTANT pls_integer:= -750;
-- Note current date is anchored to a static value so the data output
-- in this example will still work regardless of how old this post
-- may get.
c_current_date CONSTANT zz_data_attributes.last_contact%TYPE:=
to_date('03/25/2014','MM/DD/YYYY');
begin
-- Checks if person's last contact date has been in the last 750
-- days.
IF p_attrib_rec.last_contact >=
(c_current_date + c_last_contact_period)
THEN return c_result_true;
ELSE return c_result_false;
END IF;
end EVALUATE_COND_FIVE;
procedure MAIN_DRIVER
as
v_rec_attr attrib_record_type;
v_rec_cond zz_condition_results%ROWTYPE;
begin
for i in attrib_cur
loop
-- Set the input record variable with the attribute values queried by the
-- current cursor.
v_rec_attr.data_id := i.data_id;
v_rec_attr.name := i.name;
v_rec_attr.age := i.age;
v_rec_attr.hh_size := i.hh_size;
v_rec_attr.survey_score := i.survey_score;
v_rec_attr.dma_region := i.dma_region;
v_rec_attr.last_contact := i.last_contact;
-- Set each condition column value equal to their matching package function.
v_rec_cond.cond_one := evaluate_cond_one(p_attrib_rec => v_rec_attr);
v_rec_cond.cond_two := evaluate_cond_two(p_attrib_rec => v_rec_attr);
v_rec_cond.cond_three:= evaluate_cond_three(p_attrib_rec => v_rec_attr);
v_rec_cond.cond_four := evaluate_cond_four(p_attrib_rec => v_rec_attr);
v_rec_cond.cond_five := evaluate_cond_five(p_attrib_rec => v_rec_attr);
INSERT INTO zz_condition_results (data_id, cond_one, cond_two,
cond_three, cond_four, cond_five)
VALUES
( v_rec_attr.data_id,
v_rec_cond.cond_one,
v_rec_cond.cond_two,
v_rec_cond.cond_three,
v_rec_cond.cond_four,
v_rec_cond.cond_five );
结束循环;
提交;
结束 MAIN_DRIVER;
结束“ZZ_PKG_MARKETING_DEMO”;
PL/SQL 注释: 有些人可能不熟悉CUSTOM DATA TYPES,例如在过程MAIN_DRIVER 的包中定义的RECORD VARIABLE TYPE。它们为正在处理的数据提供了更易于处理和参考的标识。
简单英语中的布尔算术(嗯,有点)
可以修改名为ATTRIB_CUR 的CURSOR 以对单个记录或更小的输入数据集进行操作。现在,调用MAIN_DRIVER 过程来处理属性数据源中的所有记录(同样,这不必是单个表)。
BEGIN
ZZ_PKG_MARKETING_DEMO.MAIN_DRIVER;
END;
现在每个示例条件都已针对所有样本记录进行了评估,有几种更简单的方法来评估布尔值,当前捕获为“1”(对于TRUE)和“0”(对于@987654353)的值@)。
如果只需要满足这一系列条件中的一个(如在OR 运算符的长链中),那么WHERE 子句应如下所示:
WHERE COND_ONE = 1 OR COND_TWO = 1 OR COND_THREE = 1 OR COND_FOUR = 1 OR COND_FIVE = 1
一种速记方法可以是:
WHERE (COND_ONE + COND_TWO + COND_THREE + COND_FOUR + COND_FIVE) > 0
这能买到什么?通过在填充数据记录时处理其他静态评估(自定义条件)可以获得性能提升。一个很好的理由是每个询问此标准的后续查询都不需要再次处理业务逻辑。我们还通过具有非常、非常、非常低的基数(两个!)的决策值来利用优势
WHERE 过滤条件的第二个“速记”示例是关于最终方法将如何管理“数千个”布尔评估的线索。
可扩展性:如何连续执行数千次
假设这种方法可以扩展到 OP 中提出的数量级是不切实际的。最后一个问题:这个解决方案如何适用于 N 千个布尔值链?
提示:PIVOT 你的结果。
大量布尔条件的可扩展表设计
这里还有一个表格模型,其中包含示例数据的适合方式:
获取五个样本条件之间的多个OR关系所需的SQL可以通过聚合查询来完成:
-- For multiple OR relations:
SELECT DATA_ID
FROM ZZ_CONDITION_PIVOT
GROUP BY DATA_ID
HAVING SUM(RESULT) > 0
退伍军人可能会注意到,使用支持的数据库ANALYTICAL FUNCTIONS 可以进一步简化此语法。
这种设计应该是低维护的,在实现期间或之后引入了任意数量的布尔条件。表格设计应始终保持不变。
让我知道你的想法,看起来讨论已经转移到其他问题和贡献者上,所以这可能足够让你开始。前进!