KEY-VALUE NO!!!
A table for phone numbers -- sure. It would have userid, phone_num, and (if you like) a phone type, such as ENUM('fax', 'home', ...). Then JOIN to the main table.
To keep unlimited, unsearchable data, have a column with a bunch of key-value stuff. I like to do it in JSON, then compress it (in the app), and store it into a BLOB or MEDIUMBLOB. That makes it easily accessible by the app, reasonable compact, and quite open-ended.
In the table, have only columns that you need to search on; put the rest into the extra JSON column.
More discussion:
http://forums.mysql.com/read.php?125,428546,428769#msg-428769
http://forums.mysql.com/read.php?125,402095,402218#msg-402218
Another approach is MariaDB's "dynamic columns". This even lets you index randomly added 'columns'.
2000-3000 customers -- Yawn.
This is a good "there's more than one way to do it" question.
Start by thinking about your data usage:
Think about which fields are going to be read and written more often.
For example, first name and last name are probably written once and are rarely changed, but depending on your app, they could be read very often.
Last login date could be written frequently, and you might want to keep every login date.
Things like activation key and registration date, depending on how authentication works in your app, might be written once and then very rarely read.
You should also think about what data is pulled at the same time, and try to denormalize to make those reads faster. For example, if you are always pulling username and password and first name together (but no other contact info), put firstname in the login table, too. That is, think about avoiding joins if this data will be large, and be kind to yourself.
Also think about which fields might be in the "where" clause of your queries, and index on those fields.
Here's one way I might do it:
Contact Table
(one insert, rare updates, many reads)
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| user_id | int(11) | NO | PRI | NULL | auto_increment |
| first_name | varchar(50) | NO | | NULL | |
| last_name | varchar(50) | NO | | NULL | |
| address | varchar(50) | NO | | NULL | |
| city | varchar(50) | NO | | NULL | |
| state | varchar(50) | NO | | NULL | |
| country | varchar(50) | NO | | NULL | |
| email | varchar(1024) | NO | | NULL | |
+------------+---------------+------+-----+---------+----------------+
Login Table
(one insert, maybe updates, many reads; need first_name to be echoed on login)
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| user_id | int(11) | NO | PRI | NULL | |
| username | varchar(32) | NO | | NULL | |
| password | varchar(32) | NO | | NULL | |
| first_name | varchar(50) | NO | | NULL | |
+------------+---------------+------+-----+---------+----------------+
Login History Table
(many writes, multiple entries per user_id)
+------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+----------------+
| user_id | int(11) | NO | MUL | NULL | |
| last_login | timestamp | NO | MUL | NULL | |
+------------+---------------+------+-----+---------+----------------+
Signup Table
(one write, rarely read)
+------------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+---------------+------+-----+---------+----------------+
| user_id | int(11) | NO | PRI | NULL | |
| registration_date| timestamp | NO | | NULL | |
| activation_key | varchar(50) | NO | | NULL | |
+------------------+---------------+------+-----+---------+----------------+
Best Answer
Your second method is the right way to represent the data in the database.
If you want a user/date pair to appear only once, then create a unique index/constraint to enforce this:
Tables in SQL have a pre-defined structure. Database systems (for better or worse) are not designed to hold arbitrary numbers of columns. In cases where this is a requirement (and your problem is not one of these cases), then a JSON representation (or something similar) can be used.