PostgreSQL – Giới thiệu và so sánh với MySQL

 

PostgreSQL và MySQL là hai database quan hệ phổ biến nhất. Bài này so sánh điểm mạnh/yếu và hướng dẫn dùng PostgreSQL với NodeJS/NestJS qua TypeORM.

1. So sánh PostgreSQL vs MySQL

Tiêu chí PostgreSQL MySQL
JSON JSONB (indexed, queryable) JSON (basic)
Array Native array type Không có
Full-text search Mạnh, built-in Cơ bản
Window functions Đầy đủ Hạn chế (MySQL 8+)
CTE (WITH) Đầy đủ MySQL 8+
ACID Strict Phụ thuộc storage engine
Replication Streaming + Logical Binary log
Extensions PostGIS, pgvector, … Ít hơn
Performance write Tốt Rất tốt (InnoDB)
Performance read Tốt Tốt

Chọn PostgreSQL khi: Cần JSON phức tạp, GIS, Full-text search, complex queries. Chọn MySQL khi: Ecosystem quen thuộc, cần tốc độ write cao, simple queries.

2. Cài đặt với Docker

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

3. NestJS + TypeORM + PostgreSQL

npm install @nestjs/typeorm typeorm pg
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT ?? 5432),
      username: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: process.env.NODE_ENV !== 'production',
      ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
    }),
  ],
})
export class AppModule {}

4. Entity với PostgreSQL-specific features

// src/products/product.entity.ts
import {
  Entity, PrimaryGeneratedColumn, Column,
  Index, CreateDateColumn, UpdateDateColumn
} from 'typeorm';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Index()
  @Column({ length: 255 })
  name: string;

  @Column('decimal', { precision: 10, scale: 2 })
  price: number;

  @Column({ default: 0 })
  stock: number;

  // JSONB — lưu attributes linh hoạt
  @Column({ type: 'jsonb', nullable: true })
  attributes: Record<string, any>;

  // Array type — PostgreSQL native
  @Column({ type: 'text', array: true, default: [] })
  tags: string[];

  // Full-text search vector
  @Column({
    type: 'tsvector',
    nullable: true,
    select: false,  // Không trả về mặc định
  })
  searchVector: any;

  @Column({ nullable: true })
  categoryId: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

5. JSONB Queries

// Tìm sản phẩm có color = 'red' trong attributes JSONB
const products = await this.productRepo
  .createQueryBuilder('p')
  .where("p.attributes->>'color' = :color", { color: 'red' })
  .getMany();

// Tìm với nested JSON
const products = await this.productRepo
  .createQueryBuilder('p')
  .where("p.attributes->'specs'->>'ram' = :ram", { ram: '8GB' })
  .getMany();

// Dùng @> operator (contains)
const products = await this.productRepo
  .createQueryBuilder('p')
  .where('p.attributes @> :attrs', { attrs: JSON.stringify({ brand: 'Samsung' }) })
  .getMany();

6. Array Operations

// Tìm sản phẩm có tag 'sale'
const products = await this.productRepo
  .createQueryBuilder('p')
  .where(':tag = ANY(p.tags)', { tag: 'sale' })
  .getMany();

// Tìm sản phẩm có tất cả tags ['sale', 'new']
const products = await this.productRepo
  .createQueryBuilder('p')
  .where('p.tags @> :tags', { tags: ['sale', 'new'] })
  .getMany();
// Tạo index và trigger để tự động update search vector
// migration
await queryRunner.query(`
  ALTER TABLE products ADD COLUMN search_vector tsvector;

  CREATE INDEX products_search_idx ON products USING gin(search_vector);

  CREATE OR REPLACE FUNCTION update_products_search_vector()
  RETURNS TRIGGER AS $$
  BEGIN
    NEW.search_vector =
      setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
      setweight(to_tsvector('simple', COALESCE(NEW.description, '')), 'B');
    RETURN NEW;
  END;
  $$ LANGUAGE plpgsql;

  CREATE TRIGGER products_search_update
  BEFORE INSERT OR UPDATE ON products
  FOR EACH ROW EXECUTE FUNCTION update_products_search_vector();
`);

// Query full-text search
const results = await this.productRepo
  .createQueryBuilder('p')
  .where("p.search_vector @@ plainto_tsquery('simple', :query)", { query: keyword })
  .orderBy("ts_rank(p.search_vector, plainto_tsquery('simple', :query))", 'DESC')
  .setParameter('query', keyword)
  .getMany();

8. Window Functions

// Rank sản phẩm theo doanh thu trong từng category
const ranked = await this.dataSource.query(`
  SELECT
    id, name, category_id,
    SUM(quantity) as total_sold,
    RANK() OVER (
      PARTITION BY category_id
      ORDER BY SUM(quantity) DESC
    ) as rank_in_category
  FROM order_items oi
  JOIN products p ON p.id = oi.product_id
  GROUP BY p.id, p.name, p.category_id
`);

// Tính running total
const runningTotal = await this.dataSource.query(`
  SELECT
    date,
    revenue,
    SUM(revenue) OVER (ORDER BY date) as cumulative_revenue
  FROM daily_revenue
  ORDER BY date
`);

9. pgvector — Vector Database trong PostgreSQL

# Extension cho AI/embedding similarity search
-- Kích hoạt extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Tạo column vector
ALTER TABLE documents ADD COLUMN embedding vector(1536);

-- Tạo index
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);

-- Similarity search
SELECT id, content, 1 - (embedding <=> '[0.1, 0.2, ...]') as similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 5;

Dùng pgvector thay vì Qdrant khi đã có PostgreSQL và không cần scale vector search riêng.

10. Kết luận

PostgreSQL mạnh hơn MySQL ở:

  • JSONB: Lưu và query JSON phức tạp với index
  • Array: Native array type tiện lợi
  • Full-text search: Built-in, không cần Elasticsearch cho use case đơn giản
  • Window functions: Analytics queries mạnh mẽ
  • pgvector: Tích hợp AI embedding trực tiếp vào database

Với NodeJS/NestJS, TypeORM hỗ trợ tốt cả hai — migrate từ MySQL sang PostgreSQL khá dễ nếu dùng TypeORM.