Chỉ với một chút tinh chỉnh và cải thiện các truy vấn Postgres SQL của mình, bạn có thể cắt giảm lượng mã ứng dụng lặp lại, dễ xảy ra lỗi cần thiết để giao tiếp với cơ sở dữ liệu của mình. Điều đó thường không xảy ra, sự thay đổi như vậy cũng cải thiện hiệu suất của mã ứng dụng.
Dưới đây là một số mẹo và thủ thuật có thể giúp mã ứng dụng của bạn thuê ngoài nhiều công việc hơn cho PostgreSQL, đồng thời làm cho ứng dụng của bạn mỏng hơn và nhanh hơn.
Nâng cấp
Kể từ Postgres v9.5, có thể chỉ định điều gì sẽ xảy ra khi chèn không thành công do "xung đột". Xung đột có thể là do vi phạm chỉ mục duy nhất (bao gồm cả khóa chính) hoặc bất kỳ ràng buộc nào (được tạo trước đó bằng cách sử dụng CREATE CONSTRAINT).
Tính năng này có thể được sử dụng để đơn giản hóa logic ứng dụng chèn hoặc cập nhật vào một câu lệnh SQL duy nhất. Ví dụ:đã cho một bảng kv với key và giá trị cột, câu lệnh bên dưới sẽ chèn một hàng mới (nếu bảng không có hàng có key =’host’) hoặc cập nhật giá trị (nếu bảng có một hàng có key =’host’):
CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO kv (key, value)
VALUES ('host', '10.0.10.1')
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value;
Lưu ý rằng cột key
là khóa chính một cột của bảng và được chỉ định làm mệnh đề xung đột. Nếu bạn có khóa chính với nhiều cột, hãy chỉ định tên của chỉ mục khóa chính tại đây.
Để biết các ví dụ nâng cao, bao gồm việc chỉ định các chỉ mục và ràng buộc từng phần, hãy xem tài liệu Postgres.
Chèn .. quay lại
Câu lệnh INSERT cũng có thể trả về một hoặc nhiều hàng, giống như một câu lệnh SELECT. Nó có thể trả về các giá trị được tạo bởi các hàm, từ khóa như current_timestamp và nối tiếp / dãy / cột nhận dạng.
Ví dụ:đây là một bảng có cột nhận dạng được tạo tự động và cột chứa dấu thời gian tạo hàng:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
Chúng ta có thể sử dụng câu lệnh INSERT .. RETURNING để chỉ xác định giá trị cho cột foo và cho phép Postgres trả về các giá trị mà nó đã tạo cho id và tại cột:
db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo;
id | at | foo
----+----------------------------------+--------
1 | 2022-01-14 11:52:09.816787+01:00 | first
2 | 2022-01-14 11:52:09.816787+01:00 | second
(2 rows)
INSERT 0 2
Từ mã ứng dụng, hãy sử dụng các mẫu / API tương tự mà bạn sẽ sử dụng để chạy các câu lệnh SELECT và đọc trong các giá trị (như executeQuery () trong JDBC hoặc db.Query () trong Go).
Đây là một ví dụ khác, ví dụ này có UUID được tạo tự động:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Tương tự như INSERT, câu lệnh UPDATE và DELETE cũng có thể chứa các mệnh đề RETURNING trong Postgres. Mệnh đề RETURNING là một phần mở rộng Postgres và không phải là một phần của tiêu chuẩn SQL.
Bất kỳ trong một bộ
Từ mã ứng dụng, làm cách nào bạn tạo mệnh đề WHERE cần khớp với giá trị của cột với một tập hợp các giá trị có thể chấp nhận được? Khi số lượng giá trị được biết trước, SQL là tĩnh:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Nhưng nếu số phím không phải là 2 mà có thể là bất kỳ số nào? Bạn có xây dựng câu lệnh SQL động không? Một tùy chọn dễ dàng hơn là sử dụng mảng Postgres:
SELECT key, value FROM kv WHERE key = ANY(?)
Toán tử BẤT KỲ ở trên nhận một mảng làm đối số. Mệnh đề key =ANY (?) chọn tất cả các hàng có giá trị của key là một trong những phần tử của mảng được cung cấp. Với điều này, mã ứng dụng có thể được đơn giản hóa thành:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Cách tiếp cận này khả thi đối với một số giá trị hạn chế, nếu bạn có nhiều giá trị để so khớp, hãy xem xét các tùy chọn khác như kết hợp với bảng (tạm thời) hoặc chế độ xem cụ thể hóa.
Di chuyển hàng giữa các bảng
Có, bạn có thể xóa các hàng khỏi một bảng và chèn chúng vào một bảng khác bằng một câu lệnh SQL duy nhất! Câu lệnh INSERT chính có thể kéo vào các hàng để chèn bằng CTE, lệnh này bao bọc một DELETE.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Thực hiện tương tự trong mã ứng dụng có thể rất dài, liên quan đến việc lưu trữ toàn bộ kết quả của việc xóa trong bộ nhớ và sử dụng nó để thực hiện nhiều INSERT. Đúng là, việc di chuyển các hàng có thể không phải là trường hợp sử dụng phổ biến, nhưng nếu logic nghiệp vụ yêu cầu nó, việc tiết kiệm bộ nhớ ứng dụng và các chuyến đi vòng quanh cơ sở dữ liệu được trình bày bởi cách tiếp cận này làm cho nó trở thành giải pháp lý tưởng.
Tập hợp các cột trong bảng nguồn và bảng đích không nhất thiết phải giống nhau, tất nhiên bạn có thể sắp xếp lại, sắp xếp lại và sử dụng các hàm để thao tác các giá trị trong danh sách chọn / trả về.
Hợp nhất
Việc cung cấp các giá trị NULL trong mã ứng dụng thường mất các bước bổ sung. Ví dụ:trong Go, bạn cần sử dụng các loại như sql.NullString ; trong Java / JDBC, các hàm như resultSet.wasNull () . Đây là những công cụ cồng kềnh và dễ xảy ra lỗi.
Nếu có thể xử lý, giả sử NULL là chuỗi trống hoặc số nguyên NULL là 0, trong ngữ cảnh của một truy vấn cụ thể, bạn có thể sử dụng hàm COALESCE. Hàm COALESCE có thể biến các giá trị NULL thành bất kỳ giá trị cụ thể nào. Ví dụ:hãy xem xét truy vấn này:
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
lấy số hóa đơn và địa chỉ giao hàng của các hóa đơn phát sinh vào tháng 1 năm 2022. Có lẽ là shipping_address là NULL nếu hàng hóa không phải được vận chuyển thực tế. Ví dụ:nếu mã ứng dụng chỉ muốn hiển thị một chuỗi trống ở đâu đó trong những trường hợp như vậy, thì đơn giản hơn chỉ cần sử dụng COALESCE và xóa mã xử lý NULL trong ứng dụng.
Bạn cũng có thể sử dụng các chuỗi khác thay vì một chuỗi rỗng:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
Bạn thậm chí có thể lấy giá trị không phải NULL đầu tiên từ danh sách hoặc sử dụng chuỗi được chỉ định để thay thế. Ví dụ:để sử dụng địa chỉ thanh toán hoặc địa chỉ giao hàng, bạn có thể sử dụng:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Trường hợp
CASE là một cấu trúc hữu ích khác để xử lý dữ liệu không hoàn hảo trong đời thực. Giả sử thay vì có NULL trong shipping_address đối với các mặt hàng không thể vận chuyển, phần mềm tạo hóa đơn không quá hoàn hảo của chúng tôi đã đưa vào trạng thái "KHÔNG ĐƯỢC CỤ THỂ". Bạn muốn ánh xạ giá trị này thành một chuỗi NULL hoặc một chuỗi trống khi bạn đọc trong dữ liệu. Bạn có thể sử dụng CASE:
-- map NOT-SPECIFIED to an empty string
SELECT invoice_num,
CASE shipping_address
WHEN 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
-- same result, different syntax
SELECT invoice_num,
CASE
WHEN shipping_address = 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
CASE có một cú pháp vô duyên, nhưng về mặt chức năng tương tự như các câu lệnh switch-case trong các ngôn ngữ giống C. Đây là một ví dụ khác:
SELECT invoice_num,
CASE
WHEN shipping_address IS NULL THEN 'NOT SHIPPING'
WHEN billing_address = shipping_address THEN 'SHIPPING TO PAYER'
ELSE 'SHIPPING TO ' || shipping_address
END
FROM invoices;
Chọn .. union
Dữ liệu từ hai (hoặc nhiều) câu lệnh SELECT riêng biệt có thể được kết hợp bằng cách sử dụng UNION. Ví dụ:nếu bạn có hai bảng, một bảng đang giữ người dùng hiện tại và một bảng đã bị xóa, sau đây là cách truy vấn chúng cùng một lúc:
SELECT id, name, address, FALSE AS is_deleted
FROM users
WHERE email = ?
UNION
SELECT id, name, address, TRUE AS is_deleted
FROM deleted_users
WHERE email = ?
Hai truy vấn phải có cùng một danh sách chọn, nghĩa là, phải trả về cùng một số lượng và loại cột.
UNION cũng xóa các bản sao. Chỉ các hàng duy nhất được trả lại. Nếu bạn muốn giữ lại các hàng trùng lặp, hãy sử dụng “UNION ALL” thay vì UNION.
Khen ngợi UNION, cũng có INTERSECT và EXCEPT, hãy xem tài liệu PostgreSQL để biết thêm thông tin.
Chọn .. phân biệt trên
Các hàng trùng lặp do SELECT trả về có thể được kết hợp (nghĩa là chỉ các hàng duy nhất được trả về) bằng cách thêm từ khóa DISTINCT sau SELECT. Mặc dù đây là SQL tiêu chuẩn, Postgres cung cấp một phần mở rộng, "DISTINCT ON". Cách sử dụng hơi phức tạp nhưng trên thực tế, đây thường là cách ngắn gọn nhất để đạt được kết quả bạn cần.
Hãy xem xét một khách hàng bảng với một hàng cho mỗi khách hàng và mua hàng bảng có một hàng cho mỗi lần mua hàng của (một số) khách hàng. Truy vấn bên dưới trả về tất cả khách hàng, cùng với từng giao dịch mua của họ:
SELECT C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
Mỗi hàng của khách hàng được lặp lại cho mỗi lần mua hàng mà họ đã thực hiện. Điều gì sẽ xảy ra nếu chúng ta chỉ muốn trả lại giao dịch mua đầu tiên của khách hàng? Về cơ bản, chúng tôi muốn sắp xếp các hàng theo khách hàng, nhóm các hàng theo khách hàng, trong mỗi nhóm, sắp xếp các hàng theo thời gian mua hàng và cuối cùng chỉ trả lại hàng đầu tiên từ mỗi nhóm. Thực sự ngắn hơn khi viết điều đó trong SQL với DISTINCT ON:
SELECT DISTINCT ON (C.id) C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
Mệnh đề “DISTINCT ON (C.id)” được thêm vào thực hiện đúng những gì được mô tả ở trên. Đó là rất nhiều công việc chỉ với một vài chữ cái bổ sung!
Sử dụng các số theo thứ tự trong mệnh đề
Cân nhắc tìm nạp danh sách tên khách hàng và mã vùng số điện thoại của họ từ một bảng. Chúng tôi sẽ giả định rằng các số điện thoại ở Hoa Kỳ được lưu trữ có định dạng là (123) 456-7890
. Đối với các quốc gia khác, chúng tôi sẽ chỉ nói "KHÔNG-US" làm mã vùng.
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
Tất cả đều tốt và chúng tôi cũng có cấu trúc CASE, nhưng nếu bây giờ chúng tôi cần sắp xếp nó theo mã vùng thì sao?
Điều này hoạt động:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END ASC;
Nhưng ugh! Việc lặp lại mệnh đề trường hợp là xấu và dễ mắc lỗi. Chúng ta có thể viết một hàm được lưu trữ lấy mã quốc gia và số điện thoại và trả về mã vùng, nhưng thực sự có một tùy chọn tốt hơn:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY 3 ASC;
“ORDER BY 3” cho biết thứ tự theo trường thứ 3! Bạn phải nhớ cập nhật số khi sắp xếp lại danh sách đã chọn, nhưng nó thường đáng giá.