使用异常在复杂函数中更新 table
Update table in a complex function using exceptions
我在尝试解决问题时有点迷茫。一开始我有这 5 tables:
CREATE TABLE DOCTOR (
Doc_Number INTEGER,
Name VARCHAR(50) NOT NULL,
Specialty VARCHAR(50) NOT NULL,
Address VARCHAR(50) NOT NULL,
City VARCHAR(30) NOT NULL,
Phone VARCHAR(10) NOT NULL,
Salary DECIMAL(8,2) NOT NULL,
DNI VARCHAR(10) UNIQUE,
CONSTRAINT pk_Doctor PRIMARY KEY (Doc_Number),
CONSTRAINT ck_Salary CHECK (Salary >0)
);
CREATE TABLE PATIENT (
Pat_Number INTEGER,
Name VARCHAR(50) NOT NULL,
Address VARCHAR(50) NOT NULL,
City VARCHAR(30) NOT NULL,
DNI VARCHAR(10) UNIQUE,
CONSTRAINT pk_PATIENT PRIMARY KEY (Pat_Number)
);
CREATE TABLE VISIT (
Doc_Number INTEGER,
Pat_Number INTEGER,
Visit_Date DATE,
Price DECIMAL(7,2),
Last_Drug VARCHAR(50),
CONSTRAINT Visit_pk PRIMARY KEY (Doc_Number, Pat_Number, Visit_Date),
CONSTRAINT ck_Price CHECK (Price >0),
CONSTRAINT Visit_Doctor_fk FOREIGN KEY (Doc_Number) REFERENCES DOCTOR(Doc_Number),
CONSTRAINT Visit_PATIENT_fk FOREIGN KEY (Pat_Number) REFERENCES PATIENT(Pat_Number)
);
CREATE TABLE PRESCRIPTION (
Presc_Number INTEGER,
Drug VARCHAR(50) NOT NULL,
Doc_Number INTEGER NOT NULL,
Pat_Number INTEGER NOT NULL,
Visit_Date DATE NOT NULL,
CONSTRAINT Prescription_pk PRIMARY KEY (Presc_Number),
CONSTRAINT Prescription_Visit_fk FOREIGN KEY (Doc_Number, Pat_Number, Visit_Date) REFERENCES VISIT(Doc_Number, Pat_Number, Visit_Date)
);
CREATE TABLE VISITS_SUMMARY (
Doc_Number INTEGER,
Pat_Number INTEGER,
Year INTEGER,
Drugs_Number INTEGER,
Visits_Number INTEGER,
Acum_Amount DECIMAL(8,2),
Last_Drug VARCHAR(50),
CONSTRAINT ck_Visits_Number CHECK (Visits_Number >0),
CONSTRAINT ck_Acum_Amount CHECK (Acum_Amount >0),
CONSTRAINT Visits_Summary_pk PRIMARY KEY (Doc_Number, Pat_Number, Year),
CONSTRAINT Summary_Doctor_fk FOREIGN KEY (Doc_Number) REFERENCES DOCTOR(Doc_Number),
CONSTRAINT Summary_PATIENT_fk FOREIGN KEY (Pat_Number) REFERENCES PATIENT(Pat_Number)
);
我已经填写了前 4 个,我需要创建一个函数来更新最后一个。函数必须做:
- 计算一位医生开出的不同药物的数量
一年内给一个病人。
- 统计患者一年看一位医生的次数
- 加上一年内患者看医生的总价值
- return一年内一位医生给一位患者开的最后一种药。
我还需要考虑这些可能的错误:
- 医生不存在
- 患者不存在
- 今年这位医生没有拜访过这位患者
最后将信息保存在VISITS_SUMMARY
table中。
我已经使用 return 在不同的函数中分别完成了前 4 点,并且有效:
CREATE OR REPLACE FUNCTION sum_visits (p_Doc_Number INTEGER, p_Pat_Number INTEGER, p_Year INTEGER)
RETURNS INTEGER AS $$
DECLARE
BEGIN
SELECT COUNT(Drug)INTO drugs_num
FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year;
RETURN drugs_num;
END;
$$LANGUAGE plpgsql;
与此相同,其他使用相同的函数参数,仅更改 return 类型。
SELECT COUNT(Visit_Date)INTO visits
FROM VISIT v
WHERE v.Doc_Number = p_Doc_Number AND v.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM v.Visit_Date)) = p_Year;
total_price = 0.0;
FOR visit_price IN SELECT Price FROM VISIT v
WHERE v.Doc_Number = p_Doc_Number AND v.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM v.Visit_Date)) = p_Year LOOP
total_price := total_price + visit_price;
END LOOP;
SELECT Drug INTO last_drg FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year AND
Presc_Number = (SELECT MAX(Presc_Number)FROM PRESCRIPTION);
我尝试使用 IF
条件来处理异常,但它不起作用。这是函数的不同操作之一的完整示例:
CREATE OR REPLACE FUNCTION sum_visits (p_Doc_Number INTEGER, p_Pat_Number INTEGER, p_Year INTEGER)
RETURNS void AS $$
DECLARE
drugs_num INTEGER;
BEGIN
IF (PRESCRIPTION.Doc_Number NOT IN (p_Doc_Number))THEN
RAISE EXCEPTION
'Doctor % doesn"t exists';
ELSIF (PRESCRIPTION.Pat_Number NOT IN (p_Pat_Number))THEN
RAISE EXCEPTION
'Patient doesn"t exists';
ELSIF((SELECT EXTRACT(YEAR FROM PRESCRIPTION.Visit_Date)) NOT IN p_Year) THEN
RAISE EXCEPTION
'Date % doesn"t exists'
ELSE SELECT COUNT(Drug)INTO drugs_num
FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year;
end if;
update VISITS_SUMMARY
set Drugs_Number = drugs_num;
exception
when raise_exception THEN
RAISE EXCEPTION' %: %',SQLSTATE, SQLERRM;
END;
$$LANGUAGE plpgsql;
我需要帮助来使用更新语句,因为即使不考虑异常似乎 table 也不会更新,并且需要一些控制异常的帮助。
有一些示例可以填充第一个 table 并使用此参数调用函数 (26902, 6574405, 2011)
INSERT INTO DOCTOR (Doc_number,Name,Specialty,Address,City,Phone,Salary,DNI) values
(26902,'Dr. Alemany','Traumatologia','C. Muntaner, 55,','Barcelona','657982553',71995,'52561523T');
INSERT INTO PATIENT (Pat_Number, Name, Address, City, DNI) values
(6574405,'Sra. Alemany ','C. Muntaner, 80','Barcelona','176784267B');
INSERT INTO VISIT (Doc_Number, Pat_Number,Visit_Date,Price) values
(26902,6574405,'30/03/11',215);
INSERT INTO PRESCRIPTION (Presc_Number, Drug, Doc_Number, Pat_Number, Visit_Date) values
(44,'Diclofenac',26902,6574405,'30/03/11')
, (45,'Ibuprofè',26902,6574405,'30/03/11')
, (46,'Ibuprofè',26902,6574405,'30/03/11');
如果你想要的话,我还有更多的插页。
下面是我假设的函数。
可能需要一些解释,所以这里是:
f_start_of_year
和 f_end_of_year
被构造为进行查询 sargable (能够使用索引来加速它的执行),因为函数是黑盒到 Postgres 优化器,因此执行查询 WHERE function(visit_date) ...
无法使用在列 visit_date
上声明的索引。对于这种特殊情况,您需要在 to_char(visit_date, 'YYYY')
上建立索引,例如以获得 2011
作为字符结果。最好有一个索引并根据它调整查询,而不是相反。另一方面,Postgres 确实非常快速地评估运算符的右侧,而左侧仍然存在,以便它与索引条件相匹配。
一开始我们正在检查医生、患者和访问的存在。
如果您想更新所有不同医生、患者记录的统计信息,那么调用可能类似于
SELECT sum_visits(doc_number, pat_number, 2011)
FROM (
SELECT doc_number, pat_number
FROM visit
GROUP BY 1,2
) foo;
在计算药物数量时我放了 COUNT(DISTINCT drug)
因为你说过你想计算不同药物的存在(所以这里如果医生为一个特定的病人开了两次药,它只会计为 1。要删除此行为,只需删除 DISTINCT
子句。
考虑用 RAISE NOTICE
和 RETURN
子句替换 RAISE EXCEPTION
- 请参阅手册以供参考。提高 EXCEPTION
可防止进一步执行函数。
然后一个函数建立必须完成的操作 INSERT/UPDATE
- 因为您可能想要比一年一次更频繁地计算统计信息,所以 INSERT
语句将因 visits_Summary_pk
至于 RETURN
值 - 您将获得有关函数执行的操作以及特定行的统计信息 updated/inserted 的确切信息。这样你就可以做一些日志记录。它还可以帮助您进行调试。
CREATE OR REPLACE FUNCTION sum_visits (p_doc_number INTEGER, p_pat_number INTEGER, p_year INTEGER)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
f_start_of_year date := p_year || '-01-01';
f_end_of_year date := (p_year || '-01-01')::DATE + '1 year - 1 day'::INTERVAL;
f_drug_count integer := 0;
f_visits_count integer := 0;
f_price_sum decimal(8,2) := 0.00;
f_last_drug varchar(50);
f_check_if_record_exists boolean;
BEGIN
-- Checking
IF (SELECT count(*) FROM doctor WHERE doc_number = p_doc_number) = 0 THEN
RAISE EXCEPTION 'Doctor % does not exist', p_doc_number;
END IF;
IF (SELECT count(*) FROM patient WHERE pat_number = p_pat_number) = 0 THEN
RAISE EXCEPTION 'Patient % does not exist', p_doc_number;
END IF;
IF (SELECT count(*) FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year) = 0 THEN
RAISE EXCEPTION 'There are no visits for doctor %, patient % in year %', p_doc_number, p_pat_number, p_year;
END IF;
SELECT COUNT(DISTINCT drug) INTO f_drug_count
FROM prescription WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT COUNT(*) INTO f_visits_count
FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT SUM(price) INTO f_price_sum
FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT drug INTO f_last_drug
FROM prescription WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year ORDER BY visit_date DESC, presc_number DESC LIMIT 1;
SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END INTO f_check_if_record_exists FROM visits_summary WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND year = p_year;
IF (f_check_if_record_exists = 'f') THEN
INSERT INTO visits_summary(doc_number, pat_number, year, drugs_number, visits_number, acum_amount, last_drug)
VALUES (p_doc_number, p_pat_number, p_year, f_drug_count, f_visits_count, f_price_sum, f_last_drug);
ELSE
UPDATE visits_summary SET
drugs_number = f_drug_count, visits_number = f_visits_count, acum_amount = f_price_sum, last_drug = f_last_drug
WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND year = p_year;
END IF;
RETURN CONCAT(CASE f_check_if_record_exists WHEN true THEN 'Updated' ELSE 'Inserted into' END || ' visits_summary for Doctor_ID: ',p_doc_number,' / Patient_ID: ',p_pat_number,' / Year: ',p_year,E'\n','WITH VALUES: drug_count: ',f_drug_count,', visits_count: ',f_visits_count,', price_sum: ',f_price_sum,', last_drug: ',COALESCE(f_last_drug,'none'));
END;
$$;
一些一般提示:
- 不要将
varchar(n)
用作列类型。 varchar(n)
和 varchar
之间没有性能差异,但稍后更改它的大小可能会伤害你。如果你真的想限制存储在列中的字符,你最好使用 varchar(n)
和额外的 CHECK
约束 - 将来很容易更改它。 Read first tip in documentation about character types
- 考虑删除对
visits_summary
table 和 EXCEPTION
的 CHECK
限制。在我看来,最好将它存储在值为 0 的 table 中然后更新它而不是根本没有它(如果你想聚合并进行任何数学计算,你可以获得更准确的统计数据)将所有行放在 table) 中
- 考虑添加索引以加快查询速度,例如
visit(visit_date)
上的索引会加快上面函数中使用的查询速度。
- 提示:在考虑复合索引中的列顺序时,始终首先按相等索引,然后按范围索引。不同之处在于,相等运算符将第一个索引列限制为单个值(简化:当列值唯一时)。然后在该值索引的范围内根据日期列进行排序。如果日期列非常有选择性,那么差异当然可以忽略不计,但在大多数情况下并非如此。日期范围越大,性能差异越大。
编辑: 您还可以通过使用特殊变量 FOUND
或 NO_DATA_FOUND
来替换检查部分代码 - 更多信息 here
架构
有几个有趣的元素。最引人注目的是:
(Doc_Number, Pat_Number, Visit_Date)
对于主键来说似乎是个糟糕的主意。如果医生同一天响两次电话,那你就完蛋了。而是使用更实用的 serial
列作为代理主键:
- Auto increment SQL function
那你也可以把FK简化成prescription
:
CREATE TABLE visit (
<b>visit_id serial NOT NULL PRIMARY KEY</b>
, doc_number int NOT NULL
, pat_number int NOT NULL
, visit_date date NOT NULL
, price <b>int</b> -- amount in Cent -- Can be NULL?
<strike>, last_drug text</strike> -- seems misplaced
, CONSTRAINT ck_price CHECK (price > 0)
, CONSTRAINT visit_doctor_fk FOREIGN KEY (doc_number) REFERENCES doctor
, CONSTRAINT visit_patient_fk FOREIGN KEY (pat_number) REFERENCES patient
);
CREATE TABLE prescription (
presc_number int PRIMARY KEY -- might also be a serial?
, <b>visit_id int NOT NULL REFERENCES visit</b>
, drug text NOT NULL
);
我正在制作 price
代表美分的 integer
列。那便宜多了。显示为 € 很简单,我将在下面的 VIEW
中演示。
您知道 CHECK
约束 ck_Price CHECK (Price > 0)
允许 NULL 值吗?可能如预期的那样。
函数
当前接受的答案有很多优点。但并不是所有的都是好的。建议的功能复杂且效率低下,可以大大简化。
更重要的是,手工编织解决方案的整个想法是可疑的、容易出错的、昂贵的和复杂的。您 运行 进入典型的 UPSERT
problem and don't have a solution fit for concurrent use, yet. (A clean solution is under development 并且可能会或可能不会随 Postgres 9.5 一起提供。)
但是 none 这对你来说是必要的...
使用 VIEW
我强烈建议考虑用 VIEW
or, if your tables are big and you need the read performance, a MATERIALIZED VIEW
替换您的 table VISITS_SUMMARY
。那么你根本不需要一个函数。基于我上面建议的改进:
提取(年从v.visit_date)作为年
CREATE MATERIALIZED VIEW AS
SELECT DISTINCT ON (1,2,3)
v.doc_number
, v.pat_number
, extract(year FROM v.visit_date) AS year
, count(*) OVER () AS visits
, sum(v.price) OVER () / 100.0 AS acum_amount -- with 2 fractional digits
, sum(p.drugs_count) OVER () AS drugs_count
, v.visit_date AS last_visit
, p.last_drug
FROM visit v
LEFT JOIN (
SELECT DISTINCT ON (1)
visit_id
, drug AS last_drug
, count(*) OVER () AS drugs_count
FROM prescription
ORDER BY 1, presc_number DESC
LIMIT 1
) p USING (visit_id)
ORDER BY 1, 2, 3, v.visit_date DESC;
Drugs_Number
是一种非常具有误导性的药物计数名称,因为您还有 doc_number
等。使用 drugs_count
而不是
严格来说,一次就诊可以有多个处方,所以"last drug"是有歧义的。从上次访问中挑选 一种 种任意药物("last" 一种)。
表达式sum(v.price) / 100.0
自动强制结果为numeric
,因为数字常量100.0
(带小数位)is assumed to be numeric automatically。因此,price
中的 integer
值(代表美分)以所需格式显示,带有两个小数位(代表欧元)。
查询有点棘手,因为您需要来自 table 和 "last" 药物的聚合。我得到最后一种药物并首先计算每个 visit_id
,加入 visit
并使用 window 函数计算所有聚合以获得上次使用 DISTINCT ON
访问的最后一种药物。
关于 DISTINCT ON
:
- Select first row in each GROUP BY group?
我在尝试解决问题时有点迷茫。一开始我有这 5 tables:
CREATE TABLE DOCTOR (
Doc_Number INTEGER,
Name VARCHAR(50) NOT NULL,
Specialty VARCHAR(50) NOT NULL,
Address VARCHAR(50) NOT NULL,
City VARCHAR(30) NOT NULL,
Phone VARCHAR(10) NOT NULL,
Salary DECIMAL(8,2) NOT NULL,
DNI VARCHAR(10) UNIQUE,
CONSTRAINT pk_Doctor PRIMARY KEY (Doc_Number),
CONSTRAINT ck_Salary CHECK (Salary >0)
);
CREATE TABLE PATIENT (
Pat_Number INTEGER,
Name VARCHAR(50) NOT NULL,
Address VARCHAR(50) NOT NULL,
City VARCHAR(30) NOT NULL,
DNI VARCHAR(10) UNIQUE,
CONSTRAINT pk_PATIENT PRIMARY KEY (Pat_Number)
);
CREATE TABLE VISIT (
Doc_Number INTEGER,
Pat_Number INTEGER,
Visit_Date DATE,
Price DECIMAL(7,2),
Last_Drug VARCHAR(50),
CONSTRAINT Visit_pk PRIMARY KEY (Doc_Number, Pat_Number, Visit_Date),
CONSTRAINT ck_Price CHECK (Price >0),
CONSTRAINT Visit_Doctor_fk FOREIGN KEY (Doc_Number) REFERENCES DOCTOR(Doc_Number),
CONSTRAINT Visit_PATIENT_fk FOREIGN KEY (Pat_Number) REFERENCES PATIENT(Pat_Number)
);
CREATE TABLE PRESCRIPTION (
Presc_Number INTEGER,
Drug VARCHAR(50) NOT NULL,
Doc_Number INTEGER NOT NULL,
Pat_Number INTEGER NOT NULL,
Visit_Date DATE NOT NULL,
CONSTRAINT Prescription_pk PRIMARY KEY (Presc_Number),
CONSTRAINT Prescription_Visit_fk FOREIGN KEY (Doc_Number, Pat_Number, Visit_Date) REFERENCES VISIT(Doc_Number, Pat_Number, Visit_Date)
);
CREATE TABLE VISITS_SUMMARY (
Doc_Number INTEGER,
Pat_Number INTEGER,
Year INTEGER,
Drugs_Number INTEGER,
Visits_Number INTEGER,
Acum_Amount DECIMAL(8,2),
Last_Drug VARCHAR(50),
CONSTRAINT ck_Visits_Number CHECK (Visits_Number >0),
CONSTRAINT ck_Acum_Amount CHECK (Acum_Amount >0),
CONSTRAINT Visits_Summary_pk PRIMARY KEY (Doc_Number, Pat_Number, Year),
CONSTRAINT Summary_Doctor_fk FOREIGN KEY (Doc_Number) REFERENCES DOCTOR(Doc_Number),
CONSTRAINT Summary_PATIENT_fk FOREIGN KEY (Pat_Number) REFERENCES PATIENT(Pat_Number)
);
我已经填写了前 4 个,我需要创建一个函数来更新最后一个。函数必须做:
- 计算一位医生开出的不同药物的数量 一年内给一个病人。
- 统计患者一年看一位医生的次数
- 加上一年内患者看医生的总价值
- return一年内一位医生给一位患者开的最后一种药。
我还需要考虑这些可能的错误:
- 医生不存在
- 患者不存在
- 今年这位医生没有拜访过这位患者
最后将信息保存在VISITS_SUMMARY
table中。
我已经使用 return 在不同的函数中分别完成了前 4 点,并且有效:
CREATE OR REPLACE FUNCTION sum_visits (p_Doc_Number INTEGER, p_Pat_Number INTEGER, p_Year INTEGER)
RETURNS INTEGER AS $$
DECLARE
BEGIN
SELECT COUNT(Drug)INTO drugs_num
FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year;
RETURN drugs_num;
END;
$$LANGUAGE plpgsql;
与此相同,其他使用相同的函数参数,仅更改 return 类型。
SELECT COUNT(Visit_Date)INTO visits
FROM VISIT v
WHERE v.Doc_Number = p_Doc_Number AND v.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM v.Visit_Date)) = p_Year;
total_price = 0.0;
FOR visit_price IN SELECT Price FROM VISIT v
WHERE v.Doc_Number = p_Doc_Number AND v.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM v.Visit_Date)) = p_Year LOOP
total_price := total_price + visit_price;
END LOOP;
SELECT Drug INTO last_drg FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year AND
Presc_Number = (SELECT MAX(Presc_Number)FROM PRESCRIPTION);
我尝试使用 IF
条件来处理异常,但它不起作用。这是函数的不同操作之一的完整示例:
CREATE OR REPLACE FUNCTION sum_visits (p_Doc_Number INTEGER, p_Pat_Number INTEGER, p_Year INTEGER)
RETURNS void AS $$
DECLARE
drugs_num INTEGER;
BEGIN
IF (PRESCRIPTION.Doc_Number NOT IN (p_Doc_Number))THEN
RAISE EXCEPTION
'Doctor % doesn"t exists';
ELSIF (PRESCRIPTION.Pat_Number NOT IN (p_Pat_Number))THEN
RAISE EXCEPTION
'Patient doesn"t exists';
ELSIF((SELECT EXTRACT(YEAR FROM PRESCRIPTION.Visit_Date)) NOT IN p_Year) THEN
RAISE EXCEPTION
'Date % doesn"t exists'
ELSE SELECT COUNT(Drug)INTO drugs_num
FROM PRESCRIPTION pr
WHERE pr.Doc_Number = p_Doc_Number AND pr.Pat_Number = p_Pat_Number AND
(SELECT EXTRACT(YEAR FROM pr.Visit_Date)) = p_Year;
end if;
update VISITS_SUMMARY
set Drugs_Number = drugs_num;
exception
when raise_exception THEN
RAISE EXCEPTION' %: %',SQLSTATE, SQLERRM;
END;
$$LANGUAGE plpgsql;
我需要帮助来使用更新语句,因为即使不考虑异常似乎 table 也不会更新,并且需要一些控制异常的帮助。
有一些示例可以填充第一个 table 并使用此参数调用函数 (26902, 6574405, 2011)
INSERT INTO DOCTOR (Doc_number,Name,Specialty,Address,City,Phone,Salary,DNI) values
(26902,'Dr. Alemany','Traumatologia','C. Muntaner, 55,','Barcelona','657982553',71995,'52561523T');
INSERT INTO PATIENT (Pat_Number, Name, Address, City, DNI) values
(6574405,'Sra. Alemany ','C. Muntaner, 80','Barcelona','176784267B');
INSERT INTO VISIT (Doc_Number, Pat_Number,Visit_Date,Price) values
(26902,6574405,'30/03/11',215);
INSERT INTO PRESCRIPTION (Presc_Number, Drug, Doc_Number, Pat_Number, Visit_Date) values
(44,'Diclofenac',26902,6574405,'30/03/11')
, (45,'Ibuprofè',26902,6574405,'30/03/11')
, (46,'Ibuprofè',26902,6574405,'30/03/11');
如果你想要的话,我还有更多的插页。
下面是我假设的函数。
可能需要一些解释,所以这里是:
f_start_of_year
和 f_end_of_year
被构造为进行查询 sargable (能够使用索引来加速它的执行),因为函数是黑盒到 Postgres 优化器,因此执行查询 WHERE function(visit_date) ...
无法使用在列 visit_date
上声明的索引。对于这种特殊情况,您需要在 to_char(visit_date, 'YYYY')
上建立索引,例如以获得 2011
作为字符结果。最好有一个索引并根据它调整查询,而不是相反。另一方面,Postgres 确实非常快速地评估运算符的右侧,而左侧仍然存在,以便它与索引条件相匹配。
一开始我们正在检查医生、患者和访问的存在。
如果您想更新所有不同医生、患者记录的统计信息,那么调用可能类似于
SELECT sum_visits(doc_number, pat_number, 2011)
FROM (
SELECT doc_number, pat_number
FROM visit
GROUP BY 1,2
) foo;
在计算药物数量时我放了 COUNT(DISTINCT drug)
因为你说过你想计算不同药物的存在(所以这里如果医生为一个特定的病人开了两次药,它只会计为 1。要删除此行为,只需删除 DISTINCT
子句。
考虑用 RAISE NOTICE
和 RETURN
子句替换 RAISE EXCEPTION
- 请参阅手册以供参考。提高 EXCEPTION
可防止进一步执行函数。
然后一个函数建立必须完成的操作 INSERT/UPDATE
- 因为您可能想要比一年一次更频繁地计算统计信息,所以 INSERT
语句将因 visits_Summary_pk
至于 RETURN
值 - 您将获得有关函数执行的操作以及特定行的统计信息 updated/inserted 的确切信息。这样你就可以做一些日志记录。它还可以帮助您进行调试。
CREATE OR REPLACE FUNCTION sum_visits (p_doc_number INTEGER, p_pat_number INTEGER, p_year INTEGER)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
f_start_of_year date := p_year || '-01-01';
f_end_of_year date := (p_year || '-01-01')::DATE + '1 year - 1 day'::INTERVAL;
f_drug_count integer := 0;
f_visits_count integer := 0;
f_price_sum decimal(8,2) := 0.00;
f_last_drug varchar(50);
f_check_if_record_exists boolean;
BEGIN
-- Checking
IF (SELECT count(*) FROM doctor WHERE doc_number = p_doc_number) = 0 THEN
RAISE EXCEPTION 'Doctor % does not exist', p_doc_number;
END IF;
IF (SELECT count(*) FROM patient WHERE pat_number = p_pat_number) = 0 THEN
RAISE EXCEPTION 'Patient % does not exist', p_doc_number;
END IF;
IF (SELECT count(*) FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year) = 0 THEN
RAISE EXCEPTION 'There are no visits for doctor %, patient % in year %', p_doc_number, p_pat_number, p_year;
END IF;
SELECT COUNT(DISTINCT drug) INTO f_drug_count
FROM prescription WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT COUNT(*) INTO f_visits_count
FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT SUM(price) INTO f_price_sum
FROM visit WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year;
SELECT drug INTO f_last_drug
FROM prescription WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND visit_date BETWEEN f_start_of_year AND f_end_of_year ORDER BY visit_date DESC, presc_number DESC LIMIT 1;
SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END INTO f_check_if_record_exists FROM visits_summary WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND year = p_year;
IF (f_check_if_record_exists = 'f') THEN
INSERT INTO visits_summary(doc_number, pat_number, year, drugs_number, visits_number, acum_amount, last_drug)
VALUES (p_doc_number, p_pat_number, p_year, f_drug_count, f_visits_count, f_price_sum, f_last_drug);
ELSE
UPDATE visits_summary SET
drugs_number = f_drug_count, visits_number = f_visits_count, acum_amount = f_price_sum, last_drug = f_last_drug
WHERE doc_number = p_doc_number AND pat_number = p_pat_number AND year = p_year;
END IF;
RETURN CONCAT(CASE f_check_if_record_exists WHEN true THEN 'Updated' ELSE 'Inserted into' END || ' visits_summary for Doctor_ID: ',p_doc_number,' / Patient_ID: ',p_pat_number,' / Year: ',p_year,E'\n','WITH VALUES: drug_count: ',f_drug_count,', visits_count: ',f_visits_count,', price_sum: ',f_price_sum,', last_drug: ',COALESCE(f_last_drug,'none'));
END;
$$;
一些一般提示:
- 不要将
varchar(n)
用作列类型。varchar(n)
和varchar
之间没有性能差异,但稍后更改它的大小可能会伤害你。如果你真的想限制存储在列中的字符,你最好使用varchar(n)
和额外的CHECK
约束 - 将来很容易更改它。 Read first tip in documentation about character types - 考虑删除对
visits_summary
table 和EXCEPTION
的CHECK
限制。在我看来,最好将它存储在值为 0 的 table 中然后更新它而不是根本没有它(如果你想聚合并进行任何数学计算,你可以获得更准确的统计数据)将所有行放在 table) 中
- 考虑添加索引以加快查询速度,例如
visit(visit_date)
上的索引会加快上面函数中使用的查询速度。 - 提示:在考虑复合索引中的列顺序时,始终首先按相等索引,然后按范围索引。不同之处在于,相等运算符将第一个索引列限制为单个值(简化:当列值唯一时)。然后在该值索引的范围内根据日期列进行排序。如果日期列非常有选择性,那么差异当然可以忽略不计,但在大多数情况下并非如此。日期范围越大,性能差异越大。
编辑: 您还可以通过使用特殊变量 FOUND
或 NO_DATA_FOUND
来替换检查部分代码 - 更多信息 here
架构
有几个有趣的元素。最引人注目的是:
(Doc_Number, Pat_Number, Visit_Date)
对于主键来说似乎是个糟糕的主意。如果医生同一天响两次电话,那你就完蛋了。而是使用更实用的 serial
列作为代理主键:
- Auto increment SQL function
那你也可以把FK简化成prescription
:
CREATE TABLE visit (
<b>visit_id serial NOT NULL PRIMARY KEY</b>
, doc_number int NOT NULL
, pat_number int NOT NULL
, visit_date date NOT NULL
, price <b>int</b> -- amount in Cent -- Can be NULL?
<strike>, last_drug text</strike> -- seems misplaced
, CONSTRAINT ck_price CHECK (price > 0)
, CONSTRAINT visit_doctor_fk FOREIGN KEY (doc_number) REFERENCES doctor
, CONSTRAINT visit_patient_fk FOREIGN KEY (pat_number) REFERENCES patient
);
CREATE TABLE prescription (
presc_number int PRIMARY KEY -- might also be a serial?
, <b>visit_id int NOT NULL REFERENCES visit</b>
, drug text NOT NULL
);
我正在制作 price
代表美分的 integer
列。那便宜多了。显示为 € 很简单,我将在下面的 VIEW
中演示。
您知道 CHECK
约束 ck_Price CHECK (Price > 0)
允许 NULL 值吗?可能如预期的那样。
函数
当前接受的答案有很多优点。但并不是所有的都是好的。建议的功能复杂且效率低下,可以大大简化。
更重要的是,手工编织解决方案的整个想法是可疑的、容易出错的、昂贵的和复杂的。您 运行 进入典型的 UPSERT
problem and don't have a solution fit for concurrent use, yet. (A clean solution is under development 并且可能会或可能不会随 Postgres 9.5 一起提供。)
但是 none 这对你来说是必要的...
使用 VIEW
我强烈建议考虑用 VIEW
or, if your tables are big and you need the read performance, a MATERIALIZED VIEW
替换您的 table VISITS_SUMMARY
。那么你根本不需要一个函数。基于我上面建议的改进:
提取(年从v.visit_date)作为年
CREATE MATERIALIZED VIEW AS
SELECT DISTINCT ON (1,2,3)
v.doc_number
, v.pat_number
, extract(year FROM v.visit_date) AS year
, count(*) OVER () AS visits
, sum(v.price) OVER () / 100.0 AS acum_amount -- with 2 fractional digits
, sum(p.drugs_count) OVER () AS drugs_count
, v.visit_date AS last_visit
, p.last_drug
FROM visit v
LEFT JOIN (
SELECT DISTINCT ON (1)
visit_id
, drug AS last_drug
, count(*) OVER () AS drugs_count
FROM prescription
ORDER BY 1, presc_number DESC
LIMIT 1
) p USING (visit_id)
ORDER BY 1, 2, 3, v.visit_date DESC;
是一种非常具有误导性的药物计数名称,因为您还有Drugs_Number
doc_number
等。使用drugs_count
而不是严格来说,一次就诊可以有多个处方,所以"last drug"是有歧义的。从上次访问中挑选 一种 种任意药物("last" 一种)。
表达式
sum(v.price) / 100.0
自动强制结果为numeric
,因为数字常量100.0
(带小数位)is assumed to be numeric automatically。因此,price
中的integer
值(代表美分)以所需格式显示,带有两个小数位(代表欧元)。查询有点棘手,因为您需要来自 table 和 "last" 药物的聚合。我得到最后一种药物并首先计算每个
visit_id
,加入visit
并使用 window 函数计算所有聚合以获得上次使用DISTINCT ON
访问的最后一种药物。
关于DISTINCT ON
:- Select first row in each GROUP BY group?