使用异常在复杂函数中更新 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 个,我需要创建一个函数来更新最后一个。函数必须做:

  1. 计算一位医生开出的不同药物的数量 一年内给一个病人。
  2. 统计患者一年看一位医生的次数
  3. 加上一年内患者看医生的总价值
  4. return一年内一位医生给一位患者开的最后一种药。

我还需要考虑这些可能的错误:

最后将信息保存在VISITS_SUMMARYtable中。

我已经使用 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_yearf_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 NOTICERETURN 子句替换 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 和 EXCEPTIONCHECK 限制。在我看来,最好将它存储在值为 0 的 table 中然后更新它而不是根本没有它(如果你想聚合并进行任何数学计算,你可以获得更准确的统计数据)将所有行放在 table)
  • 考虑添加索引以加快查询速度,例如 visit(visit_date) 上的索引会加快上面函数中使用的查询速度。
  • 提示:在考虑复合索引中的列顺序时,始终首先按相等索引,然后按范围索引。不同之处在于,相等运算符将第一个索引列限制为单个值(简化:当列值唯一时)。然后在该值索引的范围内根据日期列进行排序。如果日期列非常有选择性,那么差异当然可以忽略不计,但在大多数情况下并非如此。日期范围越大,性能差异越大。

编辑: 您还可以通过使用特殊变量 FOUNDNO_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?