使用异常更新复杂函数中的表

时间:2015-04-24 09:13:07

标签: sql postgresql sql-update plpgsql postgresql-9.3

我很想丢失,试图解决问题。起初我有5个表:

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. 在一年内将一名医生开的最后一种药物退还给患者。
  5. 另外,我需要考虑这些可能的错误:

    • 医生不存在
    • 患者不存在
    • 今年没有这位医生去看这位病人

    最后将信息保存在VISITS_SUMMARY表中。

    我使用return和works在不同的函数中分别创造了第一个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;
    

    和其他使用相同的函数参数一样只改变返回类型。

    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;
    

    我需要帮助才能使用update语句,因为即使不考虑异常,表格也不会更新,并且对控件异常有所帮助。

    有一些示例可以填充第一个表并使用此参数调用函数(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');
    

    如果你愿意,我有更多的插页。

2 个答案:

答案 0 :(得分:2)

以下是我假设完成工作的功能。

可能需要一些解释,所以这里是:

构造

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 EXCEPTION替换为RAISE NOTICERETURN子句 - 请参阅手册以供参考。提高EXCEPTION会阻止进一步执行函数。

然后一个函数建立必须完成的操作INSERT/UPDATE - 因为您可能希望比一年更频繁地计算统计数据,因此INSERT语句将失败visits_Summary_pk < / p>

至于RETURN值 - 您可以获得该功能的确切信息以及针对特定行更新/插入的统计信息。这样你就可以做一些日志记录。它还可以帮助您进行调试。

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
  • 考虑删除CHECK表和visits_summary上的EXCEPTION约束。在我看来,最好将它存储在一个值为0的表中,然后更新它,而不是完全没有它(你可以获得更准确的统计数据,如果你想要聚合并通过在里面输入所有行来做任何数学运算一张桌子)
  • 考虑添加索引以加快查询速度,例如visit(visit_date)上的索引会加快上述函数中使用的查询。
  • 提示:在考虑复合索引中的列顺序时,始终先按等式索引,然后按范围索引。不同之处在于,等于运算符将第一个索引列限制为单个值(简化:列值唯一时)。然后在该值的范围内,根据日期列对索引进行排序。如果日期列非常有选择性,那么差异当然可以忽略不计,但在大多数情况下它并非如此。日期范围越大,性能差异就越大。

修改:您还可以使用特殊变量FOUNDNO_DATA_FOUND替换检查部分代码 - 更多内容here

答案 1 :(得分:1)

模式

有一些有趣的元素。最引人注目的是: 对于主键来说,(Doc_Number, Pat_Number, Visit_Date)似乎是一个糟糕的主意。如果文档在同一天响两次,那你就搞砸了。而是使用更实用的serial列作为代理主键:

然后您还可以简化prescription中的FK:

CREATE TABLE visit (
  visit_id serial NOT NULL PRIMARY KEY
, doc_number  int NOT NULL
, pat_number  int NOT NULL
, visit_date  date NOT NULL
, price       int  -- amount in Cent -- Can be NULL?
, last_drug   text  -- 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?
, visit_id     int NOT NULL REFERENCES visit
, drug         text NOT NULL
);

在进行此操作时,我正在制作price代表Cents的integer列。那要便宜得多。显示为€很简单,我将在下面的VIEW中进行演示。

您是否知道CHECK约束ck_Price CHECK (Price > 0)允许NULL值?可能与预期一样。

功能

目前接受的答案有很多好处。但并非所有这些都是好的。建议的功能复杂且效率低,可以大大简化。

更重要的是,手工编织解决方案的整个想法是可疑的,容易出错,昂贵且复杂。您遇到了典型的UPSERT problem并且没有适合并发使用的解决方案。 (A clean solution is under development并且可能会或可能不会附带Postgres 9.5。)

但这对你来说都不是必需的......

使用VIEW

我强烈建议您考虑使用VIEW替换您的表格VISITS_SUMMARY,或者如果您的表格较大且需要阅读效果,请MATERIALIZED VIEW。那你根本就不需要一个功能。基于我上面建议的改进:

提取(年份来自v.visit_date)AS年

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而不是

  • 严格来说,因为一次就诊可以有多个处方,所以&#34;最后一种药物&#34;很暧昧。从上次访问中选择一个一个任意药物(&#34;最后一个&#34;一个)。

  • 表达式sum(v.price) / 100.0强制结果自动为numeric,因为数字常量100.0(带小数位)is assumed to be numeric automatically。因此,integer(代表美分)中的price值以所需格式显示,带有两个小数位(代表€)。

  • 查询有点棘手,因为您需要来自两个表的聚合以及&#34; last&#34;药物。我首先得到最后一个药物并按visit_id计数,加入visit并使用窗口函数计算所有聚合,以使用DISTINCT ON获取上次访问时的最后一种药物。
    关于DISTINCT ON