929. Unique Email Addresses

本文探讨了电子邮件地址的结构,包括本地名称和域名部分,并详细解释了如何处理电子邮件地址中的点号和加号规则,以确定实际接收邮件的不同地址数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Every email consists of a local name and a domain name, separated by the @ sign.

For example, in alice@leetcode.comalice is the local name, and leetcode.com is the domain name.

Besides lowercase letters, these emails may contain '.'s or '+'s.

If you add periods ('.') between some characters in the local name part of an email address, mail sent there will be forwarded to the same address without dots in the local name.  For example, "alice.z@leetcode.com" and "alicez@leetcode.com" forward to the same email address.  (Note that this rule does not apply for domain names.)

If you add a plus ('+') in the local name, everything after the first plus sign will be ignored. This allows certain emails to be filtered, for example m.y+name@email.com will be forwarded to my@email.com.  (Again, this rule does not apply for domain names.)

It is possible to use both of these rules at the same time.

Given a list of emails, we send one email to each address in the list.  How many different addresses actually receive mails? 

 

Example 1:

Input: ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"]
Output: 2
Explanation: "testemail@leetcode.com" and "testemail@lee.tcode.com" actually receive mails

 

Note:

  • 1 <= emails[i].length <= 100
  • 1 <= emails.length <= 100
  • Each emails[i] contains exactly one '@' character.
public static int numUniqueEmails(String[] emails) {
        Hashtable<String, Set<String>> hashMap = new Hashtable<>();
        for (String email : emails) {
            if (email.startsWith("@") || email.endsWith("@")) continue;
            Set<String> hashSetLocal = new HashSet<>();
            String local = email.split("@")[0];
            String domain = email.split("@")[1];
            local = local.replace(".", "");
            if (local.contains("+")) {
                String[] temp = local.split("\\+");
                String part1 = temp[0];
                String part2 = "";
                if (temp.length>1) {
                    part2 = temp[1];
                }
                String part3 = "";
                if (part2.contains("+")) {
                    if (part2.split("\\+").length>1)
                    part3 = part2.split("\\+")[1];
                }
                local = part1 + part3;
            }
            if (local != null && !local.equals("")) hashSetLocal.add(local);
            hashMap.put(domain, hashSetLocal);
        }
        int res = 0;
        for (Set<String> set: hashMap.values()) res += set.size();
        return res;
    }

 

<think>我们需要将MongoDB 4.x的数据迁移到PostgreSQL。迁移过程包括以下步骤: 1. 分析MongoDB数据结构,设计PostgreSQL的表结构。 2. 导出MongoDB数据为中间格式(如JSON)。 3. 将中间格式的数据转换为PostgreSQL的插入格式。 4. 导入数据到PostgreSQL。 注意:由于MongoDB是文档数据库,PostgreSQL是关系型数据库,数据结构需要重新设计。 步骤1:设计PostgreSQL表结构 - 将MongoDB的集合(collections)映射为PostgreSQL的表(tables)。 - 将文档(documents)中的字段映射为表的列(columns)。 - 处理嵌套文档和数组:可以将其展平为多个表(关系型设计)或使用PostgreSQL的JSONB类型(如果保留文档结构)。 步骤2:导出MongoDB数据 - 使用`mongoexport`工具导出数据为JSON。 步骤3:数据转换 - 编写脚本(如Node.js、Python等)将JSON数据转换为SQL插入语句或CSV格式。 步骤4:导入到PostgreSQL - 使用`psql`命令或`pg_restore`导入数据。 下面是一个详细的示例,假设我们有一个MongoDB集合`users`,其文档结构如下: ```json { "_id": ObjectId("5f9d1a2d7e4a3b3d6c8b9a1f"), "name": "Alice", "email": "alice@example.com", "age": 30, "addresses": [ { "street": "123 Main St", "city": "New York", "zip": "10001" }, { "street": "456 Elm St", "city": "Boston", "zip": "02134" } ], "created_at": ISODate("2020-10-30T00:00:00Z") } ``` 在PostgreSQL中,我们可以设计两个表: - `users`表:存储用户基本信息。 - `addresses`表:存储用户的多个地址,与用户表通过外键关联。 设计SQL: ```sql CREATE TABLE users ( id SERIAL PRIMARY KEY, mongo_id VARCHAR(24) UNIQUE, -- 保留原MongoDB的_id,可选 name VARCHAR(100), email VARCHAR(100) UNIQUE, age INTEGER, created_at TIMESTAMPTZ ); CREATE TABLE addresses ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), street VARCHAR(100), city VARCHAR(50), zip VARCHAR(10) ); ``` 步骤2:导出MongoDB数据 ```bash mongoexport --db mydb --collection users --out users.json ``` 步骤3:数据转换(使用Node.js脚本) 安装依赖: ```bash npm install pg ``` 转换脚本(convert.js): ```javascript const fs = require('fs'); const { Client } = require('pg'); const usersData = JSON.parse(fs.readFileSync('users.json', 'utf8')); // PostgreSQL连接配置 const client = new Client({ user: 'postgres', host: 'localhost', database: 'mydb', password: 'password', port: 5432, }); async function migrate() { await client.connect(); try { for (const user of usersData) { // 插入用户表 const userRes = await client.query( `INSERT INTO users (mongo_id, name, email, age, created_at) VALUES ($1, $2, $3, $4, $5) RETURNING id`, [user._id.$oid, user.name, user.email, user.age, user.created_at.$date] ); const userId = userRes.rows[0].id; // 插入地址表 if (user.addresses && user.addresses.length > 0) { for (const address of user.addresses) { await client.query( `INSERT INTO addresses (user_id, street, city, zip) VALUES ($1, $2, $3, $4)`, [userId, address.street, address.city, address.zip] ); } } } } catch (err) { console.error('Error during migration:', err); } finally { await client.end(); } } migrate(); ``` 步骤4:运行脚本 ```bash node convert.js ``` 注意:如果数据量很大,可以使用批量插入和事务来提高性能。 对于大数据量的情况,我们可以优化脚本: - 使用事务:将整个迁移过程放在一个事务中,或者分批次使用事务。 - 批量插入:将用户数据和地址数据分别收集,然后使用`INSERT INTO ... VALUES (..), (..), ...`一次插入多条。 优化后的批量插入示例: ```javascript async function migrateBatch() { await client.connect(); try { await client.query('BEGIN'); const userValues = []; const addressValues = []; // 收集用户数据 for (const user of usersData) { userValues.push([ user._id.$oid, user.name, user.email, user.age, user.created_at.$date ]); // 收集地址数据 if (user.addresses && user.addresses.length) { for (const addr of user.addresses) { // 注意:这里还没有用户的数据库id,所以先记录mongo_id,后面再处理 addressValues.push({ mongo_id: user._id.$oid, street: addr.street, city: addr.city, zip: addr.zip }); } } } // 批量插入用户 const userPlaceholders = userValues.map((_, i) => `($${i*5+1}, $${i*5+2}, $${i*5+3}, $${i*5+4}, $${i*5+5})` ).join(','); const userQueryText = ` INSERT INTO users (mongo_id, name, email, age, created_at) VALUES ${userPlaceholders} RETURNING id, mongo_id `; const userFlatValues = userValues.flat(); const userRes = await client.query(userQueryText, userFlatValues); const insertedUsers = userRes.rows; // 返回的包括id和mongo_id // 创建mongo_id到数据库id的映射 const userMap = new Map(); insertedUsers.forEach(u => userMap.set(u.mongo_id, u.id)); // 批量插入地址 const addrValues = []; for (const addr of addressValues) { const userId = userMap.get(addr.mongo_id); if (userId) { addrValues.push([userId, addr.street, addr.city, addr.zip]); } } if (addrValues.length) { const addrPlaceholders = addrValues.map((_, i) => `($${i*4+1}, $${i*4+2}, $${i*4+3}, $${i*4+4})` ).join(','); const addrQueryText = ` INSERT INTO addresses (user_id, street, city, zip) VALUES ${addrPlaceholders} `; await client.query(addrQueryText, addrValues.flat()); } await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { await client.end(); } } ``` 注意事项: - 如果数据量极大,可能需要分批处理,每批几千条,避免内存溢出。 - 对于日期类型,MongoDB导出的JSON中是`{"$date": "ISO日期字符串"}`,需要转换为PostgreSQL的`TIMESTAMPTZ`。 - 对于ObjectId,我们通常不需要在PostgreSQL中使用,但可以保留为字符串以便于追踪。 其他复杂结构的处理: - 数组:如果数组元素是基本类型,可以使用PostgreSQL的数组类型(如`TEXT[]`)。 - 嵌套文档:如果嵌套文档结构固定,可以展平为多个列;如果变化大,可以使用JSONB类型存储整个文档。 例如,如果不想拆分地址表,可以在用户表中使用JSONB存储地址: ```sql CREATE TABLE users ( id SERIAL PRIMARY KEY, ... 其他字段, addresses JSONB ); ``` 然后在转换脚本中直接存储整个地址数组。 总结:迁移过程需要根据具体的数据结构来设计关系表,并编写转换脚本。以上提供了一个基本框架,可以根据实际情况调整。 §§相关问题§§: 1. 如何将MongoDB中的嵌套文档数组转换为PostgreSQL的JSONB类型? 2. 在迁移大数据量时,如何优化转换脚本的性能? 3. 如何处理MongoDB中的特殊数据类型(如ObjectId, ISODate)? 4. 在PostgreSQL中如何查询JSONB字段?</think># MongoDB 4.x 迁移到 PostgreSQL 的完整方案 将 MongoDB 4.x 迁移到 PostgreSQL 需要处理两种数据库模型的转换:文档模型到关系模型的映射。以下是完整的迁移方案,包含详细步骤和代码实现。 ## 迁移流程图 ```mermaid graph TD A[MongoDB 4.x] --> B[分析数据结构] B --> C[设计PostgreSQL模式] C --> D[数据提取] D --> E[数据转换] E --> F[数据加载] F --> G[验证与测试] ``` ## 迁移步骤详解 ### 1. 数据结构分析与模式设计 **MongoDB 文档示例**: ```json { "_id": ObjectId("5f9d1a2d7e4a3b3d6c8b9a1f"), "name": "Alice", "email": "alice@example.com", "age": 30, "addresses": [ { "street": "123 Main St", "city": "New York", "zip": "10001" }, { "street": "456 Elm St", "city": "Boston", "zip": "02134" } ], "created_at": ISODate("2020-10-30T00:00:00Z") } ``` **PostgreSQL 模式设计**: ```sql -- 主表 CREATE TABLE users ( id SERIAL PRIMARY KEY, mongo_id VARCHAR(24) UNIQUE, -- 保留原始MongoDB ID name VARCHAR(100), email VARCHAR(100) UNIQUE, age INTEGER, created_at TIMESTAMPTZ ); -- 地址子表(1:N关系) CREATE TABLE addresses ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, street VARCHAR(100), city VARCHAR(50), zip VARCHAR(10) ); ``` ### 2. 数据提取脚本(Node.js) ```javascript // extract.js const { MongoClient } = require('mongodb'); const fs = require('fs'); async function extractData() { const client = new MongoClient('mongodb://localhost:27017'); try { await client.connect(); const db = client.db('your_database'); const collections = await db.listCollections().toArray(); const data = {}; for (const collection of collections) { const collName = collection.name; const cursor = db.collection(collName).find({}); data[collName] = await cursor.toArray(); } fs.writeFileSync('mongodb_data.json', JSON.stringify(data, null, 2)); console.log('数据导出完成!'); } catch (err) { console.error('导出错误:', err); } finally { await client.close(); } } extractData(); ``` ### 3. 数据转换与加载脚本(Node.js + PostgreSQL) ```javascript // migrate.js const { Pool } = require('pg'); const fs = require('fs'); const data = JSON.parse(fs.readFileSync('mongodb_data.json')); // PostgreSQL 连接配置 const pool = new Pool({ user: 'postgres', host: 'localhost', database: 'your_database', password: 'your_password', port: 5432, }); // 数据类型映射 const typeMappings = { 'ObjectId': 'VARCHAR(24)', 'String': 'VARCHAR(255)', 'Number': 'NUMERIC', 'Boolean': 'BOOLEAN', 'Date': 'TIMESTAMPTZ', 'Array': 'JSONB', 'Object': 'JSONB' }; async function migrate() { const client = await pool.connect(); try { await client.query('BEGIN'); for (const [collection, documents] of Object.entries(data)) { // 创建表(如果不存在) await createTableIfNotExists(client, collection, documents); // 插入数据 for (const doc of documents) { await insertDocument(client, collection, doc); } } await client.query('COMMIT'); console.log('迁移成功!'); } catch (err) { await client.query('ROLLBACK'); console.error('迁移失败:', err); } finally { client.release(); await pool.end(); } } async function createTableIfNotExists(client, tableName, documents) { if (documents.length === 0) return; const sampleDoc = documents[0]; const columns = []; // 添加主键 columns.push('id SERIAL PRIMARY KEY'); columns.push('mongo_id VARCHAR(24) UNIQUE'); // 分析文档结构 for (const [key, value] of Object.entries(sampleDoc)) { if (key === '_id') continue; const pgType = getPostgresType(value); const columnDef = `${key} ${pgType}`; columns.push(columnDef); // 处理嵌套数组(1:N关系) if (Array.isArray(value) && typeof value[0] === 'object') { await createRelatedTable(client, tableName, key, value[0]); } } const createTableSQL = ` CREATE TABLE IF NOT EXISTS ${tableName} ( ${columns.join(',\n')} ) `; await client.query(createTableSQL); } function getPostgresType(value) { if (value === null) return 'TEXT'; if (Array.isArray(value)) { return typeof value[0] === 'object' ? 'INTEGER[]' : 'JSONB'; } const type = value.constructor.name; return typeMappings[type] || 'TEXT'; } async function createRelatedTable(client, mainTable, relationName, sampleItem) { const tableName = `${mainTable}_${relationName}`; const columns = ['id SERIAL PRIMARY KEY']; columns.push(`${mainTable}_id INTEGER REFERENCES ${mainTable}(id) ON DELETE CASCADE`); for (const [key, value] of Object.entries(sampleItem)) { const pgType = getPostgresType(value); columns.push(`${key} ${pgType}`); } const createTableSQL = ` CREATE TABLE IF NOT EXISTS ${tableName} ( ${columns.join(',\n')} ) `; await client.query(createTableSQL); } async function insertDocument(client, tableName, doc) { const keys = Object.keys(doc).filter(k => k !== '_id'); const values = []; const placeholders = []; const subInserts = []; // 主文档值 keys.forEach((key, index) => { const value = doc[key]; if (Array.isArray(value) && typeof value[0] === 'object') { // 处理嵌套数组(1:N关系) subInserts.push({ key, values: value }); } else { // 处理普通值 values.push(transformValue(value)); placeholders.push(`$${values.length}`); } }); // 添加原始MongoDB ID values.push(doc._id.toString()); placeholders.push(`$${values.length}`); const insertSQL = ` INSERT INTO ${tableName} (${keys.join(', ')}, mongo_id) VALUES (${placeholders.join(', ')}) RETURNING id `; const result = await client.query(insertSQL, values); const mainId = result.rows[0].id; // 插入关联数据 for (const { key, values: nestedDocs } of subInserts) { for (const nestedDoc of nestedDocs) { await insertRelatedDocument( client, `${tableName}_${key}`, mainId, nestedDoc ); } } } async function insertRelatedDocument(client, tableName, mainId, doc) { const keys = Object.keys(doc); const values = [mainId]; const placeholders = ['$1']; keys.forEach((key, index) => { values.push(transformValue(doc[key])); placeholders.push(`$${values.length}`); }); const insertSQL = ` INSERT INTO ${tableName} (${tableName.split('_')[0]}_id, ${keys.join(', ')}) VALUES (${placeholders.join(', ')}) `; await client.query(insertSQL, values); } function transformValue(value) { if (value === null) return null; if (value instanceof Date) return value.toISOString(); if (typeof value === 'object' && !Array.isArray(value)) return JSON.stringify(value); return value; } migrate(); ``` ## 迁移后数据验证 ### 1. 数据完整性检查 ```sql -- 检查记录数 SELECT (SELECT COUNT(*) FROM users) AS pg_users, (SELECT COUNT(*) FROM mongodb_data->'users') AS mongo_users; -- 检查关联数据 SELECT (SELECT COUNT(*) FROM addresses) AS pg_addresses, (SELECT SUM(JSONB_ARRAY_LENGTH(addresses)) FROM mongodb_data->'users') AS mongo_addresses; ``` ### 2. 数据一致性检查 ```sql -- 比较随机样本 SELECT u.name AS pg_name, (SELECT name FROM jsonb_array_elements( (SELECT data FROM mongodb_data WHERE collection = 'users') -> jsonb_array_elements->'name' )) AS mongo_name FROM users u ORDER BY RANDOM() LIMIT 10; ``` ## 迁移最佳实践 1. **分阶段迁移**: ```mermaid graph LR A[生产MongoDB] --> B[只读副本] B --> C[迁移脚本] C --> D[测试环境PostgreSQL] D --> E[验证] E --> F[生产PostgreSQL] ``` 2. **增量迁移**: ```javascript // 增量迁移示例 async function incrementalMigration(lastMigratedId) { const cursor = db.collection('users').find({ _id: { $gt: new ObjectId(lastMigratedId) } }).sort({ _id: 1 }).limit(1000); // ...迁移逻辑... return lastDocument._id.toString(); } ``` 3. **性能优化**: ```javascript // 批量插入优化 async function batchInsert(client, tableName, documents) { const values = []; const placeholders = []; documents.forEach((doc, idx) => { const rowValues = []; Object.entries(doc).forEach(([key, value]) => { if (key !== '_id') { rowValues.push(transformValue(value)); } }); rowValues.push(doc._id.toString()); values.push(...rowValues); const ph = rowValues.map((_, i) => `$${idx * rowValues.length + i + 1}` ); placeholders.push(`(${ph.join(', ')})`); }); const insertSQL = ` INSERT INTO ${tableName} (${columns.join(', ')}) VALUES ${placeholders.join(', ')} `; await client.query(insertSQL, values); } ``` ## 常见问题解决方案 1. **处理ObjectID**: ```sql -- 添加自定义函数处理ObjectID CREATE OR REPLACE FUNCTION to_timestamp(object_id TEXT) RETURNS TIMESTAMP AS $$ SELECT to_timestamp(('x' || lpad(substring(object_id, 1, 8), 8, '0'))::bit(32)::int) $$ LANGUAGE SQL IMMUTABLE; ``` 2. **处理BSON特殊类型**: ```javascript // 特殊类型转换 function transformBSON(value) { if (value instanceof Binary) { return Buffer.from(value.buffer); } if (value instanceof Decimal128) { return parseFloat(value.toString()); } if (value instanceof ObjectId) { return value.toString(); } if (value instanceof Timestamp) { return new Date(value.toNumber() * 1000); } return value; } ``` 3. **处理数组差异**: ```sql -- PostgreSQL数组操作 SELECT id, ARRAY(SELECT jsonb_array_elements_text(tags)) AS tags_array FROM users; ``` ## 迁移后优化 1. **索引优化**: ```sql -- 为常用查询字段创建索引 CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_created_at ON users(created_at); -- GIN索引用于JSONB字段 CREATE INDEX idx_users_metadata ON users USING GIN(metadata); ``` 2. **查询重写**: ```sql -- MongoDB查询 db.users.find({ age: { $gt: 25 } }) -- PostgreSQL等效 SELECT * FROM users WHERE age > 25; ``` 3. **事务处理**: ```javascript // PostgreSQL事务示例 async function transferFunds(from, to, amount) { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query( 'UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, from] ); await client.query( 'UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, to] ); await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值